Οι διευθύνσεις μνήμης στη γλώσσα C αναφέρονται στις μοναδικές τοποθεσίες μνήμης που αντιστοιχούν σε μεταβλητές ή αντικείμενα. Κάθε μεταβλητή ή αντικείμενο στη C έχει μια μοναδική διεύθυνση μνήμης που τον αναγνωρίζει μέσα στο σύστημα.
Οι διευθύνσεις μνήμης εκφράζονται συνήθως σε αριθμητική μορφή, είτε σε δεκαεξαδική (hexadecimal) είτε σε δεκαδική (decimal). Αυτές οι διευθύνσεις μνήμης αναπαριστούν τη θέση της μεταβλητής ή του αντικειμένου στην φυσική μνήμη του υπολογιστή.
Οι διευθύνσεις μνήμης χρησιμοποιούνται για πολλούς σκοπούς, όπως η πρόσβαση και η αλληλεπίδραση με μεταβλητές, η διαμοιρασμός μνήμης μεταξύ διαφορετικών τμημάτων του προγράμματος, η πρόσβαση σε συναρτήσεις με χρήση δεικτών και άλλα.
Συνολικά, οι διευθύνσεις μνήμης είναι σημαντικές για τον έλεγχο και τη διαχείριση της μνήμης σε γλώσσες όπως η C, όπου ο προγραμματιστής έχει άμεση πρόσβαση και έλεγχο στη μνήμη.
Με τη χρήση του τελεστή αναφοράς &
στη γλώσσα C, μπορούμε να αποκτήσουμε τη διεύθυνση μνήμης μιας μεταβλητής. Αν τοποθετήσουμε τον τελεστή αναφοράς &
μπροστά από τη μεταβλητή, θα επιστραφεί η διεύθυνση μνήμης της μεταβλητής.
Παράδειγμα
#include <stdio.h> int main() { int num = 10; printf("The memory address of variable num is: %p", &num); return 0; }
Στον παραπάνω κώδικα, χρησιμοποιείται η βιβλιοθήκη stdio.h
για την εισαγωγή των απαραίτητων συναρτήσεων εισόδου/εξόδου. Η συνάρτηση main()
είναι η κύρια συνάρτηση που εκτελείται κατά την εκτέλεση του προγράμματος.
Δημιουργείται μια μεταβλητή num
τύπου int
με τιμή 10. Στη συνέχεια, χρησιμοποιείται η συνάρτηση printf()
για να εμφανιστεί η διεύθυνση μνήμης της μεταβλητής num
. Το %p
αναπαριστά τη μορφή εκτύπωσης για μια διεύθυνση μνήμης.
Τέλος, η συνάρτηση main()
επιστρέφει την τιμή 0, υποδεικνύοντας ότι το πρόγραμμα ολοκληρώθηκε επιτυχώς.
Όταν εκτελέσετε τον παραπάνω κώδικα στη γλώσσα C, το αποτέλεσμα που θα εμφανιστεί στην οθόνη θα είναι η διεύθυνση μνήμης της μεταβλητής num
. Η διεύθυνση μνήμης θα εμφανίζεται σε μια μορφή που αναπαριστά τη διεύθυνση στη μνήμη του υπολογιστή.
Παράδειγμα αποτελέσματος:
The memory address of variable num is: 0x7ffeea63a9dc
Η ακριβής διεύθυνση μνήμης που θα εμφανιστεί μπορεί να διαφέρει ανάλογα με το σύστημα και τις ρυθμίσεις του υπολογιστή σας.
Για να δηλώσουμε μια μεταβλητή τύπου δείκτης, χρησιμοποιούμε τη σύνταξη type *variable_name;
:
int *ptr;
Αυτός ο κώδικας δηλώνει μια μεταβλητή τύπου int
με όνομα ptr
, η οποία είναι δείκτης σε ένα ακέραιο. Η μεταβλητή αυτή δεν έχει αρχικοποιηθεί ακόμα, οπότε η τιμή της θα είναι μη ορισμένη.
Για να αποθηκεύσουμε τη διεύθυνση μνήμης της μεταβλητής num
στον δείκτη ptr
, χρησιμοποιούμε τον τελεστή αναθέσεως (=):
ptr = #
Αυτός ο κώδικας αντιστοιχεί τη διεύθυνση μνήμης της μεταβλητής num
στον δείκτη ptr
.
Σημειώστε ότι πρέπει να δηλώσετε τον δείκτη στο ίδιο τύπο με τη μεταβλητή στην οποία θα δείχνει. Δηλαδή, αν η μεταβλητή είναι τύπου int
, ο δείκτης πρέπει επίσης να είναι τύπου int *
.
[adinserter block=”2″]
Ας δούμε ένα παράδειγμα που συνδυάζει όλα αυτά:
Παράδειγμα
#include <stdio.h> int main() { int num = 10; int *ptr; ptr = # printf("The value of variable num is: %d\n", num); // Εκτύπωση της τιμής της μεταβλητής num printf("The memory address of variable num is: %p\n", &num); // Εκτύπωση της διεύθυνσης μνήμης της μεταβλητής num printf("The value of the pointer ptr is: %p\n", ptr); // Εκτύπωση της τιμής του δείκτη ptr printf("The value pointed to by ptr is: %d\n", *ptr); // Εκτύπωση της τιμής που δείχνει ο δείκτης ptr return 0; }
Ο κώδικας αυτός προβάλλει πληροφορίες σχετικά με τη μεταβλητή num
και τον δείκτη ptr
. Αρχικά, ορίζεται η μεταβλητή num
με τιμή 10 και δημιουργείται ο δείκτης ptr
. Στη συνέχεια, ο δείκτης ptr
αναθέτεται να δείχνει στη διεύθυνση μνήμης της μεταβλητής num
με τη χρήση του τελεστή αναφοράς &
.
Στις εντολές printf
, εκτυπώνονται οι παρακάτω πληροφορίες:
- Η τιμή της μεταβλητής
num
με τη χρήση της φράσης"The value of variable num is: %d\n"
. - Η διεύθυνση μνήμης της μεταβλητής
num
με τη χρήση της φράσης"The memory address of variable num is: %p\n"
. - Η τιμή του δείκτη
ptr
με τη χρήση της φράσης"The value of the pointer ptr is: %p\n"
. - Η τιμή που δείχνει ο δείκτης
ptr
(δηλαδή η τιμή της μεταβλητήςnum
) με τη χρήση της φράσης"The value pointed to by ptr is: %d\n"
.
Αυτές οι πληροφορίες εμφανίζονται στην οθόνη κατά την εκτέλεση του προγράμματος. Οι τιμές που εκτυπώνονται είναι:
The value of variable num is: 10 The memory address of variable num is: [μνήμης] The value of the pointer ptr is: [μνήμης] The value pointed to by ptr is: 10
Οι [μνήμης]
αντιστοιχούν στις πραγματικές διευθύνσεις μνήμης που εξαρτώνται από το συγκεκριμένο σύστημα εκτέλεσης του προγράμματος.
Οι δείκτες στην C επιτρέπουν την αναφορά σε δεδομένα στη μνήμη αντί για τις τιμές των μεταβλητών. Αυτό μας επιτρέπει να δημιουργούμε πολύπλοκες δομές δεδομένων, όπως δυναμικούς πίνακες και συνδεδεμένες λίστες. Οι δείκτες είναι επίσης χρήσιμοι για την αποτελεσματική διαχείριση της μνήμης στην C.
Στο παραπάνω παράδειγμα, χρησιμοποιούμε τον τελεστή αποαναφοράς (*
) για να αποκτήσουμε πρόσβαση στην τιμή που αντιστοιχεί στη διεύθυνση μνήμης που δείχνει ο δείκτης ptr
. Αυτή η διαδικασία ονομάζεται αποαναφορά (dereferencing) του δείκτη, καθώς μας επιτρέπει να αναφερθούμε στην πραγματική τιμή της μεταβλητής που βρίσκεται στη συγκεκριμένη διεύθυνση μνήμης.
Ο τελεστής αναφοράς *
αντιστοιχεί στον τελεστή αναφοράς &
. Όταν χρησιμοποιούμε τον τελεστή &
, παίρνουμε τη διεύθυνση μνήμης μιας μεταβλητής, ενώ όταν χρησιμοποιούμε τον τελεστή *
, παίρνουμε την τιμή που αντιστοιχεί σε αυτή τη διεύθυνση μνήμης.
Σημειώνεται ότι, παρόλο που στο παραπάνω παράδειγμα χρησιμοποιήσαμε τον τελεστή *
για να αποαναφερθούμε στον δείκτη, όπως είδαμε προηγουμένως, ο δείκτης μπορεί επίσης να χρησιμοποιηθεί για να αναφερθεί σε δεδομένα στη μνήμη, χωρίς την αποαναφορά.
Στο επόμενο κεφάλαιο, θα εξετάσουμε πώς μπορούμε να χρησιμοποιήσουμε τους δείκτες για τη δημιουργία δομών δεδομένων, όπως πίνακες και συνδεδεμένες λίστες, και πώς μπορούμε να τους χρησιμοποιήσουμε για τη διαχείριση της μνήμης στην C.
Οι δείκτες αποτελούν ένα ισχυρό εργαλείο στη γλώσσα προγραμματισμού C, καθώς μας επιτρέπουν να λειτουργούμε με μεταβλητές που απαιτούν δυναμική δέσμευση μνήμης και να επιτυγχάνουμε πιο αποδοτική χρήση της μνήμης.
Επιπλέον, οι δείκτες αποτελούν σημαντικό εργαλείο για την ανταλλαγή δεδομένων μεταξύ συναρτήσεων στην C. Μπορούμε να περάσουμε δείκτες ως παραμέτρους σε συναρτήσεις και να χρησιμοποιήσουμε τους δείκτες για να τροποποιήσουμε τις τιμές των μεταβλητών που βρίσκονται εκτός της συνάρτησης.
Πρέπει να έχουμε υπόψη ότι, αν και οι δείκτες είναι χρήσιμοι, είναι επίσης ευάλωτοι σε σφάλματα. Οι δείκτες μπορούν να πάρουν μη έγκυρες τιμές, όπως την τιμή NULL, και αυτό μπορεί να οδηγήσει σε προβλήματα ασφαλείας στο πρόγραμμά μας εάν δεν τους χρησιμοποιήσουμε σωστά. Για να αποφύγουμε τέτοια προβλήματα, πρέπει να είμαστε προσεκτικοί και να χειριζόμαστε τους δείκτες με προσοχή.
Τέλος, είναι σημαντικό να σημειώσουμε ότι οι δείκτες αποτελούν ένα θεμελιώδες κομμάτι της C και ότι η κατανόησή τους είναι ζωτικής σημασίας για την ανάπτυξη προηγμένων προγραμμάτων σε αυτήν τη γλώσσα προγραμματισμού. Επιπλέον, η κατανόηση των δεικτών στη C μπορεί να συμβάλει στην καλύτερη κατανόηση και άλλων γλωσσών προγραμματισμού, όπως η C++, η Java και η Python.
Στη γλώσσα C, τα arrays και οι pointers σχετίζονται στενά μεταξύ τους, καθώς τα στοιχεία ενός πίνακα αποθηκεύονται συνεχόμενα στη μνήμη και ένας δείκτης μπορεί να δείξει στη διεύθυνση μνήμης του πρώτου στοιχείου του πίνακα.
[adinserter block=”3″]
Η σύνταξη για την αναφορά σε ένα στοιχείο πίνακα χρησιμοποιώντας έναν δείκτη είναι παρόμοια με τη σύνταξη που χρησιμοποιούμε για την αναφορά σε μια μεταβλητή χρησιμοποιώντας τον τελεστή αναφοράς &
.
Για παράδειγμα, αν έχουμε τον ακόλουθο πίνακα:
int numbers[5] = {1, 2, 3, 4, 5};
Μπορούμε να δημιουργήσουμε ένα δείκτη που δείχνει στη διεύθυνση μνήμης του πρώτου στοιχείου του πίνακα:
int *ptr = &numbers[0];
Ή, ακόμη πιο απλά, μπορούμε να αφήσουμε τον μεταγλωττιστή να υπολογίσει τη διεύθυνση μνήμης του πρώτου στοιχείου του πίνακα αυτόματα:
int *ptr = numbers;
Για να αναφερθούμε σε ένα στοιχείο πίνακα χρησιμοποιώντας έναν δείκτη, μπορούμε να χρησιμοποιήσουμε τον τελεστή αναφοράς *
. Για παράδειγμα, αν έχουμε τον ακόλουθο πίνακα:
int numbers[5] = {1, 2, 3, 4, 5};
Μπορούμε να αναφερθούμε στο τρίτο στοιχείο του πίνακα χρησιμοποιώντας έναν δείκτη ως εξής:
int *ptr = &numbers[2]; // ή int *ptr = numbers + 2; printf("%d", *ptr); // θα εκτυπωθεί: 3
Επίσης, η σύνταξη ptr[i]
μπορεί να χρησιμοποιηθεί για την αναφορά στο i-οστό στοιχείο του πίνακα που δείχνει ο δείκτης ptr
. Για παράδειγμα:
int numbers[5] = {1, 2, 3, 4, 5}; int *ptr = &numbers[0]; printf("%d", ptr[2]); // θα εκτυπωθεί: 3
Σημειώνουμε ότι η σύνταξη *(ptr+i)
μπορεί να χρησιμοποιηθεί αντί για τη σύνταξη ptr[i]
για να αναφερθούμε στο i-οστό στοιχείο του πίνακα.
Για παράδειγμα, αν έχουμε τον ακόλουθο πίνακα:
int numbers[5] = {1, 2, 3, 4, 5};
Μπορούμε να εκτυπώσουμε κάθε στοιχείο του πίνακα με τον ακόλουθο τρόπο:
for(int i = 0; i < 5; i++) { printf("%d ", numbers[i]); }
Αυτό θα εκτυπώσει:
1 2 3 4 5
Σημειώνουμε ότι μπορούμε να χρησιμοποιήσουμε τον δείκτη στην παραπάνω επανάληψη ως εξής:
for(int i = 0; i < 5; i++) { printf("%d ", *(numbers + i)); }
Αυτό θα εκτυπώσει το ίδιο αποτέλεσμα.
[adinserter block=”4″]
Μπορούμε να εκτυπώσουμε τη διεύθυνση μνήμης κάθε στοιχείου του πίνακα με τον ακόλουθο τρόπο:
for(int i = 0; i < 5; i++) { printf("The memory address of numbers[%d] is %p\n", i, &numbers[i]); }
Αυτό θα εκτυπώσει κάτι σαν το παρακάτω:
The memory address of numbers[0] is 0x7ffeee42b870 The memory address of numbers[1] is 0x7ffeee42b874 The memory address of numbers[2] is 0x7ffeee42b878 The memory address of numbers[3] is 0x7ffeee42b87c The memory address of numbers[4] is 0x7ffeee42b880
Σημειώνουμε ότι η συνάρτηση printf() χρησιμοποιεί το format specifier %p
για να εκτυπώσει διευθύνσεις μνήμης.
Στο παραπάνω παράδειγμα, η μεταβλητή numbers
είναι ένας πίνακας τύπου int και κάθε στοιχείο του πίνακα έχει μέγεθος 4 bytes (στα περισσότερα συστήματα).
Συνεπώς, η διεύθυνση μνήμης για το πρώτο στοιχείο numbers[0]
είναι 0x7ffeee42b870
, για το δεύτερο στοιχείο numbers[1]
είναι 0x7ffeee42b874
(το οποίο διαφέρει από το 0x7ffeee42b870
κατά 4), κ.λπ.
Επίσης, σημειώνουμε ότι η διεύθυνση μνήμης ενός πίνακα αναφέρεται στη διεύθυνση του πρώτου στοιχείου του πίνακα. Στο παραπάνω παράδειγμα, η διεύθυνση μνήμης του πίνακα numbers
είναι 0x7ffeee42b870
, η οποία αντιστοιχεί στη διεύθυνση μνήμης του πρώτου στοιχείου numbers[0]
.
Στη C, το όνομα ενός πίνακα αντιστοιχεί στη διεύθυνση μνήμης του πρώτου στοιχείου του πίνακα.
Για παράδειγμα, στον παρακάτω κώδικα, το όνομα του πίνακα numbers
αντιστοιχεί στη διεύθυνση μνήμης του πρώτου στοιχείου του πίνακα (&numbers[0]
):
int numbers[5] = {1, 2, 3, 4, 5}; printf("The memory address of the first element is %p\n", numbers); printf("The memory address of the first element is %p\n", &numbers[0]);
Αυτό θα εκτυπώσει κάτι σαν το παρακάτω:
The memory address of the first element is 0x7fff5586f4c0 The memory address of the first element is 0x7fff5586f4c0
Σημειώνουμε ότι και τα δύο printf() statements εκτυπώνουν τη διεύθυνση μνήμης του πρώτου στοιχείου του πίνακα, το οποίο αντιστοιχεί στη διεύθυνση που αντιστοιχεί στο όνομα του πίνακα numbers
.
Αν έχουμε έναν πίνακα myNumbers
και ένα δείκτη p
που δείχνει στον πρώτο αριθμό του πίνακα, μπορούμε να αναφερόμαστε στα στοιχεία του πίνακα χρησιμοποιώντας τον δείκτη.
Στο παρακάτω παράδειγμα, έχουμε έναν πίνακα myNumbers
και ένα δείκτη p
που δείχνει στον πρώτο αριθμό του πίνακα. Έπειτα, εκτυπώνουμε τις τιμές του πίνακα χρησιμοποιώντας το δείκτη p
και τον τελεστή αναφοράς *
:
int myNumbers[5] = {10, 20, 30, 40, 50}; int *p; p = myNumbers; for(int i = 0; i < 5; i++) { printf("%d ", *(p+i)); }
Αυτό θα εκτυπώσει:
10 20 30 40 50
Σημειώνουμε ότι στον παραπάνω κώδικα, η έκφραση *(p+i)
επιστρέφει την τιμή του στοιχείου στην i-η θέση του πίνακα.
Αν έχουμε ένα δείκτη p
που δείχνει στον πρώτο αριθμό ενός πίνακα και θέλουμε να αναφερθούμε σε άλλα στοιχεία του πίνακα, μπορούμε να αυξήσουμε την τιμή του δείκτη για να δείξει σε διαφορετικά στοιχεία του πίνακα.
[adinserter block=”5″]
Στο παρακάτω παράδειγμα, ο δείκτης p
δείχνει στο πρώτο στοιχείο του πίνακα myNumbers
. Χρησιμοποιούμε έναν for loop για να εκτυπώσουμε την τιμή του κάθε στοιχείου του πίνακα. Κάθε φορά που εκτελείται ο βρόχος, αυξάνουμε την τιμή του δείκτη p
κατά 1, ώστε να δείχνει στο επόμενο στοιχείο του πίνακα.
int myNumbers[5] = {10, 20, 30, 40, 50}; int *p; p = myNumbers; for(int i = 0; i < 5; i++) { printf("%d ", *p); p++; }
Αυτό θα εκτυπώσει:
10 20 30 40 50
Σημειώνουμε ότι ο τελεστής ++
αυξάνει την τιμή του δείκτη κατά 1. Επίσης, στο παραπάνω παράδειγμα χρησιμοποιούμε τον τελεστή αναφοράς *
για να πάρουμε την τιμή του στοιχείου στο οποίο δείχνει ο δείκτης p
.
Όπως αναφέρθηκε και προηγουμένως, ο δείκτης p
δείχνει στο πρώτο στοιχείο του πίνακα myNumbers
. Μπορούμε να χρησιμοποιήσουμε τον δείκτη p
σε έναν for loop για να εκτυπώσουμε τα στοιχεία του πίνακα.
Το παρακάτω παράδειγμα δείχνει πώς να εκτυπώσετε τα στοιχεία του πίνακα myNumbers
χρησιμοποιώντας τον δείκτη p
σε έναν for loop:
int myNumbers[5] = {10, 20, 30, 40, 50}; int *p; p = myNumbers; for(int i = 0; i < 5; i++) { printf("%d ", *(p+i)); }
Αυτό θα εκτυπώσει:
10 20 30 40 50
Σημειώνουμε ότι η έκφραση *(p+i)
επιστρέφει την τιμή του στοιχείου στην i-η θέση του πίνακα.
Χρησιμοποιώντας δείκτες, μπορούμε να αλλάξουμε τις τιμές των στοιχείων ενός πίνακα. Για παράδειγμα, μπορούμε να χρησιμοποιήσουμε ένα δείκτη για να αλλάξουμε την τιμή ενός στοιχείου στον πίνακα.
Στο παρακάτω παράδειγμα, χρησιμοποιούμε τον δείκτη p
για να αλλάξουμε την τιμή του τρίτου στοιχείου του πίνακα myNumbers
σε 100:
int myNumbers[5] = {10, 20, 30, 40, 50}; int *p; p = &myNumbers[2]; // δείκτης p δείχνει στο τρίτο στοιχείο του πίνακα *p = 100; // αλλάζουμε την τιμή του τρίτου στοιχείου στο 100 for(int i = 0; i < 5; i++) { printf("%d ", myNumbers[i]); }
Αυτό θα εκτυπώσει:
10 20 100 40 50
Σημειώνουμε ότι ο δείκτης p
δείχνει στο τρίτο στοιχείο του πίνακα myNumbers
, και χρησιμοποιούμε τον τελεστή αναφοράς *
για να αλλάξουμε την τιμή του τρίτου στοιχείου σε 100.