ΠΕΡΙΕΧΟΜΕΝΑ

 

            i.    ΟΙ POINTERS ΕΙΝΑΙ ΔΙΕΥΘΥΝΣΕΙΣ

            ii.   ΜΕΤΑΒΛΗΤΕΣ POINTER

            iii.  ΤΕΛΕΣΤΕΣ POINTER

            iv.  ΕΚΦΡΑΣΕΙΣ POINTER

                        α. Εντολές Pointer

                        β. Αριθμητική των Pointer

                        γ. Συγκρίσεις Pointer

            v.    DYNAMIC ALLOCATION FUNCTIONS

            vi .  ΠΙΝΑΚΕΣ ΚΑΙ POINTERS

                        α. Pointers σε character Arrays

                        β. Πίνακες από Pointers

            vii . POINTERS ΣΕ POINTERS

            viii. ΑΡΧΙΚΟΠΟΙΩΝΤΑΣ POINTERS

             ix.  POINTERS ΣΕ FUNCTIONS

              x.  ΠΡΟΒΛΗΜΑΤΑ ΜΕ ΤΟΥΣ POINTERS

 

 

ΚΕΦΑΛΑΙΟ 7ο

 

ΔΕΙΚΤΕΣ ΜΝΗΜΗΣ – POINTERS

 

            Η σωστή κατανόηση και χρήση των pointers είναι σημαντική στη δημιουργία των περισσότερων C προγραμμάτων για τρεις λόγους:

  1. Οι pointers αποτελούν τον τρόπο με τον οποίο οι συναρτήσεις τροποποιούν τα ορίσματα που τις κάλεσαν.
  2. Χρησιμοποιούνται στις ρουτίνες δυναμικής δέσμευσης της C.
  3. Η χρήση των pointers αυξάνει την αποτελεσματικότητα των ρουτινών.

Οι pointers αποτελούν το πιο ισχυρό στοιχείο της C, αλλά και το πιο επικίνδυνο. Π.χ. μη αρχικοποιημένοι (Wild) pointers μπορεί να προκαλέσουν την κατάρρευση του συστήματος. Ή ακόμη χειρότερα, η λάθος χρήση των pointers μπορεί να προκαλέσει “Bugs” τα οποία εντοπίζονται δύσκολα.

 

 

i)                    Οι pointers είναι διευθύνσεις

 

Ένας pointer περιέχει μία διεύθυνση μνήμης. Διεύθυνση είναι η θέση μιας μεταβλητής στη μνήμη. Αν μία μεταβλητή περιέχει τη διεύθυνση μιας άλλης μεταβλητής, μπορούμε να πούμε ότι δείχνει σε αυτή τη μεταβλητή. Π.χ. αν μία μεταβλητή στη θέση 1004 δείχνεται από μία μεταβλητή στη θέση 1000, τα περιεχόμενα της θέσης 1000 θα είναι 1004.

 

 

Δ/ση Μνήμης

Περιεχόμενα

1000

1004

1001

 

1002

 

1003

 

1004

 

 

                                                   (Μία Μεταβλητή Δείχνει σε Μία Άλλη)

 

 

 

i)                    Μεταβλητές Pointer

 

Αν μία μεταβλητή πρόκειται να δεχτεί έναν pointer, θα πρέπει να έχει δηλωθεί ως εξής: μία δήλωση pointer περιλαμβάνει ένα βασικό τύπο, ένα *, και το όνομά της μεταβλητής. Η γενική μορφή δήλωσης pointer είναι :

 

type *name

 

όπου type μπορεί να είναι κάθε αποδεκτός C τύπος (καλείται βασικός τύπος) και *name είναι το όνομα της pointer μεταβλητής.

 

            Ο βασικός τύπος του pointer διευκρινίζει τι τύπου μεταβλητές θα δείχνει ο pointer. Τεχνικά, κάθε τύπος pointer μπορεί να δείχνει οπουδήποτε στη μνήμη, αλλά η αριθμητική των pointers γίνεται σχετικά με το βασικό τύπο, έτσι είναι σημαντικό να έχει δηλωθεί σωστά ο pointer.

 

 

ii)                  Τελεστές Pointer

 

Υπάρχουν δύο ειδικοί τελεστές για τους pointers: * και &. Ο & είναι ένας μονοσήμαντος τελεστής ο οποίος επιστρέφει τη διεύθυνση μνήμης του τελεστέου του (ένας μονοσήμαντος τελεστής δέχεται μόνο έναν τελεστέο )

Π.χ.:

 

m = & count ;

 

τοποθετεί στην m τη δ/νση μνήμης της μεταβλητής count. Η δ/νση είναι η εσωτερική θέση της μεταβλητής count. Η λειτουργία του & μπορεί να ειπωθεί ότι επιστρέφει “τη διεύθυνσή του…”. Έτσι το προηγούμενο παράδειγμα μπορεί να μεταφραστεί ως εξής:

 

“Η m δέχεται τη διεύθυνση της count”.

 

            Για να αυξήσουμε την πιθανότητα κατανόησης των παραπάνω, ας υποθέσουμε ότι η μεταβλητή count χρησιμοποιεί τη διεύθυνση μνήμης 2000 για να αποθηκεύσει την τιμή της. Επίσης, η count έχει τιμή 100. έτσι μετά την παραπάνω εντολή η m θα έχει την τιμή 2000.

 

            Ο άλλος τελεστής, ο *, είναι συμπλήρωμα του πρώτου. Είναι ένας μονοσήμαντος τελεστής ο οποίος επιστρέφει την τιμή της μεταβλητής που βρίσκεται στη διεύθυνση που ακολουθεί. Αν η m περιέχει τη δ/νση μνήμης της μεταβλητής count, τότε:

 

q = * m ;

 

τοποθετεί την τιμή της count στην q. Έτσι η q έχει τιμή 100, επειδή 100 είναι αποθηκευμένο στη θέση 2000, τη δ/νση δηλαδή που είναι αποθηκευμένη στην m. Η λειτουργία του * μπορεί να χαρακτηριστεί σαν “στη διεύθυνση”. Έτσι η παραπάνω εντολή μεταφράζεται ως εξής:

“Η q δέχεται την τιμή που βρίσκεται στη διεύθυνση m.”

 

            Πρέπει να είμαστε σίγουροι ότι οι pointers που χρησιμοποιούμε δείχνουν πάντα στο σωστό τύπο δεδομένων. Π.χ. όταν δηλώνουμε έναν pointer να είναι του τύπου int, ο compiler συμπεραίνει ότι κάθε δ/νση που θα περιέχει αυτός ο pointer θα δείχνει σε δεδομένα τύπου int.

 

            Π.χ.:     float x,y;

                        int *p ;

                        p = &;

                        y = *;

 

            Αυτό το παράδειγμα ΔΕΝ  μεταφέρει τα δεδομένα του x στο y. Επειδή το p είναι int pointer μόνο 2 bytes θα περάσουν στο y (όχι 4 όπως θα έπρεπε). Έτσι αν και ο compiler θα δείξει μόνο warnings, το πρόγραμμα δε θα κάνει αυτό που εμείς θέλουμε.

 

 

 

iii)                Εκφράσεις Pointer

 

Οι εκφράσεις που περιέχουν pointers ακολουθούν τους ίδιους κανόνες όπως κάθε άλλη C έκφραση. Εδώ θα ερευνήσουμε ορισμένα καινούρια πράγματα γύρω από τις εκφράσεις που χρησιμοποιούν pointers.

 

 

a)      Εντολές Pointer

 

Όπως κάθε μεταβλητή, ένας pointer μπορεί να χρησιμοποιηθεί στη δεξιά πλευρά μιας εντολής για να καταχωρήσει την τιμή του σε ένα άλλο pointer.

 Π.χ.:

      main( )

      {

            int x;

            int *p1, *p2;

                  p1 = &;

                  p2 = p1 ;

                  printf(“%p”, p2); / * Εμφανίζει τη δεκαεξαδική τιμή της δ/νσης */

}                                            / * του x, ΟΧΙ ΤΗΝ ΤΙΜΗ ΤΟΥ * /

 

b)      Αριθμητική των Ponters

 

Μόνο δύο αριθμητικές πράξεις μπορούν να χρησιμοποιηθούν με τους pointers: πρόσθεση και αφαίρεση. Για να κατανοήσουμε τι συμβαίνει στην αριθμητική των pointers, ας θεωρήσουμε ότι η p1 είναι ένας pointer σε έναν ακέραιο με τρέχουσα τιμή 2000 (ένας ακέραιος έχει μήκος 2 bytes). Μετά την εκτέλεση της πράξης p1++; τα περιεχόμενα της p1 θα είναι 2002 και όχι 2001!! Κάθε φορά που η p1 αυξάνεται δείχνει τον αμέσως επόμενο ακέραιο. Το ίδιο συμβαίνει και με τις μειώσεις. Π.χ.: p1--; θα προκαλέσει μία μείωση κατά 2 της τιμής του p1.

 

            Κάθε φορά που ένας pointer αυξάνεται, δείχνει στη θέση μνήμης του επόμενου στοιχείου του βασικού του τύπου. Κάθε φορά που μειώνεται δείχνει στη θέση του προηγούμενου. Στην περίπτωση που χειριζόμαστε pointers σε χαρακτήρες, τα πράγματα, μοιάζουν με την κανονική αριθμητική. Έτσι, όλοι οι άλλοι pointers αυξάνονται και μειώνονται με βάση το μέγεθος του βασικού τύπου στον οποίο δείχνουν. Για παράδειγμα, θεωρώντας χαρακτήρες 1 – byte, και ακεραίους 2 – bytes, όταν ένας char pointer αυξάνει, αυξάνει κατά 1, ενώ ο int pointer αυξάνει κατά 2. Ο λόγος γι’ αυτό είναι ότι κάθε φορά που ένας pointer αυξάνει ή μειώνεται, αυτό γίνεται σε σχέση με το μέγεθος του βασικού του τύπου, έτσι ώστε να δείχνει πάντα στο επόμενο στοιχείο. Πιο γενικά, η αριθμητική των pointers γίνεται σχετικά με το βασικό τύπο του pointer, έτσι αυτός δείχνει πάντα στο ανάλογο στοιχείο του βασικού τύπου.

 

                                    char *ch = 3000;

                                    int *i = 3000;

 

ch

3000

i

ch + 1

3001

ch + 2

3002

i + 1

ch + 3

3003

ch + 4

3004

i + 2

ch + 5

3005

 

 

            Δεν υπάρχει κανείς περιορισμός (ουσιαστικά) στην αύξηση ή μείωση των pointers. Επίσης μπορούμε μόνο να προσθέσουμε ή να αφαιρέσουμε ακεραίους από pointers. Π.χ.: p1 = p1 + 9; θα δείξει στο 9ο στοιχείο του ίδιου τύπου με το p1. Καμία άλλη αριθμητική πράξη δε μπορεί να γίνει με pointers. Δε μπορούμε να πολλαπλασιάσουμε ή να διαιρέσουμε pointers. Δε μπορούμε να αφαιρέσουμε δύο pointers. Δε μπορούμε να χρησιμοποιήσουμε bitwise operators μαζί τους, ούτε shift operators. Δε μπορούμε να προσθέσουμε ή να αφαιρέσουμε float ή double τύπους με pointers.

 

 

c)      Συγκρίσεις Pointers

 

Είναι δυνατό να συγκρίνουμε δύο pointers σε μία σχεσιακή έκφραση. Π.χ. θεωρώντας δύο pointers, τους p και q η παρακάτω δήλωση είναι απολύτως αποδεκτή:

 

if (p<q) printf(“ p points to lower memory than q \n”);

 

Υπάρχουν και κάποια άλλα ειδικά προβλήματα τα οποία σχετίζονται με τις συγκρίσεις far pointers στη C. Οι συγκρίσεις pointers χρησιμοποιούνται όταν δύο ή περισσότεροι pointers δείχνουν σε ένα κοινό αντικείμενο. Σαν παράδειγμα φανταστείτε ένα stack που κρατά ακέραιες τιμές. Ένα stack είναι μία λίστα που χρησιμοποιεί προσπέλαση First InLast Out. Για τη δημιουργία μιας στοίβας χρειάζονται δύο ρουτίνες, η push( ) και η pop( ). Η push( ) χρησιμοποιείται για να δώσει τιμές στη στοίβα και η pop( ) τις αφαιρεί. Η μνήμη για τη στοίβα δεσμεύεται από το Heap. Η μεταβλητή tos κρατά τη δ/νση μνήμης του υψηλότερου σημείου της στοίβας και χρησιμοποιείται για να εμποδίσει τις υποχειλίσεις στοίβας (stack underflows).

 

Π.χ.:

 

#include <stdlib.h>

int *p1, *tos;

main( )

{

      int  value;

      p1 = (int *) malloc (50 * sizeof(int) );

      if (!p1)  {

            printf( “ Allocation Failure \n”);

            return;

}

tos = p1;

do   {

      scanf (“%d”, &value);

      if (value! = 0) push(value);

      else printf(“This Is It %d \n”, pop( ));

      }

}

push(int i)

{

      p1++;

      if (p1==(tos + 50)) {

            printf (“Stack Overflow”);

            exit(1);

      }

      *p1 = i;

}

pop( )

{

      if ( (p1) == tos)   {

            printf (“Stack Underflow”);

            exit(1);

      }

      p1--;

      return *(p1+1);

}

 

Οι συναρτήσεις push( ) και pop( ) εκτελούν ένα σχεσιακό τεστ στον pointer p1 για να ελέγξουν σφάλματα ορίων. Στην push( ) ο p1 ελέγχεται με το τέλος της στοίβας προσθέτοντας το 50 (το όριο της στοίβας) στο tos. Στην pop( ) ελέγχεται με το tos για να σιγουρευτεί ότι δεν έχει προκληθεί υποχείλιση στοίβας. Στην pop( ) οι παρενθέσεις στο return είναι απαραίτητες γιατί η δήλωση return *p1 + 1; θα επέστρεφε τιμή στη δ/νση p1 αυξημένη κατά 1 και όχι την τιμή στη θέση p1 + 1. Πρέπει να είμαστε προσεκτικοί με τη χρήση pointers και να χρησιμοποιούμε παρενθέσεις.

 

 

iv)               Dynamic Allocation Functions

 

Από τη στιγμή που θα μεταφραστούν όλα τα προγράμματα C οργανώνουν τη μνήμη του υπολογιστή σε τέσσερα τμήματα, τα οποία κρατούν τον κώδικα (code), τα συνολικά δεδομένα (global data), τη στοίβα (stack), και το σωρό (heap). To heap είναι μία περιοχή από ελεύθερη μνήμη η οποία διαχειρίζεται από τις συναρτήσεις malloc( ) και free( ).

 

H malloc( ) δεσμεύει μνήμη και επιστρέφει έναν char pointer στην αρχή της, ενώ η free( ) αποδεσμεύει την προηγουμένως δεσμευθείσα μνήμη από το heap. Οι γενικές μορφές σύνταξής τους είναι:

 

void *malloc (num_bytes);

free (void *p);

 

Εδώ, το num_bytes, είναι ο αριθμός των απαιτούμενων bytes. Εάν δεν υπάρχει αρκετή μνήμη για την εξυπηρέτηση της απαίτησης του χρήστη, η malloc( ) επιστρέφει null. Είναι σημαντικό ότι η free( ) θα καλείται μόνο με τις αποδεκτές προηγουμένως δεσμευμένες τιμές pointer, αλλιώς η οργάνωση του heap μπορεί να προκαλέσει κατάρρευση του συστήματος. Π.χ.:

 

            char *p;

            p = (char *) malloc (1000);   / * Get 1000 Bytes  * /

 

Μετά την εκτέλεση αυτής της εντολής η p δείχνει στα πρώτα 1000 bytes ελεύθερης μνήμης.  Σημειώνεται ότι πρέπει να χρησιμοποιούμε ένα cast με τη malloc( ) έτσι ώστε ο pointer να μετατρέπεται στον ανάλογο τύπο. Π.χ.:

           

            int *p;

            p = (int *) malloc( 50 * sizeof (int) );

 

Εδώ χρησιμοποιούμε τη sizeof για να σιγουρέψουμε τη μεταφερσιμότητα του κώδικα.

 

Εφόσον το heap δεν είναι ατελείωτο, όταν δεσμεύουμε μνήμη, καλό είναι να ελέγχουμε την τιμή που επιστρέφει η malloc( ) για να σιγουρέψουμε ότι δε θα χρησιμοποιηθεί ένας null pointer. Η χρήση ενός null pointer τις περισσότερες φορές θα προκαλέσει κατάρρευση του συστήματος. Ο πιο σωστός τρόπος δέσμευσης μνήμης, έτσι ώστε να ελέγχουμε για ένα αποδεκτό pointer, είναι ο παρακάτω:

 

            if (! (p = malloc(100) )   {

                  printf (“Out Of Memory ! \n”);

                  exit(1);

            }

           

Φυσικά υπάρχουν κι άλλοι τρόποι διαχείρισης τέτοιων λαθών αντί για τη χρήση της exit( ). Το σίγουρο είναι ότι δε θέλουμε να χρησιμοποιηθεί ο pointer p αν είναι null.

 

Όταν χρησιμοποιούμε τις δύο αυτές συναρτήσεις  ( malloc( ) και free( ) ), θα πρέπει να ενσωματώνουμε το stdlib.h επειδή αυτό περιέχει τα πρωτότυπά τους.

 

 

 

v)                 ­Πίνακες και Pointers

 

Υπάρχει μία στενή σχέση μεταξύ πινάκων και pointers. Ας δούμε το παρακάτω τμήμα κώδικα:

 

         char str[80], *p1;

         p1 = str;

 

Εδώ το p1 δέχεται τη δ/νση μνήμης του πρώτου στοιχείου του πίνακα str. Αν θέλουμε να προσπελάσουμε το 5ο στοιχείο του πίνακα str μπορούμε να γράψουμε:

 

str [ 4 ] ή *(p1 + 4)

 

Και οι δύο εκφράσεις επιστρέφουν το 5ο στοιχείο. Υπενθυμίζεται ότι οι πίνακες ξεκινούν από το 0, έτσι το 4 χρησιμοποιείται σαν δείκτης του πίνακα. Επίσης θα πρέπει να προσθέσουμε 4 στον pointer p1, επειδή αυτός ήδη δείχνει στο πρώτο στοιχείο του string. Βλέπουμε λοιπόν ότι η C επιτρέπει δύο μεθόδους προσπέλασης στοιχείων πινάκων. Αυτό είναι σημαντικό γιατί η αριθμητική των pointers μπορεί να είναι γρηγορότερη από την δεικτοθέτηση πινάκων. Εφόσον η ταχύτητα είναι σημαντικό στοιχείο του προγραμματισμού, η χρήση pointers στην προσπέλαση στοιχείων πινάκων είναι αρκετά συχνή στα προγράμματα C.

 

Π.χ.:

 

puts (char *s)                       / * Με Arrays  *  /

{

      register int t;

      for (t=0; s[t]; ++t;)  putch (s[ t ]);

}

puts (char *s)                       /  * Με Pointers  * /

{

      while (*s) putch (*s++);

}

 

Τις περισσότερες φορές τέτοιου είδους ρουτίνες γράφονται με τη δεύτερη μέθοδο. Ειδικά χρησιμοποιούμε pointers όταν θέλουμε να προσπελάσουμε τον πίνακα σειριακά με αύξουσα ή φθίνουσα διάταξη.

 

Όταν όμως θελήσουμε να κάνουμε τυχαία προσπέλαση τότε είναι καλύτερο να χρησιμοποιηθεί array indexing, αφού τις περισσότερες φορές η εκτίμηση μιας σύνθετης έκφρασης με pointers είναι σχετικά δύσκολη και δυσνόητη στον αναγνώστη. Επίσης όταν χρησιμοποιούμε πίνακες αναθέτουμε λίγη από τη δική μας εργασία στον compiler.


 

­α) Pointers σε Character Arrays

 

Οι περισσότερες ενέργειες που έχουν σχέση με string, στη C εκτελούνται με τη χρήση pointers και αριθμητικής pointer, επειδή τα string είναι φτιαγμένα να προσπελαύνονται σε σειριακή μορφή. Π.χ. εδώ έχουμε ένα παράδειγμα της συνάρτησης strcmp( ):

 

 strcmp ( char *s1, char *s2);

      {

            while(*s1)

               if (*s1 - *s2)

                     return  *s1 - * s2;

               else

                     {

                         s1++;

                         s2++;

                     }

               return ‘ \0 ’;                        /  * Ίσα   *  /

      }

 

Υπενθυμίζεται ότι όλα τα strings στη C τερματίζονται με null το οποίο και αποτελεί ψευδή τιμή. Έτσι η έκφραση while(*s1) είναι αληθής έως ότου ανιχνευθεί το τέλος του string. Στο παράδειγμά μας η strcmp( ) θα επιστρέψει 0 αν το s1 είναι ίσο με το s2, θα επιστρέψει αρνητική τιμή αν το s1 είναι μικρότερο του s2, και θετική τιμή αν το s1 είναι μεγαλύτερο του s2.

 

Στη συνάρτηση strcmp( ) τα s1 και s2 είναι τοπικές μεταβλητές. Μπορούν να αλλάξουν χωρίς να επηρεάσουν τις καλούμενες τιμές.

 

Π.χ.:

 

main ( )

{

   char *p1, s [80];

   do    {

            p1 = s;

            gets (s);

            while (*p1) printf (“%d”, *p1++);

   } while (!strcmp( s, “done”) );

}

 

Εδώ κάθε φορά που ο βρόγχος εκτελείται, το p1 τίθεται στην αρχή του string. Το πρόγραμμα τελειώνει όταν εισαχθεί τιμή ίση με done.

 

 

­β) Πίνακες από Pointers

 

Οι pointers μπορούν να τοποθετηθούν σε πίνακες όπως όλοι οι άλλοι τύποι δεδομένων. Η δήλωση ενός πίνακα από pointers 10 στοιχείων θα είναι:

 

int *x [10];

 

Για να αποδώσουμε τη δ/νση ενός ακεραίου ο οποίος καλείται var στο τρίτο στοιχείο του pointer array, θα γράψουμε:

 

x [2] = &var;

 

Για να βρούμε την τιμή του var θα γράψουμε:

 

*x [2]

 

Αν χρειαστεί να περάσουμε έναν πίνακα από pointers σε μία συνάρτηση, μπορούμε να χρησιμοποιήσουμε την ίδια μέθοδο που χρησιμοποιήσαμε και για τους άλλους πίνακες, απλά καλώντας τη συνάρτηση με το όνομα του πίνακα, χωρίς δείκτες.

 

Π.χ.:

         display_array (q)

         int  *q[ ] ;

         {

            int t;

            for  (t = 0;  t<10;  t++)  printf(“%d”,  *q [ t ]);

         }

 

Το q δεν είναι ένας pointer σε ακεραίους, αλλά ένας πίνακας από pointers σε ακεραίους. Έτσι είναι απαραίτητο να δηλώσουμε την παράμετρο q σαν array of integer pointers. Δεν πρέπει να δηλωθεί απλά σαν pointer σε ακέραιο γιατί δεν είναι αυτό.

 

Μία συχνή χρήση των pointers arrays είναι να κρατούν pointers σε μηνύματα λαθών. Μπορούμε να δημιουργήσουμε μία συνάρτηση η οποία θα εξάγει ένα μήνυμα δίνοντας τον κωδικό λάθους τους. Π.χ.

 

serror (int num)

{

      static char  * err[ ] =    {

                  “Cannot Open File \n”,

                  “Read Error           \n”,

                  “Write Error           \n”,

                  “Media Failure       \n”,

      }

      printf(“%s, err [num]);

}

 

Όπως βλέπουμε η printf( ) μέσα στην serror( ) καλείται με ένα char pointer ο οποίος δείχνει σε ένα από τα μηνύματα λάθους, με δείκτη τον κωδικό λάθους ο οποίος έχει περαστεί σαν όρισμα στη συνάρτηση. Π.χ. αν περαστεί στο num ο αριθμός 2, θα εμφανιστεί το μήνυμα “Write Error”. Είναι σημαντικό να σημειωθεί ότι το command-line argument argv είναι πίνακας από character pointers.

 

 

vi)               ­Pointers σε Pointers

 

Ένας πίνακας από pointers είναι κάτι που μοιάζει με τη φράση pointers σε pointers. Η έννοια των πινάκων pointers είναι μία ευθεία και αυτό γιατί οι δείκτες ξεκαθαρίζουν την υπόθεση. Πάντως, οι pointers σε pointers μπορεί να είναι ιδιαίτερα πολύπλοκοι και δύσκολοι στην κατανόηση.

 

Ένας pointer σε pointer αποτελεί μία αλυσίδα από pointers. Π.χ.:

 

                     Pointer                                     Variable

 

 


     i) Single Inirection

Pointer                                  Pointer                             Variable

Πλαίσιο κειμένου: Address Πλαίσιο κειμένου: Value
 

 

 


ii) Multiple Indirection

 

Μία μεταβλητή η οποία είναι pointer to pointer πρέπει να έχει δηλωθεί ανάλογα. Αυτό γίνεται τοποθετώντας ακόμη έναν αστερίσκο μπροστά από το όνομα. Π.χ. η δήλωση αυτή λέει στον compiler ότι η newbalance είναι ένας pointer σε έναν pointer τύπου float.

float * * newbalance;

 

Είναι σημαντικό να κατανοηθεί ότι η newbalance δεν είναι ένας pointer σε ένα floating point αριθμό, αλλά ένας pointerσε ένα float pointer. Π.χ.:

 

main ( )

{

   int x, *p, **q;

   x = 10 ;

   p = &x ;

   q = &p ;

   printf (“%d”, **q);

}

 

Η printf( ) θα εμφανίσει 10 στην οθόνη. Γιατί το p είναι pointer σε ακέραιο και το q είναι pointer σε int pointer.

 

 

vii)             ­Αρχικοποιώντας Pointers

 

 

Μετά τη δήλωσή του, ένας pointer (πριν όμως τη χρήση του, και την ανάθεση σε αυτόν μιας τιμής), περιέχει μία άγνωστη τιμή. Σε περίπτωση που προσπαθήσουμε να τον χρησιμοποιήσουμε πριν του δώσουμε τιμή, ενδέχεται να καταστρέψουμε όχι μόνο το πρόγραμμά μας, αλλά και το λειτουργικό σύστημα του η/υ ( το χειρότερο είδος σφάλματος).

 

Σε ένα pointer ο οποίος δε δείχνει πουθενά πρέπει να δίνουμε την τιμή null για να σιγουρευτούμε ότι δε δείχνει πουθενά. Πάντως, το γεγονός ότι ένας pointer έχει τιμή null δε μας κάνει ασφαλείς. Η χρήση ενός null pointer, στα αριστερά μιας εντολής προκαλεί crash του προγράμματος ή και του λειτουργικού συστήματος.

 

Επειδή ένας null pointer εξ’ ορισμού δε χρησιμοποιείται, μπορούμε να χρησιμοποιήσουμε τον null pointer για να κάνουμε πιο αποτελεσματικές κάποιες από τις ρουτίνες μας. Π.χ. μπορούμε να χρησιμοποιήσουμε τον null pointer για να μαρκάρουμε το τέλος ενός pointer array. Αν γίνει αυτό, μία ρουτίνα η οποία προσπελαύνει τον πίνακα θα ξέρει ότι έχει φτάσει στο τέλος του πίνακα όταν συναντήσει το null. Π.χ.:

 

search (char  *p [ ], char *name)

{

   register int i;

   for (t = 0;  p[ t ];  ++t)

         if (!strcmp (p [t], name) ) return t;

   return –1;  / * Δεν βρέθηκε  */

}

 

Ο βρόγχος for μέσα στην search( ) εκτελείται έως ότου βρεθεί μία ομοιότητα ή ένας null pointer. Επειδή το τέλος του πίνακα είναι σημαδεμένο με ένα null η συνθήκη που ελέγχει το loop αποτυγχάνει όταν συναντήσει το null.

 

Είναι συχνό φαινόμενο σε επαγγελματικά γραμμένα προγράμματα να αρχικοποιούμε strings.

 

π.χ.:

char *p = “hello world \n”;

 

Όπως βλέπουμε ο pointer p δεν είναι πίνακας. Ο λόγος για τον οποίο αυτό το είδος αρχικοποίησης εργάζεται σωστά, έχει να κάνει με τον τρόπο που λειτουργεί η Turbo C. Όλοι οι C compilers δημιουργούν αυτό που λέμε string table, το οποίο χρησιμοποιείται εσωτερικά από τον compiler για αποθήκευση string σταθερών που χρησιμοποιεί το πρόγραμμα. Έτσι η προηγούμενη δήλωση, καταχωρεί τη δν/ση του “hello world” στον pointer p. Από εδώ και πέρα το p μπορεί να χρησιμοποιηθεί όπως κάθε άλλη μεταβλητή τύπου string. Π.χ.:

 

char *p = “Hello World”;

main( )

{

   register int ii;

    / * Print The String Forward and Backwards  * /

   printf ( p );

   for (t=strlen(p)-1; t>-1; t--)

         printf (“%c”, p [t]);

}

 

 

viii)           ­Pointers σε Functions

 

 

Μία συνάρτηση δεν είναι μεταβλητή, παρ’ όλ’ αυτά έχει μία φυσική θέση στη μνήμη η οποία μπορεί να αποδοθεί σε ένα pointer. Ένας function pointer είναι το σημείο εισόδου σε μία συνάρτηση. Εξαιτίας αυτού ένας function pointer μπορεί να χρησιμοποιηθεί για να κληθεί μία συνάρτηση.

 

Σε πολλά προγράμματα ο χρήστης μπορεί να επιλέξει μεταξύ διαφόρων επιλογών. Για παράδειγμα σε ένα πρόγραμμα λογιστικής μπορεί να έχουμε ένα μενού με 20 επιλογές. Από τη στιγμή που θα γίνει η επιλογή, η ρουτίνα που οδηγεί την εκτέλεση του προγράμματος, μπορεί να χειριστεί τα πράγματα με δύο τρόπους. Πρώτα απ’ όλα μπορεί να χρησιμοποιήσει τη δομή case. Πάντως σε εφαρμογές που απαιτούν καλύτερη εκτέλεση υπάρχει κι άλλος τρόπος. Να δημιουργηθεί ένας array of pointers με κάθε pointer να δείχνει τη διεύθυνση μίας συνάρτησης. Η επιλογή που γίνεται από το χρήστη κωδικοποιείται και χρησιμοποιείται σαν δείκτης μέσα στον pointer array, καλώντας την εκτέλεση της αντίστοιχης συνάρτησης. Η μέθοδος αυτή μπορεί να είναι αρκετά γρήγορη.

 

Π.χ. ας φανταστούμε μια απλή βάση αποθήκης στην οποία μπορούμε να εκτελέσουμε enter, delete, review, quit, θα έχουμε με βάση τα παραπάνω:

 

         int enter( ), delete( ), review( ), quit( );

         int *options [ ] = {

                     (int * ) enter,

                     (int * ) delete,

                     (int * ) review,

                     (int * ) quit

         };

 

Τα casts που χρησιμοποιούνται αποτρέπουν τα warnings του compiler αλλά τεχνικά δεν είναι απαραίτητα.

 

Στην πραγματικότητα οι ρουτίνες διαχείρισης της αποθήκης δεν έχουν ακόμη δημιουργηθεί. Το παρακάτω πρόγραμμα διευκρινίζει τον σωστό τρόπο για να εκτελούμε συναρτήσεις με χρήση function pointers. Παρατηρήστε πως η συνάρτηση menu( ) λειτουργεί και αυτομάτως επιστρέφει τον απαραίτητο δείκτη στον function pointer.

 

Π.χ.:

#include <stdio.h>

int enter( ), delete( ), review( ), quit( );

int *options [ ] =   {

         (int * ) enter,

         (int * ) delete,

         (int * ) review,

         (int * ) quit

};

main ( )

{

         int i;

         i = menu( );                 /  * Επιλογή του χρήστη  * /

         process (options [i] );

}

menu( )

{

         char ch;

         do   {

                     printf ( “ 1.   Enter  \n”);

                     printf ( “ 2.   Delete  \n”);

                     printf ( “ 3.   Review  \n”);

                     printf ( “ 4.   Quit  \n”);

                     printf ( “ Select A Number : ”);

                     ch = getche( );

                     printf( “ \ n “ );

         }     while (! strchr (“1234”, ch) );

         return ch – 49;             /  * Μετατροπή σε ακέραιο  * /

}

process ( int ( * f)  ( ) )

{

         ( * f) ( ) ;

}

enter( )

{

         printf( “In Enter( )”);

}

delete( )

{

         printf( “In Delete( )”);

}

review( )

{

         printf( “In Review( )”);

}

quit( )

{

         printf(“In Quit( )”);

         exit (0);

}

 

Το πρόγραμμα αυτό εργάζεται ως εξής: Το menu εμφανίζεται και ο χρήστης εισάγει έναν αριθμό ανάλογα με την επιλογή που επιθυμεί. Εφόσον ο αριθμός εκφράζεται σε ASCII, αφαιρούμε από αυτόν 49 (δεκαδική τιμή του 0) και τον μετατρέπουμε σε δυαδικό ακέραιο. Η τιμή αυτή επιστρέφεται στο main( ) και χρησιμοποιείται σα δείκτης στον πίνακα options[ ], ο οποίος και περιέχει τους function pointers. Μετά η κλήση της process( ) εκτελεί την αντίστοιχη συνάρτηση.

 

Η χρήση function pointers είναι κοινή όχι μόνο στους interpeters και τους compilers αλλά και σε προγράμματα βάσεων δεδομένων επειδή συχνά αυτά τα προγράμματα προβάλλουν ένα μεγάλο αριθμό επιλογών και η αποτελεσματικότητα είναι σημαντική.

 

 

ix)                ­Προβλήματα με τους Pointers

 

 

Οι pointers είναι απαραίτητοι σε πολλά προγράμματα, και τους δίνουν αρκετή δύναμη. Όταν όμως ένας pointer κατά λάθος περιέχει λάθος τιμή, μπορεί να αποτελέσει το πιο δύσκολο στον εντοπισμό bug. Ο pointer δεν είναι το πρόβλημα, το πρόβλημα είναι ότι κάθε φορά που εκτελούμε μία διαδικασία χρησιμοποιώντας pointers, γράφουμε ή διαβάζουμε σε κάποιο άγνωστο τμήμα της μνήμης. Αν διαβάσουμε τον pointer το χειρότερο που μπορεί να πάθουμε είναι να διαβάσουμε «σκουπίδια» μνήμης. Αν γράψουμε όμως σ’ αυτόν μπορεί να γράψουμε πάνω σε κάποιο τμήμα του κώδικα ή των δεδομένων μας. Αυτό θα φανεί αργότερα και ίσως μας οδηγήσει να ερευνήσουμε λάθος σημείο για το bug.

 

Επειδή τα λάθη με pointers συχνά καταλήγουν σε εφιάλτες, θα πρέπει να κάνουμε ότι είναι δυνατό ώστε να μην τα προκαλούμε. Το πιο κλασικό λάθος με pointers είναι οι μη αρχικοποιημένοι pointers. Π.χ.:

 

main( )  /  * ΛΑΘΟΣ ΠΡΟΓΡΑΜΜΑ  * /

{

      int x, *p;

      x = 10 ;

      *p = ;

}

 

Το πρόγραμμα αυτό αποδίδει την τιμή 10 σε κάποιο μη γνωστό τμήμα της μνήμης. Ο pointer p, ποτέ δεν πήρε κάποια τιμή, οπότε περιέχει «σκουπίδια» μνήμης. Συνήθως, στα μικρά προγράμματα αυτό περνά απαρατήρητο επειδή ο p περιέχει μία ασφαλή τιμή έξω από την περιοχή του προγράμματος, των δεδομένων ή του λειτουργικού συστήματος. Πάντως καθώς ο κώδικας μεγαλώνει, αυξάνεται και η πιθανότητα πρόκλησης σφάλματος. Η λύση σ’ αυτό το πρόβλημα είναι να σιγουρέψουμε ότι ο pointer δείχνει σε κάποιο σωστό σημείο πριν χρησιμοποιηθεί.

 

Ένα άλλο πιο κοινό λάθος προκαλείται από την απλή παρανόηση του τρόπου χρήσης των pointers. Π.χ.:

main( )   /  * ΛΑΘΟΣ ΠΡΟΓΡΑΜΜΑ  * /

{

                                    int x, *p;

                                    x = 10 ;

                                    p = ;

                                    printf (“%d”, *p);

                                 }

Η κλήση της printf( ) δε θα εμφανίσει στην οθόνη την τιμή του x, που είναι 10, στην οθόνη. Δείχνει σε κάποια άγνωστη τιμή επειδή η εντολή p = x; είναι λάθος. Η εντολή αυτή καταχώρησε την τιμή 10 στον pointer p, ο οποίος θα έπρεπε να περιέχει μια δ/νση και όχι τιμή. Για να κάνουμε το πρόγραμμα σωστό θα πρέπει να γράψουμε p = &x;

 

Το θέμα ότι οι pointers μπορούν να προκαλέσουν περίεργα bugs αν τους χειριστούμε εσφαλμένα, δεν αποτελεί λόγο να αποφεύγουμε τη χρήση τους. Απλά θα πρέπει να είμαστε προσεκτικοί μαζί τους και να σιγουρευτούμε από πριν που δείχνουν για να τους χρησιμοποιήσουμε.