ΠΕΡΙΕΧΟΜΕΝΑ
i. ΟΙ POINTERS ΕΙΝΑΙ ΔΙΕΥΘΥΝΣΕΙΣ
iii. ΤΕΛΕΣΤΕΣ POINTER
v. DYNAMIC ALLOCATION FUNCTIONS
vi . ΠΙΝΑΚΕΣ ΚΑΙ POINTERS
α.
Pointers σε character Arrays
vii . POINTERS
ΣΕ POINTERS
viii. ΑΡΧΙΚΟΠΟΙΩΝΤΑΣ POINTERS
x. ΠΡΟΒΛΗΜΑΤΑ ΜΕ ΤΟΥΣ POINTERS
ΚΕΦΑΛΑΙΟ 7ο
ΔΕΙΚΤΕΣ ΜΝΗΜΗΣ – POINTERS
Η
σωστή κατανόηση και χρήση των pointers είναι
σημαντική στη δημιουργία των περισσότερων C προγραμμάτων για τρεις λόγους:
Οι pointers αποτελούν το πιο ισχυρό
στοιχείο της C, αλλά και το πιο επικίνδυνο. Π.χ. μη αρχικοποιημένοι (Wild) pointers μπορεί να
προκαλέσουν την κατάρρευση του συστήματος. Ή ακόμη χειρότερα, η λάθος χρήση των
pointers μπορεί να
προκαλέσει “Bugs” τα οποία
εντοπίζονται δύσκολα.
i)
Οι pointers είναι διευθύνσεις
Ένας pointer περιέχει μία διεύθυνση μνήμης.
Διεύθυνση είναι η θέση μιας μεταβλητής στη μνήμη. Αν μία μεταβλητή περιέχει τη
διεύθυνση μιας άλλης μεταβλητής, μπορούμε να πούμε ότι δείχνει σε αυτή τη
μεταβλητή. Π.χ. αν μία μεταβλητή στη θέση 1004 δείχνεται από μία μεταβλητή στη
θέση 1000, τα περιεχόμενα της θέσης 1000 θα είναι 1004.
|
Δ/ση Μνήμης |
Περιεχόμενα |
|
1000 |
1004 |
|
1001 |
|
|
1002 |
|
|
1003 |
|
|
1004 |
|
(Μία Μεταβλητή Δείχνει σε Μία Άλλη)
Αν μία
μεταβλητή πρόκειται να δεχτεί έναν pointer, θα
πρέπει να έχει δηλωθεί ως εξής: μία δήλωση pointer περιλαμβάνει ένα βασικό τύπο, ένα *, και το όνομά της
μεταβλητής. Η γενική μορφή δήλωσης pointer είναι :
type *name
όπου type μπορεί να είναι κάθε αποδεκτός C τύπος (καλείται βασικός
τύπος) και *name είναι το
όνομα της pointer
μεταβλητής.
Ο
βασικός τύπος του pointer
διευκρινίζει τι τύπου μεταβλητές θα δείχνει ο pointer. Τεχνικά, κάθε τύπος pointer μπορεί να δείχνει οπουδήποτε στη μνήμη, αλλά η αριθμητική
των pointers γίνεται
σχετικά με το βασικό τύπο, έτσι είναι σημαντικό να έχει δηλωθεί σωστά ο 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 = &x ;
y = *p ;
Αυτό
το παράδειγμα ΔΕΝ μεταφέρει τα
δεδομένα του x στο y. Επειδή το p είναι int pointer μόνο 2 bytes θα
περάσουν στο y (όχι 4
όπως θα έπρεπε). Έτσι αν και ο compiler θα δείξει
μόνο warnings, το
πρόγραμμα δε θα κάνει αυτό που εμείς θέλουμε.
Οι
εκφράσεις που περιέχουν pointers
ακολουθούν τους ίδιους κανόνες όπως κάθε άλλη C έκφραση. Εδώ θα ερευνήσουμε ορισμένα καινούρια
πράγματα γύρω από τις εκφράσεις που χρησιμοποιούν pointers.
Όπως κάθε
μεταβλητή, ένας pointer μπορεί να
χρησιμοποιηθεί στη δεξιά πλευρά μιας εντολής για να καταχωρήσει την τιμή του σε
ένα άλλο pointer.
Π.χ.:
main( )
{
int x;
int *p1,
*p2;
p1 = &x ;
p2 = p1 ;
printf(“%p”, p2); / * Εμφανίζει τη δεκαεξαδική τιμή της δ/νσης */
} / * του x, ΟΧΙ ΤΗΝ ΤΙΜΗ ΤΟΥ * /
Μόνο δύο
αριθμητικές πράξεις μπορούν να χρησιμοποιηθούν με τους 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.
Είναι
δυνατό να συγκρίνουμε δύο pointers
σε μία σχεσιακή έκφραση. Π.χ. θεωρώντας δύο pointers, τους p και q η παρακάτω δήλωση είναι απολύτως αποδεκτή:
if (p<q) printf(“ p points to lower memory than q
\n”);
Υπάρχουν
και κάποια άλλα ειδικά προβλήματα τα οποία σχετίζονται με τις συγκρίσεις far pointers στη C. Οι συγκρίσεις pointers χρησιμοποιούνται όταν δύο ή περισσότεροι pointers δείχνουν σε ένα κοινό
αντικείμενο. Σαν παράδειγμα φανταστείτε ένα stack που κρατά ακέραιες τιμές. Ένα stack είναι μία λίστα που χρησιμοποιεί προσπέλαση First In – Last
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 επειδή
αυτό περιέχει τα πρωτότυπά τους.
Υπάρχει
μία στενή σχέση μεταξύ πινάκων και 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 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.
Ένας
πίνακας από pointers είναι
κάτι που μοιάζει με τη φράση pointers σε pointers. Η έννοια των πινάκων pointers είναι μία ευθεία και αυτό γιατί
οι δείκτες ξεκαθαρίζουν την υπόθεση. Πάντως, οι pointers σε pointers μπορεί να
είναι ιδιαίτερα πολύπλοκοι και δύσκολοι στην κατανόηση.
Ένας pointer σε pointer αποτελεί μία αλυσίδα από pointers. Π.χ.:
Pointer Variable
![]()
i) Single
Inirection
Pointer Pointer Variable
![]()
![]()
![]()
![]()
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.
Μετά τη
δήλωσή του, ένας 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]);
}
Μία συνάρτηση δεν είναι μεταβλητή, παρ’ όλ’ αυτά έχει μία φυσική θέση στη μνήμη η οποία μπορεί να αποδοθεί σε ένα 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 = x ;
}
Το πρόγραμμα αυτό αποδίδει την τιμή 10 σε κάποιο μη γνωστό τμήμα της μνήμης. Ο pointer p, ποτέ δεν πήρε κάποια τιμή, οπότε περιέχει «σκουπίδια» μνήμης. Συνήθως, στα μικρά προγράμματα αυτό περνά απαρατήρητο επειδή ο p περιέχει μία ασφαλή τιμή έξω από την περιοχή του προγράμματος, των δεδομένων ή του λειτουργικού συστήματος. Πάντως καθώς ο κώδικας μεγαλώνει, αυξάνεται και η πιθανότητα πρόκλησης σφάλματος. Η λύση σ’ αυτό το πρόβλημα είναι να σιγουρέψουμε ότι ο pointer δείχνει σε κάποιο σωστό σημείο πριν χρησιμοποιηθεί.
Ένα άλλο πιο κοινό λάθος προκαλείται από την απλή παρανόηση του τρόπου χρήσης των pointers. Π.χ.:
main( ) / * ΛΑΘΟΣ ΠΡΟΓΡΑΜΜΑ * /
{
int x, *p;
x = 10 ;
p = x ;
printf (“%d”, *p);
}
Η κλήση της printf( ) δε θα εμφανίσει στην οθόνη την τιμή του x, που είναι 10, στην οθόνη. Δείχνει σε κάποια άγνωστη τιμή επειδή η εντολή p = x; είναι λάθος. Η εντολή αυτή καταχώρησε την τιμή 10 στον pointer p, ο οποίος θα έπρεπε να περιέχει μια δ/νση και όχι τιμή. Για να κάνουμε το πρόγραμμα σωστό θα πρέπει να γράψουμε p = &x;
Το θέμα ότι οι pointers μπορούν να προκαλέσουν περίεργα bugs αν τους χειριστούμε εσφαλμένα, δεν αποτελεί λόγο να αποφεύγουμε τη χρήση τους. Απλά θα πρέπει να είμαστε προσεκτικοί μαζί τους και να σιγουρευτούμε από πριν που δείχνουν για να τους χρησιμοποιήσουμε.