Τμήμα Ηλεκτρονικών Μηχανικών Τ.Ε.Ι. Κρήτης Προγραμματισμός Η/Υ (ΤΛ2007 ) Δρ. Μηχ. Νικόλαος Πετράκης (npet@chania.teicrete.gr) Ιστοσελίδα Μαθήματος: https://eclass.chania.teicrete.gr/ Εξάμηνο: Εαρινό 2014-15
Δείκτες και διατάξεις (πίνακες) Η σχέση των διατάξεων και των δεικτών είναι πολύ σημαντική Εάν θυμηθούμε τη δήλωση μίας διάταξης float a[10]; /* Μονοδιάστατη διάταξη 10 στοιχείων τύπου float */ a[0] a[1]... a[9] Ουσιαστικά η μεταβλητή a υποδηλώνει τη διεύθυνση του πρώτου του στοιχείου (δηλαδή του a[0]), και μετά τόσες θέσεις μνήμης όσες να χωρέσουν 10 πραγματικοί αριθμοί κινητής υποδιαστολής (float). Με αυτό τον τρόπο η C απεικονίζει εσωτερικά τους πίνακες (διατάξεις). 2
Δείκτες και διατάξεις (πίνακες) Ένα όνομα διάταξης χωρίς αγκύλες είναι ένας δείκτης στο πρώτο στοιχείο της διάταξης. Είναι ένας σταθερός δείκτης. Η έκφραση array είναι ισοδύναμη με &array[0] Μπορούμε να δηλώσουμε ένα μεταβλητό δείκτη και να του δώσουμε αρχική τιμή να δείχνει στη διάταξη. int array[100], *p_array; /* πρόσθετος κώδικας */ p_array = array; O παραπάνω κώδικας δίνει αρχική τιμή στο μεταβλητό δείκτη p_array τη διεύθυνση του πρώτου στοιχείου της array[] Εδώ, ο p_array είναι ένας μεταβλητός δείκτης που μπορεί να πάρει και άλλες τιμές και να δείχνει σε άλλα στοιχεία της διάταξης. 3
Δείκτες και διατάξεις (πίνακες) short int a[10], *p; p = &a[0]; p a[0] a[1] 1000 1001 1002 1003 *p = 5; /* To a[0] παίρνει τιμή 5 */ Ένας δείκτης πρέπει να αυξηθεί κατά 2 για να προσπελάσει επιτυχώς τα στοιχεία σε μία διάταξη τύπου short int Άρα θα έχουμε &a[0] = 1000, &a[1] = 1002 κλπ. Η συνάρτηση sizeof (τύπος_δεδομένων) μας επιστρέφει τον αριθμό των bytes που καταλαμβάνει κάποιος τύπος δεδομένων ή μεταβλητή. 4
Ακόμα ένα παράδειγμα Να γίνει ένα πρόγραμμα το οποίο θα διαβάζει ένα αρχείο κειμένου (πρότυπη είσοδο) γραμμή-γραμμή και θα την εμφανίζει στην οθόνη (πρότυπη έξοδο) μόνο εφόσον περιέχει κάποιο συγκεκριμένο συνδυασμό χαρακτήρων, ενώ όταν φτάσει στο τέλος του αρχείου να εμφανίζει το πλήθος των γραμμών που περιείχαν τον ζητούμενο συνδυασμό. Περίγραμμα προγράμματος ενόσω (υπάρχει άλλη γραμμή, διάβασέ την) εάν (η γραμμή περιέχει τον συνδυασμό) εμφάνισέ την αύξησε τον μετρητή κατά ένα εμφάνισε τον συνολικό αριθμό γραμμών που βρέθηκαν να περιέχουν το συνδυασμό. Θα βρείτε την λύση, που δόθηκε στον πίνακα, στις σημειώσεις της θεωρίας. 5
Δείκτες διεύθυνσης ως ορίσματα Μία από τις σημαντικές χρήσεις των δεικτών είναι ότι επιτρέπουν την διοχέτευση ορισμάτων σε συναρτήσεις με αναφορά (by reference). Αυτό σημαίνει ότι αντί να περνάμε ένα αντίγραφο του ορίσματος στην αντίστοιχη παράμετρο, περνάμε τη διεύθυνση του ορίσματος. Οπότε, κάθε αλλαγή στην παράμετρο (στο σώμα της συνάρτησης) ουσιαστικά μεταβάλει τα περιεχόμενα στη θέση μνήμης του ορίσματος! 6
main() { int k = 10, *a; a = &k; plus(a); plus(a); Παράδειγμα Η παράμετρος x λαμβάνει ως τιμή τη διεύθυνση a και προσθέτει το περιεχόμενό της, το αποτέλεσμα θα είναι 40. return 0; } void plus(int *x) { *x = *x + *x; return ; } main() { int k = 10; } plus(&k); plus(&k); return 0; Η παράμετρος x λαμβάνει ως τιμή τη διεύθυνση της μεταβλητής k και προσθέτει το περιεχόμενό της. Το αποτέλεσμα θα είναι και πάλι 40. 7
Κι άλλο παράδειγμα Η παρακάτω συνάρτηση πραγματοποιεί αντιμετάθεση των περιεχομένων δύο μεταβλητών πραγματικού τύπου κινητής υποδιαστολής διπλής ακρίβειας. void swap (double *ptr_x, double *ptr_y) { double tmp; } tmp = *ptr_x; *ptr_x = *ptr_y; *ptr_y = tmp; return; Ενώ η εντολή κλήσης της συνάρτησης θα μπορούσε να είναι: main () { double a = 13.25, b = 8.79; } printf ("A = %.2f και Β = %.2f\n", a, b); swap(&a, &b); printf ("A = %.2f και Β = %.2f\n", a, b); return 0; 8
Δείκτης διεύθυνσης ως τιμή επιστροφής από συνάρτηση Οι δείκτες δεν χρησιμοποιούνται μόνο για διοχέτευση ορισμάτων σε συναρτήσεις. Χρησιμοποιούνται και για επιστροφή τιμών από συναρτήσεις π.χ.: int *max (int *, int *); void main() { int *p, x=10, y=20;... p = max(&x, &y);... } Σημαίνει ότι η max επιστρέφει δείκτη διεύθυνσης σε ακέραιο Διοχετεύουμε σαν ορίσματα τις διευθύνσεις των μεταβλητών x και y και παραλαμβάνουμε με δείκτες int * max (int *a, int *b) { if (*a > *b) return a; else return b; } 9
Σημείωση ΠΡΟΣΟΧΗ: Μην επιστρέφετε ως αποτέλεσμα συνάρτησης δείκτη σε τοπική μεταβλητή π.χ. int *fctn(void) { int i;... } return &i; Η μεταβλητή i δεν θα υπάρχει μετά το τέλος της συνάρτησης οπότε ο δείκτης (η διεύθυνση του i) θα είναι άκυρος 10
Αριθμητική δεικτών διεύθυνσης Μπορούμε να προσθέσουμε ένα ακέραιο αριθμό σε ένα δείκτη διεύθυνσης (pointer), να αφαιρέσουμε ένα ακέραιο αριθμό από ένα δείκτη, ή να αφαιρέσουμε ένα δείκτη διεύθυνσης από έναν άλλο. Όταν έχουμε να κάνουμε με διατάξεις (πίνακες), αυτές οι πράξεις γίνονται πολύ ενδιαφέρουσες. 11
Μεταβολή δείκτη διεύθυνσης Όταν αυξάνουμε ένα δείκτη, αυξάνουμε τη τιμή του. Όταν αυξάνουμε ένα δείκτη κατά 1, η αριθμητική δεικτών αυξάνει αυτόματα την τιμή του δείκτη έτσι ώστε να δείχνει στο επόμενο στοιχείο μίας διάταξης. Ας υποθέσουμε ότι ο ptr_chr είναι δείκτης προς κάποιο στοιχείο διάταξης. ptr_chr++ η τιμή του ptr_chr αυξάνεται κατά το μέγεθος του τύπου char (1 byte) και η ptr_chr δείχνει τώρα στο επόμενο στοιχείο της διάταξης (επόμενο χαρακτήρα) ptr_int++ η τιμή του ptr_int αυξάνεται κατά το μέγεθος του τύπου int (συνήθως 4 bytes) και η ptr_int δείχνει τώρα στο επόμενο στοιχείο της διάταξης. ptr_double++, αύξηση της αποθηκευμένης τιμής του δείκτη κατά 8 (bytes), δείχνει στο επόμενο στοιχείο ptr_chr += 3 ; (κατά 3 bytes, δείχνει 3 στοιχεία μετά), ptr_int += 4 ; (κατά 16 bytes, δείχνει 4 στοιχεία μετά), ptr_double += 10 ; (κατά 80 bytes, δείχνει 10 στοιχεία μετά) 12
Άλλοι χειρισμοί δεικτών διεύθυνσης Η μόνη άλλη λειτουργία της αριθμητικής δεικτών είναι η διαφορά και αφορά την αφαίρεση δύο δεικτών διεύθυνσης. Εάν έχουμε δύο δείκτες διεύθυνσης που να δείχνουν προς δύο στοιχεία του ίδιου πίνακα, μπορούμε να τους αφαιρέσουμε και να βρούμε πόσο απέχει το ένα στοιχείο από το άλλο. ptr1-ptr2 H C γνωρίζει τον τύπο δεδομένων που δείχνει ο δείκτης από τη δήλωσή του και μεταβάλει τη διεύθυνση που είναι αποθηκευμένη στον δείκτη κατά το μέγεθος του τύπου δεδομένων. 13
Άλλοι χειρισμοί δεικτών διεύθυνσης Τα χαμηλότερα στοιχεία μίας διάταξης (εκείνα με το χαμηλότερο δείκτη, δηλαδή τα πρώτα) έχουν πάντα χαμηλότερη διεύθυνση από τα υψηλότερα στοιχεία. Η σχέση ptr1 < ptr2 είναι αληθής αν το ptr1 δείχνει σε ένα χαμηλότερο μέλος της διάταξης από εκείνο στο οποίο δείχνει ο ptr2. Οι συγκρίσεις δεικτών είναι έγκυρες μόνο μεταξύ δεικτών που δείχνουν στην ίδια διάταξη. O πολλαπλασιασμός και η διαίρεση δεν έχουν νόημα με τους δείκτες. ptr *= 2 δίνει μήνυμα λάθους αν ptr είναι δείκτης 14
Άλλοι χειρισμοί δεικτών διεύθυνσης Εάν μία δηλωμένη διάταξη είναι η array[], η έκφραση *array είναι το πρώτο στοιχείο της διάταξης, η *(array + 1) είναι το δεύτερο στοιχείο της διάταξης, κτλ. Αν p_array είναι ένας δείκτης στο πρώτο στοιχείο μίας διάταξης δηλ. η διεύθυνση του πρώτου στοιχείου, ισοδύναμο με &array[0] Είναι αληθείς οι ακόλουθες σχέσεις: *(p_array) == array[0] *(p_array + 1) == array[1] *(p_array + n) == array[n] αλλά και οι ακόλουθες: *(array) == p_array[0] *(array + 1) *(array + n) == p_array[1] == p_array[n] Αυτό δείχνει την ισοδυναμία της σημειογραφίας δεικτών διάταξης και της σημειογραφίας δεικτών σε διάταξη. 15
Παράδειγμα int a[10], *p, *q, *w; p = &a[0]; p a[0] a[1] *p = 40; /* To a[0] παίρνει τιμή 40 */ q = p + 3; w = q-1; p w q a[0] a[1] a[2] a[3] a[4] 16
Αφαίρεση δεικτών διεύθυνσης Ας θεωρήσουμε: int a[10], *p, *q, i; p = &a[5]; q = &a[3]; i = p - q; /* Η τιμή της μεταβλητής i είναι 2 */ i = q - p; /* Η τιμή της μεταβλητής i είναι -2 */ Σημείωση: Η αριθμητική σε δείκτες έχει νόημα όταν χρησιμοποιείται σε διατάξεις όπως στα παραδείγματα παραπάνω. Η αφαίρεση μας λέει πόσο απέχουν τα στοιχεία μιας διάταξης και όχι αριθμητική διαφορά μεταξύ διευθύνσεων δεικτών διεύθυνσης. 17
Παράδειγμα #define N 100 int a[n], sum, *p; Προσθέτει όλα τα στοιχεία μίας διάταξης ακέραιων main() { sum = 0; for (p = &a[0]; p < &a[n]; p++) sum = sum + *p; return 0; } 18
Κι άλλα παραδείγματα Υλοποίηση ενός στοιχειώδους κατανεμητή μνήμης με συναρτήσεις για δέσμευση, alloc(n), και αποδέσμευση, afree(p), μνήμης. Συνάρτηση που να επιστρέφει τον ν-οστό όρο της ακολουθίας Fibonacci, όταν γνωρίζουμε ότι ο κάθε όρος προκύπτει από το άθροισμα των δύο προηγούμενων όρων και ότι ο πρώτος όρος ισούται με μηδέν (0), ενώ ο δεύτερος με ένα (1). Οι λύσεις, που δόθηκαν στον πίνακα, υπάρχουν στις σημειώσεις της θεωρίας. 19
Σύνοψη λειτουργιών δεικτών διεύθυνσης Εκχώρηση Έμμεση διευθυνσιοδότηση Διεύθυνση Αύξηση Μείωση Διαφορά Σύγκριση 20