Τεχνολογικό Εκπαιδευτικό Ίδρυμα Κρήτης Σχολή Εφαρμοσμένων Επιστημών Τμήμα Ηλεκτρονικών Μηχανικών Τομέας Αυτοματισμού και Πληροφορικής Δομημένος Προγραμματισμός (ΤΛ1006) Δρ. Μηχ. Νικόλαος Πετράκης, Καθηγητής Εφαρμογών (npet@chania.teicrete.gr) Όγδοη (8 η ) τρίωρη διάλεξη. Ιστοσελίδα Μαθήματος: https://eclass.chania.teicrete.gr/courses/el106 Εξάμηνο: Χειμερινό 2017-18
Οι πίνακες ως ορίσματα Μέχρι τώρα έχουμε δει τους πίνακες ως μεταβλητές. Τι γίνεται όμως όταν χρησιμοποιούμε το όνομα ενός πίνακα σαν πραγματική παράμετρο; Μεταβιβάζεται ολόκληρος ο πίνακας; ΟΧΙ. Μεταβιβάζεται στην συνάρτηση μόνο η διεύθυνση της αρχής του πίνακα. Έτσι, δεικτοδοτώντας την τιμή αυτή η συνάρτηση μπορεί τόσο να διαβάσει όσο και γράψει οποιοδήποτε στοιχείο του πίνακα. 2
Πίνακες χαρακτήρων Είναι από τους συνηθέστερους πίνακες στην C Κάθε στοιχείο τού πίνακα αποτελείται από έναν χαρακτήρα (μία ψηφιολέξη = 1 byte) Με αυτόν τον τρόπο αποθηκεύονται στην C οι αλφαριθμητικές ακολουθίες (strings), και μάλιστα στο τέλος κάθε τέτοιας ακολουθίας αποθηκεύεται ακόμα ένας χαρακτήρας, ο μηδενικός χαρακτήρας ( \0 ), για την οριοθέτηση της αλφαριθμητικής ακολουθίας. Π.χ. η συμβολοσειρά "Computers" απεικονίζεται: C o m p u t e r s \0 3
Παράδειγμα Να γίνει ένα πρόγραμμα το οποίο θα διαβάζει ένα αρχείο κειμένου γραμμή-γραμμή και όταν φτάσει στο τέλος να εμφανίζει την μεγαλύτερη γραμμή καθώς και το μήκος της. Περίγραμμα προγράμματος ενόσω (υπάρχει άλλη γραμμή, διάβασέ την) εάν (είναι μεγαλύτερη από την μέχρι στιγμής μεγαλύτερη) αποθήκευσέ την αποθήκευσε το μήκος της εμφάνισε την μεγαλύτερη γραμμή και το μήκος της. Θα βρείτε την λύση, που δόθηκε στον πίνακα, στις σημειώσεις της θεωρίας. 4
Δείκτες και διατάξεις (πίνακες) Η σχέση των διατάξεων και των δεικτών είναι πολύ σημαντική. Ας θυμηθούμε τη δήλωση μιας διάταξης: float a[10]; /* Μονοδιάστατη διάταξη 10 στοιχείων τύπου float */ a[0] a[1]... a[9] Ουσιαστικά η μεταβλητή a υποδηλώνει τη διεύθυνση του πρώτου του στοιχείου (δηλαδή του a[0]), και μετά τόσες θέσεις μνήμης όσες απαιτούνται για να χωρέσουν 10 πραγματικοί αριθμοί κινητής υποδιαστολής απλής ακρίβειας (float). Με αυτό τον τρόπο η C απεικονίζει εσωτερικά τους πίνακες (διατάξεις). 5
Δείκτες και διατάξεις (πίνακες) Ένα όνομα διάταξης χωρίς αγκύλες είναι ένας δείκτης στο πρώτο στοιχείο της διάταξης. Είναι ένας σταθερός δείκτης. Π.χ. αν δηλώσουμε: int array[100]; Η έκφραση array είναι ισοδύναμη με τη &array[0] Μπορούμε να δηλώσουμε ένα μεταβλητό δείκτη και να του δώσουμε αρχική τιμή τέτοια που να δείχνει στη διάταξη. int *p_array; /* πρόσθετος κώδικας */ p_array = array; O παραπάνω κώδικας δίνει αρχική τιμή στο μεταβλητό δείκτη p_array τη διεύθυνση του πρώτου στοιχείου της array[] Εδώ, ο p_array είναι ένας μεταβλητός δείκτης που μπορεί να πάρει και άλλες τιμές και να δείχνει σε άλλα στοιχεία της διάταξης. 6
Δείκτες και διατάξεις (πίνακες) 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 που καταλαμβάνει κάποιος τύπος δεδομένων ή μεταβλητή. 7
Δείκτες διεύθυνσης ως ορίσματα Μία από τις σημαντικές χρήσεις των δεικτών είναι ότι επιτρέπουν την διοχέτευση ορισμάτων σε συναρτήσεις με αναφορά (by reference). Αυτό σημαίνει ότι αντί να περνάμε ένα αντίγραφο του ορίσματος στην αντίστοιχη παράμετρο, περνάμε τη διεύθυνση του ορίσματος. Οπότε, κάθε αλλαγή στην παράμετρο (στο σώμα της συνάρτησης) ουσιαστικά μεταβάλει τα περιεχόμενα στη θέση μνήμης του ορίσματος! 8
Παράδειγμα 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. 9
Κι άλλο παράδειγμα Η παρακάτω συνάρτηση πραγματοποιεί αντιμετάθεση των περιεχομένων δύο μεταβλητών πραγματικού τύπου κινητής υποδιαστολής διπλής ακρίβειας. 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; } 10
Δείκτης διεύθυνσης ως τιμή επιστροφής από συνάρτηση Οι δείκτες δεν χρησιμοποιούνται μόνο για διοχέτευση ορισμάτων σε συναρτήσεις. Χρησιμοποιούνται και για επιστροφή τιμών από συναρτήσεις π.χ.: 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; } 11
Σημείωση ΠΡΟΣΟΧΗ: Μην επιστρέφετε ως αποτέλεσμα συνάρτησης δείκτη σε τοπική μεταβλητή π.χ. int *fctn(void) { int i;... } return &i; Η μεταβλητή i δεν θα υπάρχει μετά το τέλος της συνάρτησης οπότε ο δείκτης θα είναι άκυρος (διότι το i δεν θα έχει υπόσταση 12
Αριθμητική δεικτών διεύθυνσης Μπορούμε να προσθέσουμε ένα ακέραιο αριθμό σε ένα δείκτη διεύθυνσης (pointer), να αφαιρέσουμε ένα ακέραιο αριθμό από ένα δείκτη, ή να αφαιρέσουμε ένα δείκτη διεύθυνσης από έναν άλλο. Όταν έχουμε να κάνουμε με διατάξεις (πίνακες), αυτές οι πράξεις γίνονται πολύ ενδιαφέρουσες. 13
Μεταβολή δείκτη διεύθυνσης Όταν αυξάνουμε ένα δείκτη, αυξάνουμε τη τιμή του. Όταν αυξάνουμε ένα δείκτη κατά 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 στοιχεία μετά) 14
Άλλοι χειρισμοί δεικτών διεύθυνσης Η μόνη άλλη λειτουργία της αριθμητικής δεικτών είναι η διαφορά και αφορά την αφαίρεση δύο δεικτών διεύθυνσης. Εάν έχουμε δύο δείκτες διεύθυνσης που να δείχνουν προς δύο στοιχεία του ίδιου πίνακα, μπορούμε να τους αφαιρέσουμε και να βρούμε πόσο απέχει το ένα στοιχείο από το άλλο. ptr1-ptr2 H C γνωρίζει τον τύπο δεδομένων που δείχνει ο δείκτης από τη δήλωσή του και μεταβάλει τη διεύθυνση που είναι αποθηκευμένη στον δείκτη κατά το μέγεθος του τύπου δεδομένων. 15
Άλλοι χειρισμοί δεικτών διεύθυνσης Τα χαμηλότερα στοιχεία μίας διάταξης (εκείνα με το χαμηλότερο δείκτη, δηλαδή τα πρώτα) έχουν πάντα χαμηλότερη διεύθυνση από τα υψηλότερα στοιχεία. Η σχέση ptr1 < ptr2 είναι αληθής αν το ptr1 δείχνει σε ένα χαμηλότερο μέλος της διάταξης από εκείνο στο οποίο δείχνει ο ptr2. Οι συγκρίσεις δεικτών είναι έγκυρες μόνο μεταξύ δεικτών που δείχνουν στην ίδια διάταξη. O πολλαπλασιασμός και η διαίρεση δεν έχουν νόημα με τους δείκτες. ptr *= 2 δίνει μήνυμα λάθους αν ptr είναι δείκτης 16
Άλλοι χειρισμοί δεικτών διεύθυνσης Εάν μία δηλωμένη διάταξη είναι η array[], η έκφραση *array είναι το πρώτο στοιχείο της διάταξης, η *(array + 1) είναι το δεύτερο στοιχείο της διάταξης, κτλ. Αν p_array είναι ένας δείκτης στο πρώτο στοιχείο μίας διάταξης δηλ. η διεύθυνση του πρώτου στοιχείου, ισοδύναμο με &array[0] Είναι αληθείς οι ακόλουθες σχέσεις: *(p_array) == array[0] *(p_array + 1) == array[1] *(p_array + n) == array[n] αλλά και οι ακόλουθες: *(array) *(array + 1) *(array + n) == p_array[0] == p_array[1] == p_array[n] Αυτό δείχνει την ισοδυναμία της σημειογραφίας δεικτών διάταξης και της σημειογραφίας δεικτών σε διάταξη. 17
Παράδειγμα 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] 18
Αφαίρεση δεικτών διεύθυνσης Ας θεωρήσουμε: int a[10], *p, *q, i; p = &a[5]; q = &a[3]; i = p - q; /* Η τιμή της μεταβλητής i είναι 2 */ i = q - p; /* Η τιμή της μεταβλητής i είναι -2 */ Σημείωση: Η αριθμητική σε δείκτες έχει νόημα όταν χρησιμοποιείται σε διατάξεις όπως στα παραδείγματα παραπάνω. Η αφαίρεση μας λέει πόσο απέχουν τα στοιχεία μιας διάταξης και όχι αριθμητική διαφορά μεταξύ διευθύνσεων δεικτών διεύθυνσης. 19
Παράδειγμα #define N 100 int a[n], sum, *p; Προσθέτει όλα τα στοιχεία μίας διάταξης ακέραιων main() { sum = 0; for (p = &a[0]; p < &a[n]; p++) sum = sum + *p; return 0; } 20
Σύνοψη λειτουργιών δεικτών διεύθυνσης Εκχώρηση Έμμεση διευθυνσιοδότηση Διεύθυνση Αύξηση Μείωση Διαφορά Σύγκριση 21