Διαδρομές
/
C
C
/
Ύλη
/
Δείκτες
Δε

Δείκτες σε C

{one: "1 άσκηση", other: "%{count} ασκήσεις"}

Σχετικά με την έννοια Δείκτες

Ένας δείκτης είναι μια μεταβλητή ή σταθερά της οποίας η τιμή είναι η διεύθυνση μιας άλλης μεταβλητής ή σταθεράς. Οι δείκτες είναι ένα ισχυρό και δυνητικά επικίνδυνο χαρακτηριστικό της C. Μπορεί να είναι δύσκολο να κατανοήσεις τους δείκτες.

int i = 5;

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

Ποιος είσαι;

Είμαι το i.

Τι έχεις;

Έχω τον ακέραιο 5.

Υπάρχει και μια τρίτη ερώτηση που μπορούμε να κάνουμε.

Πού είσαι;

Βρίσκομαι στη διεύθυνση 0x6dfed4.

Κάτσε, τι;

Οι μεταβλητές και οι σταθερές αποθηκεύουν την τιμή τους σε μια θέση μνήμης. Αυτή η θέση στη μνήμη είναι γνωστή ως η διεύθυνση της μεταβλητής ή της σταθεράς.

Ο τελεστής διεύθυνσης και ο τελεστής έμμεσης αναφοράς (μέρος 1)

Για να πάρουμε τη διεύθυνση μιας μεταβλητής χρησιμοποιούμε τον τελεστή διεύθυνσης, που είναι το &.

int i = 5;
int *i_address = &i;

Κάτσε, τι σημαίνει αυτό το * πριν από το i_address; Δεν σημαίνει πολλαπλασιασμός;

Ένας τύπος που ακολουθείται από ένα * και μετά από ένα αναγνωριστικό είναι ο τρόπος για να δηλώσεις έναν δείκτη σε μια μεταβλητή αυτού του τύπου. Το * είναι γνωστό ως ο τελεστής έμμεσης αναφοράς, επειδή αναφέρεται έμμεσα στην τιμή της οποίας ο δείκτης κρατά τη διεύθυνση.

Εντάξει, αλλά γιατί να φτιάξεις μια μεταβλητή μόνο και μόνο για να κρατά τη διεύθυνση μιας άλλης μεταβλητής;

Και γιατί να μας νοιάζει εξαρχής ποια είναι η διεύθυνση μιας μεταβλητής;

Λοιπόν, μας νοιάζει πού βρίσκονται οι μεταβλητές όταν είναι μέσα σε έναν πίνακα.

Πίνακες και δείκτες

Όταν δημιουργούμε έναν πίνακα όπως παρακάτω

int my_ints[] = {1, 2};

Η τιμή του my_ints είναι στην πραγματικότητα η διεύθυνσή του, η οποία τυχαίνει να είναι ίδια με τη διεύθυνση του πρώτου στοιχείου του. Η διεύθυνση του δεύτερου στοιχείου του θα είναι η διεύθυνση του πρώτου στοιχείου συν το μέγεθος ενός int στο σύστημα.

Για να το δεις καλύτερα, δοκίμασε να τρέξεις το παρακάτω

#include <stdio.h>

int main() {
    // my_int's value is its address
    int my_ints[] = {1, 2};
    
    // since my_ints is already an address
    // we don't need the address operator to make a pointer from it.
    // a pointer to my_ints holds the address of my_ints
    int *my_ptr = my_ints;
    
    // the address of my_ints and the value of my_ptr are the same
    printf("my_ints is %p. my_ptr is %p.\n", my_ints, my_ptr);
    
    // the address of the first element is the same as the values in the line above
    printf("&my_ints[0] is %p. my_ptr + 0 is %p.\n", &my_ints[0], my_ptr + 0);
    
    // the address of the second element has increased by the size of an int
    printf("&my_ints[1] is %p. my_ptr + 1 is %p.\n", &my_ints[1], my_ptr + 1); // 
    
    return 0;
}

Αριθμητική δεικτών, ή γιατί οι θέσεις στους πίνακες ξεκινούν από το 0

Το προηγούμενο παράδειγμα δείχνει γιατί η ευρετηρίαση των πινάκων ξεκινά από το 0 αντί για το 1. Για να υπολογιστεί η θέση μέσα σε έναν πίνακα, πολλαπλασιάζουμε τον αριθμό της θέσης επί το μέγεθος του τύπου και τον προσθέτουμε στη διεύθυνση έναρξης του πίνακα. Επειδή το πρώτο στοιχείο ευθυγραμμίζεται με την αρχή του πίνακα, πολλαπλασιάζουμε το μέγεθος του τύπου επί 0 και το προσθέτουμε στη διεύθυνση έναρξης για να πάρουμε το πρώτο στοιχείο. Για να πάρουμε το δεύτερο στοιχείο, πολλαπλασιάζουμε το μέγεθος του τύπου επί 1 και το προσθέτουμε στη διεύθυνση έναρξης. Αν ο τύπος έχει μήκος τέσσερα byte, η διεύθυνση του δεύτερου στοιχείου θα είναι τέσσερα byte μεγαλύτερη από του πρώτου στοιχείου.

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

Μα περίμενε, υπάρχουν κι άλλα! Δεν νόμιζες ότι είχαμε τελειώσει με το *, έτσι;

Ο τελεστής έμμεσης αναφοράς (μέρος 2)

Τώρα που καταλαβαίνουμε πώς λειτουργούν οι διευθύνσεις με την ευρετηρίαση των πινάκων, τι αξία έχει αν δεν πάρουμε την τιμή που είναι αποθηκευμένη σε μια διεύθυνση; Πώς το κάνουμε αυτό; Για να πάρουμε την τιμή που είναι αποθηκευμένη σε μια διεύθυνση, χρησιμοποιούμε τον τελεστή έμμεσης αναφοράς, ο οποίος, όπως ίσως θυμάσαι, είναι το *. Σε αυτή την περίπτωση, ο τελεστής έμμεσης αναφοράς αποκαλείται μερικές φορές και τελεστής αποαναφοράς, επειδή κάνει αποαναφορά στην τιμή της οποίας ο δείκτης έχει τη διεύθυνση. Το να τον αποκαλούμε τελεστή αποαναφοράς όταν χρησιμοποιείται για αποαναφορά τον ξεχωρίζει από τον τελεστή έμμεσης αναφοράς για τον ορισμό ενός δείκτη, αλλά το * και για τις δύο χρήσεις ονομάζεται τεχνικά τελεστής έμμεσης αναφοράς.

Έλα τώρα! Πλάκα μου κάνεις; Πόσες χρήσεις έχει το *; Πολλαπλασιασμός, δήλωση δείκτη, και τώρα αποαναφορά! Είναι μπερδεμένο! Και τι ακριβώς είναι η αποαναφορά, τέλος πάντων;

Ένα παράδειγμα δείχνει πώς να χρησιμοποιήσεις τον τελεστή έμμεσης αναφοράς για αποαναφορά

#include <stdio.h>

int main() {
    // define the variable
    int my_ints[] = {1, 2};
    
    // define the pointer from the address of the variable
    int *my_ptr = my_ints;
    
    // prints: my_ints[0] is 1. *(my_ptr + 0) is 1.
    printf("my_ints[0] is %u. *(my_ptr + 0) is %u.\n", my_ints[0], *(my_ptr + 0));
    
    // prints: my_ints[1] is 2. *(my_ptr + 1) is 2.
    printf("my_ints[1] is %u. *(my_ptr + 1) is %u.\n", my_ints[1], *(my_ptr + 1));
    
    return 0;
}

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

#include <stdio.h>

int main() {
    int my_ints[] = {1, 42};
    int *my_ptr = my_ints;
    
    // prints: my_ints[0] is 1. *++my_ptr is 42.
    printf("my_ints[0] is %u. *++my_ptr is %u.\n", my_ints[0], *++my_ptr);
    
    // prints: my_ints[1] is 43. ++*my_ptr++ is 43.
    printf("my_ints[1] is %u. ++*my_ptr++ is %u.\n", my_ints[1], ++*my_ptr);

    return 0;
}

Τόσο το * όσο και το ++ έχουν την ίδια προτεραιότητα, αλλά αξιολογούνται από τα δεξιά προς τα αριστερά. Στην πρώτη γραμμή, το my_ptr αυξάνεται πρώτα στο δεύτερο στοιχείο, το οποίο και μετά αποαναφέρεται. Στη δεύτερη γραμμή, το my_ptr, το οποίο είχε αυξηθεί στο δεύτερο στοιχείο στην πρώτη γραμμή, αποαναφέρεται πρώτα, και μετά η τιμή που προκύπτει από την αποαναφορά αυξάνεται. Αν προσθέσουμε αυτή τη γραμμή στο παραπάνω παράδειγμα

// prints: my_ints[1] is 43. *++my_ptr is (some garbage value).
printf("my_ints[1] is %u. *++my_ptr is %u.\n", my_ints[1], *++my_ptr);

Σε αυτή τη γραμμή, έχουμε αυξήσει τον δείκτη ώστε να βρίσκεται πέρα από τον πίνακα. Έχουμε ξεπεράσει τα όρια του πίνακα και δείχνουμε στα επόμενα τέσσερα byte της μνήμης, τα οποία ο δείκτης ερμηνεύει ως int, αλλά στην πραγματικότητα αυτά τα byte μπορεί να ανήκουν σε μια άλλη μεταβλητή ή μεταβλητές. Αυτά τα τέσσερα byte θα μπορούσαν να είναι μέρος ενός double ή θα μπορούσαν να είναι τέσσερα char. Το πρόβλημα είναι ότι δεν θα έπρεπε καν να τα κοιτάμε με τον δείκτη μας.

Θα μπορούσαμε να είχαμε χρησιμοποιήσει

// prints: my_ints[2] is (some garbage value).
printf("my_ints[2] is %u", my_ints[2]);

Εφόσον η ευρετηρίαση των πινάκων υλοποιείται με δείκτη, ισχύει ο ίδιος κίνδυνος να ξεπεράσουμε τα όρια του πίνακα.

Και, εφόσον μια συμβολοσειρά είναι ένας πίνακας από char, οι δείκτες μπορούν να χρησιμοποιηθούν και με συμβολοσειρές. Μια συμβολοσειρά δηλώνεται συχνά με σύνταξη δείκτη, όπως παρακάτω

char *my_string = "This is my string";
// instead of
// char my_string[] = "This is my string";

Έχουμε όμως μια απορία. Γιατί βάζουμε το αστεράκι δίπλα στο αναγνωριστικό; Γιατί να μην το βάλουμε δίπλα στον τύπο, αφού είναι δείκτης σε αυτόν τον τύπο; Επειδή αυτό

int* my_ptr1, my_ptr2_that_is_really_an_int;

δεν δηλώνει δύο δείκτες σε int, αλλά δηλώνει έναν δείκτη σε int και έναν int. Για να δηλώσεις δύο δείκτες σε int, θα έγραφες κάπως έτσι

int *my_ptr1, *my_ptr2;

Είναι λοιπόν καλό να βάζεις το * κοντά στο αναγνωριστικό για να ελαχιστοποιείς την ασάφεια. Ή να δηλώνεις μόνο ένα αναγνωριστικό ανά γραμμή. Και αν δεν έχεις χορτάσει το *, καλώς ήρθες στους...

Δείκτης σε δείκτη

Είναι συνηθισμένο να βλέπεις πίνακες συμβολοσειρών να αναπαρίστανται έτσι

#include <stdio.h>

void print_strings(char **passed_in_strings) {
    printf("%s\n", *passed_in_strings);
    printf("%s\n", *(passed_in_strings + 1));
}

int main() {
    char *my_strings[] = {"One string", "Two string"};
    print_strings(my_strings);
    return 0;
}

Μπορεί να μην είναι προφανές, αλλά ο πίνακας συμβολοσειρών δηλώνεται και αρχικοποιείται ως δείκτης σε έναν πίνακα από πίνακες char. Όταν ο πίνακας από πίνακες char περνιέται στη print_strings(), η υπογραφή τον περιγράφει ως δείκτη σε έναν ή περισσότερους δείκτες σε char.

Πώς λοιπόν ξέρουμε ότι το char ** είναι πραγματικά δείκτης σε έναν πίνακα από δείκτες σε πίνακες char, και όχι δείκτης σε δείκτη ενός μόνο char;

Δεν το ξέρουμε.

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

#include <stdio.h>

void print_strings(char **passed_in_strings) {
    // prints: A (followed by gibberish)
    printf("%s\n", *passed_in_strings);
    // prints gibberish
    printf("%s\n", *(passed_in_strings + 1));
}

int main() {
    char my_char = 'A';
    char *my_ptr = &my_char;
    char **my_ptr_to_ptr = &my_ptr;
    print_strings(my_ptr_to_ptr);
    return 0;
}

Αλλά η print_strings() δεν χρησιμοποιείται όπως θα έπρεπε. Η χρήση διαφορετικής υπογραφής δεν βοηθά. Αλλάζοντας την υπογραφή σε αυτή

void print_strings(char *passed_in_strings[]) {
    // code snipped
}

μπορεί και πάλι να μεταγλωττιστεί και να εκτυπώσει πάλι ασυναρτησίες. Αλλά πραγματικά δεν έχει καμία αξία να περνάς δείκτη σε δείκτη ενός μόνο char. Το πιθανότερο είναι ότι, όταν βλέπεις char **, αυτό αναπαριστά έναν δείκτη σε πίνακα δεικτών σε πίνακες char.

Έχουμε δει έναν δείκτη να περνιέται σε μια συνάρτηση. Αλλά γιατί να περνάς έναν δείκτη σε μια συνάρτηση;

Πέρασμα δείκτη σε συνάρτηση

Στη C, τα ορίσματα περνιούνται στις συναρτήσεις κατά τιμή. Αυτό σημαίνει ότι η τιμή αντιγράφεται από το όρισμα και ότι αυτό το αντίγραφο είναι τοπικό στη συνάρτηση. Όταν τελειώνει η συνάρτηση, τελειώνει και η αντιγραμμένη τιμή.

Δες τι συμβαίνει όταν προσπαθούμε να αλλάξουμε τιμές μέσα σε μια συνάρτηση

#include <stdio.h>

void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    //prints:  In 'swap', a is 4 and b is 3.
    printf("In 'swap', a is %d and b is %d.\n", a, b);
}

int main() {
    int a = 3, b = 4;
    swap(a, b);
    // prints: After 'swap', a is 3 and b is 4
    printf("After 'swap', a is %d and b is %d.\n", a, b);
    return 0;
}

Αυτό συμβαίνει επειδή, παρόλο που οι μεταβλητές έχουν τα ίδια ονόματα, τα a και b στη swap() δεν είναι τα ίδια a και b στη main(). Είναι αντίγραφα των τιμών των a και b. Μπορούμε να αλλάξουμε τη swap ώστε να λειτουργεί όπως θέλουμε;

Ναι, μπορούμε! Μπορούμε να περάσουμε τις διευθύνσεις των a και b ως δείκτες. Αλλά τι όφελος θα έχει αυτό, αφού η συνάρτηση αντιγράφει τους δείκτες; Έχει όφελος, επειδή μια αντιγραμμένη διεύθυνση είναι ακόμα η ίδια διεύθυνση. Κάνοντας αποαναφορά στη διεύθυνση, μπορούμε να αποκτήσουμε πρόσβαση στην αρχική τιμή όπως ορίστηκε στη main().

#include <stdio.h>

// pass in pointers to a and b
void swap(int *a, int *b) {
    // set temp to value at a's dereferenced address
    int temp = *a;

    // set a's value to value at b's dereferenced address
    *a = *b;

    // set b's value to temp
    *b = temp;

    // prints: In 'swap', a is 4 and b is 3.
    printf("In 'swap', a is %d and b is %d.\n", *a, *b);
}

int main() {
    int a = 3, b = 4;
    swap(&a, &b);
    // prints: After 'swap', a is 4 and b is 3.
    printf("After 'swap', a is %d and b is %d.\n", a, b);
    return 0;
}

Τώρα η swap έχει πολλά *, αλλά πλέον λειτουργεί όπως θέλουμε.

Ένας τρόπος να διαβάζεις τον τελεστή έμμεσης αναφοράς είναι "η τιμή στο". Έτσι, το *a είναι η τιμή στο a. Πόσο καλά λειτουργεί αυτό με τις συμβολοσειρές;

Δείκτες σε Char

Ξέρουμε ότι η τιμή μιας μεταβλητής πίνακα είναι η διεύθυνση όπου ξεκινά ο πίνακας. Και ξέρουμε ότι μπορούμε να ορίσουμε μια συμβολοσειρά είτε ως πίνακα char, char my_string[], είτε ως δείκτη σε char, char *. Πώς διαφέρουν;

Σε αυτόν τον κώδικα περνάμε ένα όρισμα char * στη describe_me(). Το όρισμα adjective ορίζεται ως πίνακας char και γίνεται δεκτό από τη συνάρτηση ως δείκτης σε char.

#include <stdio.h>
#include <string.h>

// takes pointer to char
void describe_me(char *adjective){
    strcpy(adjective, "evil");
    // prints: I am evil.
    printf("I am %s.\n", adjective);
}

int main() {
    // defined as a char array
    char adjective[] = "good";
    describe_me(adjective);
    // prints: I am really evil.
    printf("I am really %s.\n", adjective);
    
    return 0;
}

Μπορούμε να κάνουμε μια μόνιμη αλλαγή στο adjective, αποδεικνύοντας ότι ο ορισμένος πίνακας char μπορεί να γίνει δεκτός ως δείκτης σε char.

Τώρα ας δοκιμάσουμε να ορίσουμε το adjective απευθείας ως δείκτη σε char.

#include <stdio.h>
#include <string.h>

void describe_me(char *adjective){
    // segmentation fault on this line when program is executed
    strcpy(adjective, "evil");
    printf("I am %s.\n", adjective);
}

int main() {
    char *adjective = "good";
    describe_me(adjective);
    printf("I am really %s.\n", adjective);
    
    return 0;
}

Θυμάσαι ότι οι δείκτες είναι ισχυροί, αλλά και δυνητικά επικίνδυνοι; Αλλά γιατί δεν λειτουργεί; Ένας πίνακας είναι δείκτης και ο δείκτης λειτούργησε με έναν πίνακα char! Ο λόγος είναι ότι, προηγουμένως, το adjective είχε οριστεί ως πίνακας char, ο οποίος είναι μια μεταβλητή που ανήκει στη main(). Τώρα, το adjective ορίζεται ως literal συμβολοσειράς, που σημαίνει ότι τα chars γράφονται σε στατική μνήμη μόνο για ανάγνωση, η οποία ανήκει σε ολόκληρο το πρόγραμμα. Και τα δύο είναι δείκτες, αλλά δείχνουν σε διαφορετικά πράγματα. Το να προσπαθείς να αλλάξεις μια literal τιμή συμβολοσειράς είναι σαν να προσπαθείς να αλλάξεις την literal τιμή του 5. Δεν επιτρέπεται. Έτσι, το πρόβλημα δεν είναι πραγματικά ότι είναι δείκτης. Το πρόβλημα είναι σε τι δείχνει.

Επεξεργασία μέσω GitHub Ο σύνδεσμος ανοίγει σε νέο παράθυρο ή καρτέλα