5ο σετ σημειώσεων - Δείκτες

Σχετικά έγγραφα
3ο σετ σημειώσεων - Πίνακες, συμβολοσειρές, συναρτήσεις

Πίνακες. 1 Πίνακες. 30 Μαρτίου 2014

Μεταβλητές τύπου χαρακτήρα

2ο σετ σημειώσεων. 1 Εντολές εκτέλεσης υπό συνθήκη. 19 Μαρτίου 2012

Προγραμματισμός Ι. Δείκτες. Δημήτρης Μιχαήλ. Τμήμα Πληροφορικής και Τηλεματικής Χαροκόπειο Πανεπιστήμιο

Εισαγωγή στον Προγραμματισμό

Εντολές ελέγχου ροής if, for, while, do-while

Δομημένος Προγραμματισμός (ΤΛ1006)

Η γλώσσα προγραμματισμού C

Διάλεξη 3η: Τύποι Μεταβλητών, Τελεστές, Είσοδος/Έξοδος

Προγραμματισμός Η/Υ (ΤΛ2007 )

Προγραμματισμός Η/Υ (ΤΛ2007 )

Δομημένος Προγραμματισμός (ΤΛ1006)

Η πρώτη παράμετρος είναι ένα αλφαριθμητικό μορφοποίησης

Εισαγωγή στην C. Μορφή Προγράµµατος σε γλώσσα C

Προγραμματισμός Ι. Δυναμική Διαχείριση Μνήμης. Δημήτρης Μιχαήλ. Ακ. Έτος Τμήμα Πληροφορικής και Τηλεματικής Χαροκόπειο Πανεπιστήμιο

Διαδικασιακός Προγραμματισμός

Διαδικασιακός Προγραμματισμός

ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΣ ΥΠΟΛΟΓΙΣΤΩΝ & ΥΠΟΛΟΓΙΣΤΙΚΗ ΦΥΣΙΚΗ

Διαδικασιακός Προγραμματισμός

Pascal. 15 Νοεμβρίου 2011

Ψευδοκώδικας. November 7, 2011

Στόχοι και αντικείμενο ενότητας. Εκφράσεις. Η έννοια του τελεστή. #2.. Εισαγωγή στη C (Μέρος Δεύτερο) Η έννοια του Τελεστή

ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΣ ΜΑΘΗΜΑ 3 Ο. Σταθερές-Παράμετροι-Μεταβλητές Αριθμητικοί & Λογικοί Τελεστές Δομή ελέγχου-επιλογής Σύνθετοι έλεγχοι

Προβλήματα, αλγόριθμοι, ψευδοκώδικας

ΑΣΚΗΣΗ 6: ΔΕΙΚΤΕΣ. Σκοπός της Άσκησης. 1. Εισαγωγικά στοιχεία για τους Δείκτες

Η γλώσσα προγραμματισμού C

Προγραμματισμός Η/Υ (ΤΛ2007 )

ΔΟΜΗΜΕΝΟΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΣ

Δομημένος Προγραμματισμός. Τμήμα Επιχειρηματικού Σχεδιασμού και Πληροφοριακών Συστημάτων

Διαδικασιακός Προγραμματισμός

Λύσεις για τις ασκήσεις του lab5

Διαδικασιακός Προγραμματισμός

Διαδικασιακός Προγραμματισμός

ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΣ Η/Υ Ακαδημαϊκό έτος ΤΕΤΡΑΔΙΟ ΕΡΓΑΣΤΗΡΙΟΥ #4

Pascal, απλοί τύποι, τελεστές και εκφράσεις

ΠΑΝΕΠΙΣΤΗΜΙΟ ΘΕΣΣΑΛΙΑΣ ΣΧΟΛΗ ΘΕΤΙΚΩΝ ΕΠΙΣΤΗΜΩΝ ΤΜΗΜΑ ΠΛΗΡΟΦΟΡΙΚΗΣ

ΤΕΜ-101 Εισαγωγή στους Η/Υ Εξεταστική Ιανουαρίου 2011 Θέματα Β

Κλήση Συναρτήσεων ΚΛΗΣΗ ΣΥΝΑΡΤΗΣΕΩΝ. Γεώργιος Παπαϊωάννου ( )

Κεφάλαιο ΙV: Δείκτες και πίνακες. 4.1 Δείκτες.

3 ο Εργαστήριο Μεταβλητές, Τελεστές

Διδάσκων: Κωνσταντίνος Κώστα Διαφάνειες: Δημήτρης Ζεϊναλιπούρ

Προγραμματισμός Υπολογιστών & Υπολογιστική Φυσική

int array[10]; double arr[5]; char pin[20]; Προγραµµατισµός Ι

ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΣ ΥΠΟΛΟΓΙΣΤΩΝ & ΥΠΟΛΟΓΙΣΤΙΚΗ ΦΥΣΙΚΗ

Εισαγωγή στον Προγραμματισμό

Διάλεξη 6: Δείκτες και Πίνακες

scanf() scanf() stdin scanf() printf() int float double %lf float

Βασικές Αρχές Προγραμματισμού

Κεφάλαιο 8.7. Πολυδιάστατοι Πίνακες (Διάλεξη 19)

Δομημένος Προγραμματισμός. Τμήμα Επιχειρηματικού Σχεδιασμού και Πληροφοριακών Συστημάτων

Η γλώσσα προγραμματισμού C

Δομές Δεδομένων (Εργ.) Ακ. Έτος Διδάσκων: Ευάγγελος Σπύρου. Εργαστήριο 3 Επανάληψη Γ μέρος

6. ΠΙΝΑΚΕΣ & ΑΛΦΑΡΙΘΜΗΤΙΚΑ

ΦΥΛΛΑΔΙΟ ΕΡΓΑΣΤΗΡΙΟΥ 1

2ο ΓΕΛ ΑΓ.ΔΗΜΗΤΡΙΟΥ ΑΕΠΠ ΘΕΟΔΟΣΙΟΥ ΔΙΟΝ ΠΡΟΣΟΧΗ ΣΤΑ ΠΑΡΑΚΑΤΩ

Τύποι δεδομένων, τελεστές, μεταβλητές

Δομημένος Προγραμματισμός (ΤΛ1006)

Διάλεξη 5: Δείκτες και Συναρτήσεις

Διάλεξη 11η: Δείκτες, μέρος 1

Προγραμματισμός Ι (ΗΥ120)

Προγραμματισμός Ι. Δομές & Ενώσεις. Πανεπιστήμιο Πελοποννήσου Τμήμα Πληροφορικής & Τηλεπικοινωνιών

Η Γλώσσα Προγραμματισμού C (Μέρος 2 - Οι Bασικές Εντολές της C) Οι Βασικοί Τελεστές της C

Υπερφόρτωση τελεστών

Δομημένος Προγραμματισμός

ΕΙΣΑΓΩΓΗ ΣΤΟ ΔΟΜΗΜΕΝΟ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟ

4. ΒΡΟΧΟΙ ΕΠΑΝΑΛΗΨΗΣ (Α' μέρος: for)

Εισαγωγή στον Προγραμματισμό

Ανάπτυξη και Σχεδίαση Λογισμικού

Προγραμματισμός ΙI (Θ)

Διαδικασιακός Προγραμματισμός

5. ΒΡΟΧΟΙ ΕΠΑΝΑΛΗΨΗΣ (Β' μέρος: while - do..while)

Οι δείκτες στη γλώσσα C

Προγραμματισμός Ι (ΗΥ120)

Δείκτες (Pointers) Ένας δείκτης είναι μια μεταβλητή με τιμή μια διεύθυνση μνήμης. 9.8

Εισαγωγή στον προγραμματισμό. Τμήμα Πληροφορικής & Επικοινωνιών ΤΕΙ Σερρών Εργαστήριο 2

Προγραμματισμός Ι (ΗΥ120)

Πίνακες: μια σύντομη εισαγωγή. Πίνακες χαρακτήρων: τα "Αλφαριθμητικά"

Στη C++ υπάρχουν τρεις τύποι βρόχων: (a) while, (b) do while, και (c) for. Ακολουθεί η σύνταξη για κάθε μια:

Ενδεικτικές λύσεις και στατιστικά

Δομημένος Προγραμματισμός (ΤΛ1006)

Τιμή Τιμή. σκορ. ζωές

ΑΣΚΗΣΗ 7: ΑΛΦΑΡΙΘΜΗΤΙΚΑ

Προγραμματισμός Η/Υ 1 (Εργαστήριο)

ΔΟΜΗΜΕΝΟΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΣ

Ινστιτούτο Επαγγελµατική Κατάρτιση Κορυδαλλού "ΤΕΧΝΙΚΟΣ ΣΥΣΤΗΜΑΤΩΝ ΥΠΟΛΟΓΙΣΤΩΝ" (Ερωτήσεις Πιστοποίησης στην γλώσσα προγραµµατισµού C)

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Κλάσεις και Αντικείμενα Αναφορές

Διαδικασιακός Προγραμματισμός

Ορισμός μεταβλητών δεικτών και αρχικοποίηση

Προγραμματισμός Ι. Χαρακτήρες. Πανεπιστήμιο Πελοποννήσου Τμήμα Πληροφορικής & Τηλεπικοινωνιών

a = 10; a = k; int a,b,c; a = b = c = 10;

Παρακάτω δίνεται o σκελετός προγράμματος σε γλώσσα C. Σχολιάστε κάθε γραμμή του κώδικα.

3.1 Αριθμητικοί και Λογικοί Τελεστές, Μετατροπές Τύπου (Casting)

53 Χρόνια ΦΡΟΝΤΙΣΤΗΡΙΑ ΜΕΣΗΣ ΕΚΠΑΙΔΕΥΣΗΣ Σ Α Β Β Α Ϊ Δ Η Μ Α Ν Ω Λ Α Ρ Α Κ Η

Λογικός τύπος Τελεστές σύγκρισης Λογικοί τελεστές Εντολές επιλογής Εμβέλεια Μαθηματικές συναρτήσεις Μιγαδικός τύπος ΔΕΥΤΕΡΗ ΔΙΑΛΕΞΗ

Ανάπτυξη και Σχεδίαση Λογισμικού

Διδάσκων: Παναγιώτης Ανδρέου

Σημειώσεις για πρόοδο στο εργαστήριο

ΥΠΟΛΟΓΙΣΤΕΣ ΙΙ. Τι περιλαμβάνει μια μεταβλητή; ΔΕΙΚΤΕΣ. Διεύθυνση μεταβλητής. Δείκτης

ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΣ Η/Υ Ακαδημαϊκό έτος ΤΕΤΡΑΔΙΟ ΕΡΓΑΣΤΗΡΙΟΥ #2

Transcript:

5ο σετ σημειώσεων - Δείκτες 11 Ιουνίου 01 1 Γενικά Σύμφωνα με το γενικό μοντέλο υπολογιστή, ένας υπολογιστής είναι μία μηχανή που διαθέτει μία κεντρική μονάδα επεξεργασίας η οποία μπορεί μεταξύ άλλων να διαβάζει και να γράφει δεδομένα στη μνήμη. Η μνήμη είναι οργανωμένη σε μία σειρά διαδοχικών θέσεων και μπορεί κανείς να αναφερθεί σε κάποια από αυτές με τη διεύθυνσή της. Είδαμε ότι δηλώνοντας μεταβλητές μπορούμε να αποφύγουμε τον άμεσο χειρισμό διευθύνσεων και να αναφερόμαστε σε αυτές με κάποιο όνομα, γεγονός που κάνει την ανάπτυξη προγραμμάτων σαφώς ευκολότερη. Δηλώνοντας μία μεταβλητή, κάπως έτσι int a; η γλώσσα αυτόματα την αντιστοιχίζει σε μία θέση μνήμης την οποία δεν χρειάζεται (και δεν θέλουμε) να ξέρουμε. Γράφοντας το όνομα της μεταβλητής μέσα στα πλαίσια μιας έκφρασης όπως a + ή (printf("%d",a); η γλώσσα βρίσκει σε ποια θέση μνήμης είναι αποθηκευμένη, τη διαβάζει και χρησιμοποιεί τα περιεχόμενά της όπως έχει ορίσει ο προγραμματιστής. Αντίστοιχα, αν θέλουμε να γράψουμε μία τιμή σε μία θέση μνήμης απλώς αναφέρουμε το όνομα της μεταβλητής στο αριστερό μέρος μίας εντολής ανάθεσης. Η γλώσσα και πάλι θα ψάξει να βρει την αντιστοίχιση. Ο παραπάνω χειρισμός της μνήμης είναι σαφώς βολικότερος από την απ ευθείας αναφορά αλλά έχει κάποιους περιορισμούς, καθώς υπάρχουν περιπτώσεις στις οποίες θέλουμε να ξέρουμε τη διεύθυνση μνήμης μιας μεταβλητής ή έστω την χρειαζόμαστε γιατί μας δίνει κάποιες δυνατότητες τις οποίες αλλιώς δεν θα είχαμε. Για παράδειγμα, είδαμε ότι οι συναρτήσεις της C δεν μπορούν να μεταβάλουν τα ορίσματά τους γιατί αυτά δίνονται στις συναρτήσεις κατ αξία, δηλαδή δημιουργείται ένα τοπικό αντίγραφο. Ο κώδικας της συνάρτησης θα μπορούσε όμως να πειράξει τα ορίσματα αν ήταν γνωστές οι διευθύνσεις τους. Μια άλλη περίπτωση στην οποία είναι απαραίτητη η γνώση των διευθύνσεων είναι η δυναμική δέσμευση μνήμης ¹. Τελεστές & και Η C μας δίνει τη δυνατότητα να μάθουμε, αν θέλουμε, τη θέση μνήμης στην οποία είναι αποθηκευμένη μία μεταβλητή με τον τελεστή αναφοράς (reference). Π.χ. το παρακάτω 1 int a; printf("%p", &a); ¹Είναι έξω από τα θέματα της πιστοποίησης. 1

θα μπορούσε να τυπώσει στην οθόνη 0xbfb971cc. Αν μετρήσετε τα ψηφία θα δείτε ότι είναι 4 ζευγάρια δεκαεξαδικών ψηφίων δηλαδή 4 bytes, δηλαδή όσα χρειάζονται για να ορίσουμε τη διεύθυνση μίας θέσης μνήμης σε έναν 3-bit υπολογιστή. Στο παραπάνω παράδειγμα δηλώσαμε μία μεταβλητή a τύπου int στη γραμμή 1. Στην επόμενη γραμμή με τον τελεστή & πήραμε τη διεύθυνση της μνήμης που δεσμεύτηκε για αυτήν τη μεταβλητή γράφοντας &a. Για να την τυπώσουμε χρειάστηκε να χρησιμοποιήσουμε τον specifier %p. Δείτε επίσης το παρακάτω: 3 int main() 5 int a; 6 char c, d; 7 float f; 8 printf("&a %p\n", &a); 9 printf("&c %p\n", &c); 10 printf("&d %p\n", &d); 11 printf("&f %p\n", &f); 1 13 return 0; 14 } Αν το τρέξετε θα δείτε κάτι σαν αυτό: &a 0xbff53664 &c 0xbff5366e &d 0xbff5366f &f 0xbff53668 Μπορούμε να βγάλουμε διάφορα συμπεράσματα: Ένα είναι ότι η διεύθυνση μιας ακέραιας μεταβλητής είναι ακριβώς της ίδιας μορφής με τη διεύθυνση μιας κινητής υποδιαστολής ή μιας χαρακτήρα. Αυτό είναι λογικό: Οι διευθύνσεις δηλώνουν τον αύξοντα αριθμό της θέσης μνήμης στην οποία είναι αποθηκευμένη μία μεταβλητή και αυτό είναι ανεξάρτητο από τον τύπο της. Επίσης μπορεί κανείς να παρατηρήσει ότι ο μεταγλωττιστής δεν βάζει τις μεταβλητές στη μνήμη με την ίδια σειρά που τις δηλώνουμε και για αυτό δεν κάνουμε ποτέ υποθέσεις για το που βρίσκονται οι μεταβλητές στη μνήμη. Μια άλλη ενδιαφέρουσα παρατήρηση που μπορεί να κάνει κανείς αν τρέξει το ίδιο πρόγραμμα αρκετές φορές είναι ότι οι θέσεις μνήμης στις οποίες είναι αποθηκευμένες οι μεταβλητές δεν είναι πάντα οι ίδιες. Πράγματι, το σύστημα τοποθετεί τις μεταβλητές όπου υπάρχει ελεύθερη μνήμη τη στιγμή που το τρέχουμε και προφανώς δεν είναι πάντα τα ίδια τμήματα της μνήμης ελεύθερα. Μια τελευταία παρατήρηση είναι ότι οι διευθύνσεις μνήμης είναι όλες τις ίδιας μορφής: 3-μπιτοι αριθμοί² και όπως είπαμε αυτό είναι αναμενόμενο αφού μία διεύθυνση είναι διεύθυνση ανεξάρτητα από το τι περιέχει. Θα μπορούσε κανείς να πει ότι όλες οι διευθύνσεις είναι ίδιες. Θα δούμε στη συνέχεια, ότι παρόλο που αυτό ισχύει, η C απαιτεί να δηλώνουμε τι είναι αυτό το οποίο βρίσκεται μέσα σε κάθε διεύθυνση. ²Σε έναν 3-μπιτο υπολογιστή εννοείται.

Είδαμε ότι αν γράψουμε τον τελεστη & μπροστά από το όνομα μιας μεταβλητής μας επιστρέφεται η διεύθυνσή της. Η αντίστροφή πράξη, αν δηλαδή έχουμε τη διεύθυνση μιας μεταβλητής και θέλουμε να διαβάσουμε ή να γράψουμε σε αυτήν πραγματοποιείται με τον τελεστή αποαναφοροποίησης (dereference) *. Γράφοντας δηλαδή το * μπροστά από μία διεύθυνση παίρνουμε τα περιεχόμενά της. Δείτε το παρακάτω 1 int a = ; printf("&a %p\n", &a); 3 printf("a %d\n", *(&a)); που θα τύπωνε κάτι σαν αυτό: &a 0xbf83b4c a Η μεταβλητή a έχει την τιμή και βρίσκεται στη θέση μνήμης 0xbf83b4c. η εντολή λοιπόν της γραμμής τυπώνει τη θέση μνήμης της μεταβλητής a δηλαδή 0xbf83b4c ενώ η εντολή της γραμμής 3 τυπώνει τα περιεχόμενα της θέσης μνήμης αυτής, δηλαδή. Μπορούμε κατά μία έννοια να πούμε ότι οι τελεστές & και * είναι αντίστροφοι, δηλαδή ο ένας κάνει την αντίστροφη δουλειά από τον άλλο. Αν έχουμε μια μεταβλητή, ο τελεστής & μας δίνει τη διεύθυνσή της. Και αν έχουμε μια διεύθυνση, ο τελεστής * μας δίνει πάλι τη μεταβλητή δηλαδή τα περιεχόμενά της³. 3 Δείκτες Η έννοια της διεύθυνσης μιας μεταβλητής έχει μεγάλη σημασία για αυτό και η γλώσσα ορίζει τύπους κατάλληλους για να αποθηκεύσουν διευθύνσεις. Και μάλιστα, παρόλο που όπως είδαμε στην προηγούμενη ενότητα, οι διευθύνσεις είναι ίδιες είτε είναι διευθύνσεις ακέραιων, είτε χαρακτήρων είτε ο,τιδήποτε, η C ορίζει ένα νέο τύπο διεύθυνσης για κάθε τύπο μεταβλητής. Οι μεταβλητές στις οποίες αποθηκεύονται διευθύνσεις άλλων μεταβλητών ονομάζονται δείκτες (pointers). Κάθε δείκτης πρέπει να δηλώνεται και μάλιστα να δηλώνεται και ο τύπος στον οποίο δείχνει. Για κάθε βασικό τύπο⁴ της C υπάρχει και ο αντίστοιχος τύπος δείκτη, αν δηλαδή θέλουμε να αποθηκεύσουμε σε έναν δείκτη τη διεύθυνση ενός ακεραίου τότε αυτός ο δείκτης θα πρέπει να έχει δηλωθεί ώς δείκτηςσε-ακέραιο, αν θέλουμε να αποθηκεύσουμε τη διεύθυνση ενός χαρακτήρα τότε ο δείκτης θα πρέπει να έχει δηλωθεί ως δείκτης-σε-χαρακτήρα κ.ο.κ. Για να δηλώσουμε ένα δείκτη προς κάποιο βασικό τύπο, γράφουμε τον τύπο και το όνομα της μεταβλητής-δείκτη αλλά μπροστά από το όνομα βάζουμε το χαρακτήρα *. Ο χαρακτήρας * όταν βρίσκεται σε μία δήλωση δεν πρέπει να συγχέεται με τον τελεστή αποαναφοροποίησης που αναφέρθηκε στην προηγούμενη ενότητα. Στα παρακάτω παραδείγματα οι μεταβλητές των οποίων το όνομα ξεκινάει από p είναι δείκτες στις αντίστοιχες μεταβλητές βασικών τύπων: ³Κάποιος μπορεί να φανταστεί ότι αν βάλουμε τον έναν μετά τον άλλον είναι σαν να αλληλοακυρώνονται. Πράγματι στο προηγούμενο παράδειγμα το να γράψουμε *(&a) ήταν σαν να γράφαμε a. Όμως το ανάποδο δηλαδή να γράψουμε &(*p) αν ο p είναι διεύθυνση μεταβλητής δεν επιτρέπεται. Καταλαβαίνετε γιατί; ⁴Φυσικά υπάρχουν δείκτες προς σύνθετους τύπους όπως οι structs, απλώς αναφέρονται οι βασικοί για λόγους απλότητας. 3

1 int a; char c; 3 float f; 4 5 int * pa = &a; 6 char * pc = &c; 7 float * pf = &f; Οι μεταβλητές τύπου δείκτη είναι μεταβλητές όπως και οι υπόλοιπες, δηλαδή μπορούμε να τις δηλώσουμε είτε ως τοπικές μεταβλητές είτε ως παραμέτρους σε συναρτήσεις, να αποτελούν μέρος μίας struct κτλ. Επίσης όμως διαφέρουν και σε αρκετά σημεία: Οι τελεστές που έχουμε συνηθίσει να χρησιμοποιούμε (αριθμητικοί για παράδειγμα) δεν έχουν όλοι την ίδια σημασία για τους δείκτες και πολλές φορές δεν ορίζονται. Και βέβαια υπάρχει η βασική εννοιολογική διαφορά ότι ορίζουμε μεταβλητές βασικών τύπων για να αποθηκεύσουμε σε αυτές δεδομένα που θέλουμε να χειριστεί το πρόγραμμά μας. Αντίθετα, οι τιμές των δεικτών δεν μας ενδιαφέρουν αυτές καθ αυτές, αλλά τα περιεχόμενα της θέσης μνήμης στην οποία δείχνουν. Δείτε για παράδειγμα το παρακάτω πρόγραμμα: 3 int main() 5 int a = 0; 6 int *p = &a; 7 8 printf("a: %d\n", a); 9 printf("p: %p\n", p); 10 printf("*p: %d\n", *p); 11 1 return 0; 13 } Έχουμε μια μεταβλητή a η οποία κρατάει το αποτέλεσμα ενός υπολογισμού το οποίο είναι ας πούμε 0 και έναν δείκτη προς την a με τιμή ας πούμε 0xbf916cc8. Η έξοδος του προγράμματος είναι: a: 0 p: 0xbf916cc8 *p: 0 Είναι προφανές ότι αυτό που μας ενδιαφέρει είναι το 0, η τιμή της μεταβλητής a η οποία κρατάει το αποτέλεσμα κάποιου φανταστικού υπολογισμού και όχι η τιμή της p η οποία άλλωστε συνήθως δεν θα είναι η ίδια. Απλώς, για τεχνικούς λόγους, είναι πιθανόν να έχουμε στη διάθεσή μας όχι την a αλλά έναν δείκτη προς την a, τον p. Σε τέτοιες περιπτώσεις, χρησιμοποιούμε τον δείκτη, όχι για να δώσουμε στον τελικό χρήστη την τιμή του αλλά την αποαναφοροποίηση του δηλαδή *p, δηλαδή τα περιεχόμενα της θέσης μνήμης στην οποία δείχνει ο p και όχι την τιμή του αυτήν καθ αυτήν. Δείτε το παρακάτω παράδειγμα για δείκτες. Δείχνει αφενός έναν από τους λόγους για τους οποίους χρησιμοποιούμε δείκτες, αφετέρου θα βοηθήσει να γίνει κατανοητό τι εννοούμε όταν λέμε ότι μας ενδιαφέρει όχι η τιμή του δείκτη αλλά η τιμή της θέσης μνήμης στην οποία δείχνει. Το παρακάτω 4

παράδειγμα δείχνει μία συνάρτηση false η οποία προσπαθεί να αλλάξει την τιμή ενός ορίσματός της, πράγμα που φυσικά δεν γίνεται και μία συνάρτηση correct που το πετυχαίνει με τη χρήση δεικτών. 3 void false(int a) 5 printf(" == false ==\ n"); 6 a = 5; 7 printf("a : %d\n", a); 8 printf("&a: %p\n", &a); 9 } 10 11 void correct(int *p) 1 { 13 printf(" == correct ==\ n"); 14 *p = 5; 15 printf("*p: %d\n", *p); 16 printf(" p: %p\n", p); 17 } 18 19 int main() 0 { 1 int a = ; printf("h a stn main eivai stn 8esn %p\n", &a); 3 false(a); 4 printf("h a stn main meta tnv false: %d\n", a); 5 correct(&a); 6 printf("h a stn main meta tnv correct: %d\n", a); 7 8 return 0; 9 } Αξίζει να τρέξετε το πρόγραμμα για να δείτε τι κάνει. Ακόμα καλύτερα να το τρέξετε μέσα από τον debugger. Η έξοδος του προγράμματος είναι η παρακάτω (αν το τρέξετε θα δείτε διαφορετικές τιμές στους δείκτες προφανώς): H a stn main eivai stn 8esn 0xbfb1c90c == false == a : 5 &a: 0xbfb1c8f0 H a stn main meta tnv false: == correct == *p: 5 p: 0xbfb1c90c H a stn main meta tnv correct: 5 5

Ας δούμε πρώτα για ποιο λόγο η συνάρτηση false δεν μπορεί να αλλάξει την τιμή της μεταβλητής a της συνάρτησης main: Βλέπουμε στην παραπάνω έξοδο, στη γραμμή 3 ότι η τιμή της a έγινε 5. Αλλά (όπως θα περιμέναμε και από τη θεωρία) δεν είναι η a της main που αλλάζει τιμή αλλά η παράμετρος a της συνάρτησης η οποία απλώς αρχικοποιήθηκε με την τιμή της a της main. Η a της συνάρτησης false βρίσκεται στη θέση μνήμης 0xbfb1c8f0 ενώ η a της main βρίσκεται στην 0xbfb1c90c, άρα πρόκειται για διαφορετικές μεταβλητές που απλώς έτυχε να έχουν το ίδιο όνομα. Το ότι η συνάρτηση false δεν μπόρεσε να αλλάξει την τιμή της a της main φαίνεται και από την γραμμή 5 στην έξοδο του προγράμματος όπου βλέπουμε ότι η a παραμένει. Αντίθετα η συνάρτηση correct χρησιμοποιεί ένα δείκτη προς την a, και στη γραμμή 14 αλλάζει τα περιεχόμενα της θέσης μνήμης 0xbfb1c90c στην οποία είναι αποθηκευμένη η μεταβλητή a της main. Το παρακάτω παράδειγμα είναι επίσης χαρακτηριστικό. Η συνάρτηση swap αλλάζει τις τιμές των δύο ορισμάτων της. Αυτό, προφανώς, δεν μπορεί να γίνει με απλές μεταβλητές και πρέπει να χρησιμοποιηθούν δείκτες. 3 void swap( int *pa, int * pb) 5 int t; 6 printf(" pa: %p, pb: %p\n", pa, pb); 7 t = *pa; 8 *pa = *pb; 9 *pb = t; 10 } 11 1 int main() 13 { 14 int a = ; 15 int b = 5; 16 17 printf("h a stn main eivai stn 8esn %p\n", &a); 18 printf("h b stn main eivai stn 8esn %p\n", &b); 19 0 swap(&a, &b); 1 printf("a: %d, b: %d\n", a, b); 3 return 0; 4 } Θα ήταν καλό να τρέξετε το πρόγραμμα και αν είναι δυνατόν μέσα από τον debugger. Στην οθόνη θα τυπώσει κάτι σαν το παρακάτω. Βλέπουμε ότι οι μεταβλητές a και b είναι αποθηκευμένες στις θέσεις 0xbf8f5598 και 0xbf8f559c αντίστοιχα. Αυτές οι θέσεις μνήμης (και όχι οι τιμές των a και b) δίνονται ως παράμετροι στη συνάρτηση swap όπως φαίνεται και από την τρίτη γραμμή στην παρακάτω έξοδο. Στις γραμμές 7 έως 9 της συνάρτησης γίνεται η ανταλλαγή. Αν δεν χρησιμοποιούσαμε δείκτες θα κάναμε κάτι σαν αυτό: t = a;, a = b; και b = t;. Στη συνάρτησή μας κάνουμε το ίδιο αλλά με 6

τους αντίστοιχους δείκτες. Εδώ χρειάζεται προσοχή στο να χρησιμοποιηθούν οι αποαναφοροποιήσεις των δεικτών (*pa, *pb) και όχι οι δείκτες αυτοί καθ αυτοί (pa και pb). Για αυτό και στη γραμμή 7 αποθηκεύεται στην προσωρινή μεταβλητή t όχι η τιμή του δείκτη pa που είναι 0xbf8f5598 αλλά η τιμή που βρίσκεται στη θέση μνήμης 0xbf8f5598, που είναι η θέση μνήμης που αντιστοιχεί στη μεταβλητή a της main, δηλαδή. H a stn main eivai stn 8esn 0xbf8f5598 H b stn main eivai stn 8esn 0xbf8f559c pa: 0xbf8f5598, pb: 0xbf8f559c a: 5, b: Είναι εύκολο κανείς να μπερδευτεί και να χρησιμοποιήσει έναν δείκτη εκεί που θα έπρεπε να χρησιμοποιήσει την αποαναφοροποίησή του, δηλαδή να γράψει t = pa; εκεί που θα έπρεπε να γράψει t = *pa;. Ελπίζω ότι οι παρακάτω υποδείξεις θα βοηθήσουν: Να θυμάστε πάντα ότι ένας δείκτης έχει τιμή σαν αυτήν: 0xbf8f5598. Οπότε αν διστάζετε ανάμεσα στο να γράψετε t = pa; ή t = *pa; σκεφτείτε το εξής: έχει νόημα να δώσετε στην t ή σε οποιαδήποτε άλλη μεταβλητή του προγράμματός σας την τιμή 0xbf8f5598 (ή κάποια άλλη σαν κι αυτήν); Ένας άλλος κανόνας είναι να ελέγχετε τους τύπους των μεταβλητών. Η t είναι ακέραια. Ο p είναι δείκτης σε ακέραιο άρα οι τύποι τους δεν είναι συμβατοί. Αφού ο p είναι δείκτης σε ακέραιο τότε η αποαναφοροποίησή του *p είναι επίσης ακέραιος οπότε μία εντολή σαν την t = *pa; έχει συμβατούς αριστερά και δεξιά τύπους ενώ η t = pa; δεν έχει. 4 Δείκτες και πίνακες - αριθμητική δεικτών Σε αυτήν την ενότητα θα δούμε την σχέση ανάμεσα στους δείκτες και τους πίνακες. Θεωρήστε το παρακάτω πρόγραμμα το οποίο δηλώνει έναν πίνακα ακεραίων με 5 στοιχεία. Επίσης στη γραμμή 6 δηλώνει ένα δείκτη σε ακέραιο και του αναθέτει τη διεύθυνση του πρώτου στοιχείου του πίνακα. 3 int main() 5 int pin[5] = { 0, 11,, 33, 44 }; 6 int* p = &pin[0]; 7 int i; 8 9 printf(" To pin ws deiktns: %p\n", pin); 10 printf("h dieu8uvsn tou prwtou stoixeiou: %p\n", p); 11 1 for (i = 0; i < 5; ++i) 13 printf("&pin[%d]: %p, p+%d: %d\n", &pin[i], p+i); 14 15 for (i = 0; i < 5; ++i) 16 printf("#%d: %d ", i, pin[i]); 17 printf("\n"); 7

18 for (i = 0; i < 5; ++i) 19 printf("#%d: %d ", i, *(p+i)); 0 1 return 0; } Η έξοδος του οποίου είναι η παρακάτω: To pin ws deiktns: 0xbfa36754 H dieu8uvsn tou prwtou stoixeiou: 0xbfa36754 &pin[0]: 0xbfa36754, p+0: 0xbfa36754 &pin[1]: 0xbfa36758, p+1: 0xbfa36758 &pin[]: 0xbfa3675c, p+: 0xbfa3675c &pin[3]: 0xbfa36760, p+3: 0xbfa36760 &pin[4]: 0xbfa36764, p+4: 0xbfa36764 #0: 0 #1: 11 #: #3: 33 #4: 44 #0: 0 #1: 11 #: #3: 33 #4: 44 Παρατηρείστε ότι η C δεν θεωρεί συντακτικό λάθος να τυπώσουμε την τιμή pin ως δείκτη και μάλιστα η τιμή αυτή είναι ίδια με τη διεύθυνση μνήμης του πρώτου στοιχείου του πίνακα. Το for των γραμμών 1-13 του πίνακα τυπώνει την διεύθυνση των στοιχείων pin[i] του πίνακα για τιμές του i από 0 έως 4 και παράλληλα την τιμή του δείκτη p+i. Βλέπουμε ότι ανά δύο οι τιμές είναι ίδιες. Δηλαδή αν p είναι η διεύθυνση του πρώτου στοιχείου ενός πίνακα, τότε p+1 είναι η διεύθυνση του δεύτερου, p+ είναι η διεύθυνση του τρίτου κ.ο.κ. Χρησιμοποιούμε αυτό το δεδομένο στους βρόχους των γραμμών 15-16 και 18-19 για να τυπώσουμε τα στοιχεία του πίνακα στη μία περίπτωση με το συντακτικό με τις αγκύλες που ήδη γνωρίζουμε αλλά και χρησιμοποιώντας δείκτες στα αντίστοιχα στοιχεία και αποαναφοροποίηση. Εδώ χρειάζεται λίγη προσοχή: είπαμε ότι ο δείκτης p+i είναι δείκτης προς το i-1 στοιχείο του πίνακα. Άρα βάζοντας μποροστά του τον τελεστή αποαναφοροποίησης * θα πάρουμε τα περιεχόμενά του. Λόγω της προτεραιότητας όμως των τελεστών και συγκεκριμένα επειδή ο * έχει υψηλότερη προτεραιότητα από τον + αν γράψουμε *p+i δεν θα πάρουμε τα περιεχόμενα του i-1 στοιχείου του πίνακα αλλά τα περιεχόμενα του πρώτου στοιχείου του πίνακα συν τον αριθμό i, δηλαδή η σειρά υπολογισμού είναι αυτή: (*p)+i. Για να το αποφύγουμε αυτό βάζουμε παρενθέσεις ώστε πρώτα να προστεθεί το i στο p για να πάρουμε ένα δείκτη στο i-1 στοιχείο και μετά να γίνει η αποαναφοροποίηση: *(p+i); Επίσης παρατηρούμε το εξής: Ενώ ο δείκτης p έχει τιμή 0xbfa36754, ο δείκτης p+1 έχει τιμή 0xbfa36758, ο δείκτης p+ 0xbfa3675c κοκ. Μέ άλλα λόγια, προσθέτοντας τον αριθμό 1 σε κάποιον δείκτη, δεν παίρνουμε αυτό που θα μας έδινε η συνηθισμένη πρόσθεση ακεραίων αλλά ένα δείκτη που δείχνει στον επόμενο ακέραιο. Παρατηρήστε ότι οι τιμές των δεικτών διαφέρουν κατά 4 μονάδες, όσα bytes δηλαδή καταλαμβάνει ένας ακέραιος. Αυτή η ιδιαίτερη αριθμητική ονομάζεται αριθμητική δεικτών. 8

5 Παραδείγματα χρήσης δεικτών Ας δούμε ορισμένα παραδείγματα που συχνά εμφανίζονται στην πράξη. Είδαμε στην προηγούμενη ενότητα πώς μπορούμε να σαρώσουμε τα στοιχεία ενός πίνακα αν έχουμε ένα δείκτη στο πρώτο στοιχείο του πίνακα γράφοντας *p, *(p+1), *(p+) κοκ. Στην πράξη αυτό το ιδίωμα χρησιμοποιείται σπάνια. Αντί για αυτό, ξεκινάμε με ένα δείκτη στο πρώτο στοιχείο του πίνακα τον οποίο αυξάνουμε κατά μία μονάδα με τον τελεστή ++ σε κάθε επανάληψη. Για να τυπώσουμε τα στοιχεία του προηγούμενου πίνακα, για παράδειγμα, θα χρησιμοποιούσαμε ένα for σαν κι αυτό: 1 for (i = 0; i < 5; ++i) { printf("%d ", *p); 3 p++; 4 } Το προηγούμενο παράδειγμα είναι απλό και κατανοητό αλλά ακόμα κι αυτό χρησιμοποιείται σπάνια στην πράξη. Οι προγραμματιστές συνήθως γράφουν το πιο σύντομο: 1 for (i = 0; i < 5; ++i) printf("%d ", *p++); Ας δούμε λίγο πιο αναλυτικά τι σημαίνει αυτό το *p++. Ο τελεστής ++ έχει υψηλότερη προτεραιότητα από τον τελεστή * αλλά επειδή χρησιμοποιείται η μεταθεματική μορφή του σημαίνει ότι θα αυξήσει τον δείκτη p κατά μία μονάδα αλλά αφού τελειώσει η εκτέλεση της τρέχουσας εντολής. Οπότε ο τελεστής * εφαρμόζεται στην τρέχουσα τιμή του δείκτη p και έτσι παίρνουμε την τιμή του στοιχείου στο οποίο δείχνει ο p. Αφού γίνει αυτό εκτελείται ο μεταθεματικός τελεστής και ο p αυξάνεται κατά μία μονάδα, έχοντας την τιμή που θα χρειαστεί στην επόμενη επανάληψη. Μια άλλη χαρακτηριστική χρήση των δεικτών είναι στο πέρασμα στοιχείων συμβολοσειρών. Θυμηθείτε ότι συμβολοσειρά είναι μια σειρά χαρακτήρων αποθηκευμένων σε έναν πίνακα της οποίας το τέλος σηματοδοτείται από τον ειδικό χαρακτήρα '\0'. Έχοντας λοιπόν στη διάθεσή μας ένα δείκτη προς τον πρώτο χαρακτήρα μιας συμβολοσειράς μπορούμε να σαρώσουμε όλα τα στοιχεία της. Δεν χρειάζεται να γνωρίζουμε το μήκος της: θα σταματήσουμε όταν ο χαρακτήρας στον οποίο δείχνει ο δείκτης μας είναι ο χαρακτήρας τερματισμού. Δείτε την παρακάτω συνάρτηση στην οποία δίνεται ο δείκτης στην αρχή μιας συμβολοσειράς και τυπώνει έναν προς έναν τους χαρακτήρες της. 3 void printstring(char* p) 5 while (*p) 6 printf("%c", *p++); 7 } 8 9 int main() 10 { 11 char s[100]; 1 printf(" Grapse kati: "); gets(s); 13 printstring(s); 9

14 return 0; 15 } Αρχικά ζητείται από τον χρήστη μια συμβολοσειρά η οποία αποθηκεύεται στον πίνακα s. Αυτός δίνεται ως παράμετρος στη συνάρτηση printstring (οι τύποι είναι συμβατοί, ένας πίνακας χαρακτήρων είναι δείκτης σε χαρακτήρα). Στη γραμμή 5 ξεκινάει ένας βρόχος while ο οποίος εκτελείται όσο το *p δηλαδή ο χαρακτήρας στον οποίο δείχνει ο p δεν είναι μηδέν δηλαδή δεν είναι ο χαρακτήρας τερματισμού συμβολοσειράς. Αν αυτό συμβαίνει τότε τυπώνεται ο χαρακτήρας και ο δείκτης p αυξάνεται κατά μία μονάδα ώστε να δείχνει στον επόμενο χαρακτήρα κοκ. Δείτε επίσης μία συνάρτηση η οποία μετράει το μήκος μιας συμβολοσειράς. Το σκεπτικό είναι ότι αν έχουμε ένα δείκτη στην αρχή της συμβολοσειράς και ένα δείκτη στο τέλος της, τότε το μήκος είναι η διαφορά των δύο δεικτών. Οπότε χρειαζόμαστε δύο δείκτες, έναν για να τον προχωρήσουμε μέχρι το τέλος της συμβολοσειράς και έναν που να δείχνει στην αρχή της. Το while της γραμμής 4 προχωράει το δείκτη s1 που αρχικά δείχνει στην αρχή της συμβολοσειράς, μέχρι το τέλος της. Το σώμα του βρόχου είναι κενό γιατί δεν χρειάζεται να κάνουμε κάποια πράξη. Παρατηρήστε ότι ο δείκτης s1 θα προχωρήσει λίγο περισσότερο από όσο θα θέλαμε, μία θέση για την ακρίβεια. Ο λόγος είναι ότι όταν ο δείκτης δείξει το τέλος της συμβολοσειράς τότε η συνθήκη θα είναι ψευδής *s1++ αλλά ο τελεστής ++ θα αυξήσει την τιμή του κατά μία μονάδα οπότε θα δείχνει εκεί που θέλαμε, συν ένα. Για αυτό τελικά δεν επιστρέφουμε στη γραμμή 5 s1 - s αλλά s1 - s - 1. 1 int mystrlen(char* s) { 3 char * s1 = s; 4 while (*s1++); 5 return s1 - s - 1; 6 } Το παρακάτω παράδειγμα δείχνει μία συνάρτηση η οποία αντιγράφει μία συμβολοσειρά που είναι αποθηκευμένη στον πίνακα s1 στον πίνακα s. 1 void mystrcpy( char* s1, char* s) { 3 while (*s++ = *s1++); 4 } Και ένα παράδειγμα το οποίο μετατρέπει τους πεζούς χαρακτήρες σε κεφαλαίους σε μία συμβολοσειρά s. 1 void mycap(char* s) { 3 while (*s) { 4 if (*s >= 'a' && *s <= 'z') 5 *s += 'A' - 'a'; 6 } 7 } Ασκήσεις: Μπορείτε να θεωρήσετε όλα τα παραπάνω παραδείγματα ως ασκήσεις τις οποίες θα πρέπει να υλοποιήσετε. Κοιτάξτε τα θέματα πιστοποίησης 11, 134, 135, 136, 138, 140, 141, 10

14, 143 και 154. Επίσης μπορείτε να δείτε τα 114, 18, 130, 13 που υλοποιούνται μεν με πίνακες αλλά μπορείτε να τα υλοποιήσετε με συναρτήσεις οπότε να χρησιμοποιήσετε δείκτες. 11