Κατανόηση Διευθύνσεων Μνήμης και Έμμεση Πρόσβαση σε Δεδομένα
Οι δείκτες (pointers) είναι ένα από τα πιο ισχυρά χαρακτηριστικά της C. Μας επιτρέπουν να:
Στη γλώσσα C, κάθε μεταβλητή αποθηκεύεται κάπου στη μνήμη του υπολογιστή. Αυτή η θέση έχει μια διεύθυνση (address).
Σκεφτείτε τη μνήμη σαν ένα μεγάλο κτίριο με δωμάτια:
Ένας δείκτης είναι μια μεταβλητή που αποθηκεύει τη διεύθυνση μιας άλλης μεταβλητής.
🔎 Σκεφτείτε τους δείκτες ως "δείκτες" που δείχνουν πού βρίσκεται μια πληροφορία.
Ο δείκτης p (στη διεύθυνση 0x2000) δείχνει στον x (στη διεύθυνση 0x1000)
Για να δηλώσουμε έναν δείκτη χρησιμοποιούμε το σύμβολο *.
int *p; // p είναι δείκτης σε ακέραιο float *f; // f είναι δείκτης σε δεκαδικό char *c; // c είναι δείκτης σε χαρακτήρα
Το * στη δήλωση δεν σημαίνει πολλαπλασιασμό — σημαίνει "δείκτης σε".
int *p; διαβάζεται: "Ο p είναι δείκτης σε int"float *f; διαβάζεται: "Ο f είναι δείκτης σε float"| Δήλωση | Τύπος Μεταβλητής | Τι Αποθηκεύει |
|---|---|---|
int x; |
Ακέραιος | Μια ακέραια τιμή (π.χ. 10) |
int *p; |
Δείκτης σε ακέραιο | Διεύθυνση μνήμης όπου βρίσκεται ένας int |
Ο τελεστής & (address-of operator) χρησιμοποιείται για να πάρουμε τη διεύθυνση μιας μεταβλητής.
int x = 5; // Δηλώνουμε έναν ακέραιο με τιμή 5 int *p; // Δηλώνουμε έναν δείκτη σε int p = &x; // Ο p τώρα δείχνει στον x (παίρνει τη διεύθυνση του x)
Αυτό σημαίνει: Ο p δεν έχει την τιμή 5. Έχει τη διεύθυνση όπου βρίσκεται ο x στη μνήμη.
Δημιουργείται ο x με τιμή 5 στη διεύθυνση 0x1000
Δημιουργείται ο δείκτης p (δεν δείχνει ακόμα πουθενά)
Ο p αποθηκεύει τη διεύθυνση 0x1000 και δείχνει στον x!
Για να πάρουμε την τιμή στη διεύθυνση που δείχνει ο δείκτης, χρησιμοποιούμε το * (dereference operator).
int x = 10; int *p = &x; printf("%d", *p); // εκτυπώνει την τιμή του x → 10
Το *p σημαίνει: "πήγαινε στη διεύθυνση που έχει ο p και διάβασε την τιμή εκεί".
p → η διεύθυνση (π.χ. 0x1000)*p → η τιμή σε αυτή τη διεύθυνση (π.χ. 10)| Έκφραση | Τι Επιστρέφει | Παράδειγμα Τιμής |
|---|---|---|
x |
Την τιμή του x | 10 |
&x |
Τη διεύθυνση του x | 0x1000 |
p |
Τη διεύθυνση που αποθηκεύει ο p | 0x1000 (ίδιο με &x) |
*p |
Την τιμή στη διεύθυνση που δείχνει ο p | 10 (ίδιο με x) |
#include <stdio.h> // Βιβλιοθήκη για εντολές εισόδου/εξόδου (printf) int main() { int x = 10; // Δήλωση μεταβλητής x τύπου int και αρχικοποίηση με 10 int *p; // Δήλωση δείκτη p που θα δείχνει σε ακέραιο (int) p = &x; // Ο p παίρνει ως τιμή τη διεύθυνση της μεταβλητής x // Εκτύπωση της τιμής της μεταβλητής x printf("Τιμή του x: %d\n", x); // Εκτύπωση της διεύθυνσης μνήμης όπου βρίσκεται ο x printf("Διεύθυνση του x: %p\n", &x); // Εκτύπωση της διεύθυνσης που έχει αποθηκευτεί στον δείκτη p (θα είναι ίδια με του x) printf("Διεύθυνση που αποθηκεύει ο p: %p\n", p); // Εκτύπωση της τιμής του x μέσω του δείκτη p (με αποαναφορά - dereferencing) printf("Τιμή μέσω δείκτη p: %d\n", *p); return 0; // Τερματισμός προγράμματος με επιτυχία }
Το %p χρησιμοποιείται για την εκτύπωση διευθύνσεων μνήμης σε δεκαεξαδική μορφή.
Αφού ο δείκτης δείχνει στον x, μπορούμε να αλλάξουμε τον x μέσω του δείκτη!
int x = 10; int *p = &x; printf("Πριν: x = %d\n", x); // Εκτυπώνει: 10 *p = 20; // Αλλάζει τον x σε 20 μέσω του δείκτη! printf("Μετά: x = %d\n", x); // Εκτυπώνει: 20
Μέσω του *p αλλάξαμε την τιμή στη διεύθυνση 0x1000 (δηλαδή τον x)!
#include <stdio.h> int main() { int x = 10; int *p = &x; printf("Αρχική τιμή x: %d\n", x); *p = 20; // Αλλαγή μέσω δείκτη printf("Μετά το *p = 20, x = %d\n", x); *p = *p + 5; // Προσθήκη 5 μέσω δείκτη printf("Μετά το *p = *p + 5, x = %d\n", x); return 0; }
int *p; // Ο p δεν δείχνει πουθενά (wild pointer) *p = 10; // ⚠️ ΣΦΑΛΜΑ! Δεν ξέρουμε πού γράφουμε!
Αυτό είναι επικίνδυνο! Γράφουμε σε τυχαία θέση μνήμης!
int x = 5; int *p = &x; // ✅ Ο p δείχνει στον x
| Κατάσταση Δείκτη | Περιγραφή | Ασφαλής; |
|---|---|---|
| Μη αρχικοποιημένος | int *p; |
❌ Επικίνδυνο |
| Δείχνει σε μεταβλητή | int *p = &x; |
✅ Ασφαλές |
Στη C, το όνομα ενός πίνακα είναι δείκτης στο πρώτο του στοιχείο!
int arr[3] = {1, 2, 3}; int *p = arr; // ⚠️ Όχι &arr, γιατί arr είναι ήδη διεύθυνση! // Το arr είναι το ίδιο με το &arr[0]
arr = 0x1000 (δείχνει στο πρώτο στοιχείο)
arr[1]
Αυτό σημαίνει το στοιχείο με δείκτη 1 στον πίνακα → 2.
arr*(arr + 1)
arr είναι η διεύθυνση του πρώτου στοιχείουarr + 1 είναι η διεύθυνση του δεύτερου στοιχείου*(arr + 1) σημαίνει διάβασε τη τιμή εκείp*(p + 1)
Αφού:
p = arr;
τότε p + 1 είναι η ίδια διεύθυνση με το arr + 1. Άρα και αυτό επιστρέφει 2.
| Τρόπος | Ισοδύναμο |
|---|---|
arr[2] |
κλασικά |
*(arr + 2) |
πίνακας σαν pointer |
*(p + 2) |
pointer arithmetic |
Και τα 3 δίνουν: 3
Η έκφραση arr[i] είναι ακριβώς ίδια με *(arr + i) στη C.
Ακόμα και ο τελεστής [] λειτουργεί εσωτερικά σαν pointer arithmetic!
arr (p) → [ 1 ] [ 2 ] [ 3 ] ↑ ↑ arr+1 arr+2
arr[0] = *(arr + 0) = *(p + 0)arr[1] = *(arr + 1) = *(p + 1)arr[2] = *(arr + 2) = *(p + 2)int arr[3] = {1, 2, 3}; int *p = arr; // Όλοι αυτοί οι τρόποι είναι ισοδύναμοι: printf("%d\n", arr[0]); // 1 printf("%d\n", *arr); // 1 printf("%d\n", *p); // 1 // Για το δεύτερο στοιχείο: printf("%d\n", arr[1]); // 2 printf("%d\n", *(arr + 1)); // 2 (pointer arithmetic) printf("%d\n", *(p + 1)); // 2 // Για το τρίτο στοιχείο: printf("%d\n", arr[2]); // 3 printf("%d\n", *(arr + 2)); // 3 printf("%d\n", *(p + 2)); // 3
| Έκφραση | Τι Κάνει | Αποτέλεσμα |
|---|---|---|
arr |
Διεύθυνση πρώτου στοιχείου | 0x1000 |
arr[i] |
Τιμή i-οστού στοιχείου | arr[1] = 2 |
*(arr + i) |
Ίδιο με arr[i] | *(arr + 1) = 2 |
p + i |
Διεύθυνση i-οστού στοιχείου | 0x1000 + 4*i |
Εκφώνηση:
Δημιουργήστε ένα πρόγραμμα σε C που:
{4, 7, 1, 3, 9}.p και τον αρχικοποιεί ώστε να δείχνει στο πρώτο στοιχείο του πίνακα.*(p + i).#include <stdio.h> int main() { int arr[5] = {4, 7, 1, 3, 9}; int *p = arr; // ο p δείχνει στο πρώτο στοιχείο του πίνακα int i; printf("Στοιχεία πίνακα με χρήση δείκτη:\n"); for (i = 0; i < 5; i++) { printf("%d ", *(p + i)); // χρήση pointer arithmetic } printf("\n"); return 0; }
p → [4] [7] [1] [3] [9] ↑ ↑ ↑ ↑ ↑ p+0 p+1 p+2 p+3 p+4
Όταν περνάμε μια μεταβλητή σε μια συνάρτηση κανονικά (call by value), η συνάρτηση παίρνει αντίγραφο της τιμής. Αλλαγές στη συνάρτηση δεν επηρεάζουν την αρχική μεταβλητή.
Με δείκτες (call by reference), η συνάρτηση μπορεί να αλλάξει την πραγματική μεταβλητή!
Όταν καλούμε μια συνάρτηση στη γλώσσα C, μπορούμε να της στείλουμε πληροφορίες. Αυτό ονομάζεται πέρασμα παραμέτρων. Υπάρχουν δύο βασικοί τρόποι με τους οποίους μπορεί να γίνει αυτό:
Στο πέρασμα κατά τιμή, αυτό που "δίνουμε" στη συνάρτηση είναι απλώς ένα αντίγραφο της τιμής. Η συνάρτηση δουλεύει με αυτό το αντίγραφο, χωρίς να επηρεάζει την αρχική μεταβλητή έξω από αυτήν.
💬 Παρομοίωση: σαν να δίνεις σε κάποιον μια φωτοτυπία. Αν την τσαλακώσει, η αυθεντική παραμένει άθικτη.
Στο πέρασμα κατά αναφορά, δεν στέλνουμε μια τιμή, αλλά τη διεύθυνση της πραγματικής μεταβλητής στη μνήμη. Έτσι, η συνάρτηση δεν δουλεύει σε αντίγραφο, αλλά στο ίδιο το πρωτότυπο.
💬 Παρομοίωση: σαν να δίνεις σε κάποιον το αυθεντικό έγγραφο. Αν το αλλάξει, αλλάζει το πραγματικό.
Η γλώσσα C δεν έχει μηχανισμό "call by reference" αυτόματα. Για να το πετύχουμε, χρησιμοποιούμε δείκτες (pointers). Με ένα δείκτη μπορούμε να περάσουμε στη συνάρτηση τη διεύθυνση μιας μεταβλητής, ώστε να μπορεί να την αλλάξει.
| Μέθοδος | Τι δίνουμε στη συνάρτηση; | Μπορεί να αλλάξει την αρχική τιμή; |
|---|---|---|
| Call by Value | Αντίγραφο της τιμής | ❌ Όχι |
| Call by Reference | Τη διεύθυνση (με δείκτη) | ✔ Ναι |
🔎 Αυτό θα το χρησιμοποιήσουμε όταν θέλουμε η συνάρτηση να επιδράσει πραγματικά στα δεδομένα μας.
#include <stdio.h> void tryToAddOne(int x) { x = x + 1; // Αλλάζει μόνο το τοπικό αντίγραφο printf("Μέσα στη συνάρτηση: x = %d\n", x); } int main() { int x = 10; printf("Πριν: x = %d\n", x); tryToAddOne(x); // Περνάμε αντίγραφο printf("Μετά: x = %d\n", x); // x δεν άλλαξε! return 0; }
#include <stdio.h> void addOne(int *p) { *p = *p + 1; // Αλλάζει την πραγματική μεταβλητή! } int main() { int x = 10; printf("Πριν: x = %d\n", x); addOne(&x); // Περνάμε τη διεύθυνση του x printf("Μετά: x = %d\n", x); // x άλλαξε! return 0; }
| Call by Value | Call by Reference |
|---|---|
void func(int x) { x = x + 1; } func(y); // αντίγραφο |
void func(int *p) { *p = *p + 1; } func(&y); // διεύθυνση |
| Η συνάρτηση δουλεύει με αντίγραφο | Η συνάρτηση δουλεύει με το πρωτότυπο |
| Δεν αλλάζει την αρχική μεταβλητή | Αλλάζει την αρχική μεταβλητή |
Συνάρτηση που ανταλλάσσει τις τιμές δύο μεταβλητών:
#include <stdio.h> void swap(int *a, int *b) { int temp; // Προσωρινή μεταβλητή temp = *a; // Αποθηκεύουμε την τιμή του a *a = *b; // Βάζουμε στο a την τιμή του b *b = temp; // Βάζουμε στο b την παλιά τιμή του a } int main() { int x = 3; int y = 7; printf("Πριν το swap: x = %d, y = %d\n", x, y); swap(&x, &y); // Περνάμε τις διευθύνσεις printf("Μετά το swap: x = %d, y = %d\n", x, y); return 0; }
| Λόγος | Παράδειγμα | Όφελος |
|---|---|---|
| Αποδοτική διαχείριση μνήμης | Δυναμική εκχώρηση με malloc() | Δημιουργία δομών μεταβλητού μεγέθους |
| Πέρασμα παραμέτρων κατά αναφορά | Αλλαγή μεταβλητών μέσα σε συναρτήσεις | Συναρτήσεις που αλλάζουν πολλές τιμές |
| Χειρισμός πινάκων και strings | Επεξεργασία strings με char* | Αποδοτική πρόσβαση σε μεγάλα δεδομένα |
| Χρήση δομών | Linked lists, trees, stacks | Δημιουργία σύνθετων δομών δεδομένων |
| Επιστροφή πολλών τιμών | Συνάρτηση που επιστρέφει 2+ τιμές | Ευελιξία στις συναρτήσεις |
#include <stdio.h> // Συνάρτηση που υπολογίζει άθροισμα ΚΑΙ γινόμενο void calculate(int a, int b, int *sum, int *product) { *sum = a + b; // Αποθηκεύουμε το άθροισμα *product = a * b; // Αποθηκεύουμε το γινόμενο } int main() { int s, p; // Μεταβλητές για τα αποτελέσματα calculate(5, 3, &s, &p); // Περνάμε διευθύνσεις printf("Άθροισμα: %d\n", s); // 8 printf("Γινόμενο: %d\n", p); // 15 return 0; }
Εκφώνηση: Να γραφτεί πρόγραμμα που:
xp που δείχνει στον xxxx μέσω του δείκτη (*p)#include <stdio.h> int main() { int x = 10; // Δηλώνουμε ακέραιο με τιμή 10 int *p; // Δηλώνουμε δείκτη σε ακέραιο p = &x; // Ο p δείχνει στον x (παίρνει τη διεύθυνσή του) // Εκτύπωση πληροφοριών printf("Τιμή του x: %d\n", x); printf("Διεύθυνση του x (&x): %p\n", &x); printf("Διεύθυνση που αποθηκεύει ο p: %p\n", p); printf("Τιμή του x μέσω p (*p): %d\n", *p); return 0; }
Εκφώνηση: Να γραφτεί πρόγραμμα που:
x = 5p που δείχνει στον xx κατά 10 μέσω του δείκτηx#include <stdio.h> int main() { int x = 5; // Αρχική τιμή του x int *p; // Δηλώνουμε δείκτη p = &x; // Ο p δείχνει στον x printf("Αρχική τιμή του x: %d\n", x); *p = *p + 10; // Αυξάνουμε την τιμή του x κατά 10 μέσω του δείκτη printf("Νέα τιμή του x: %d\n", x); // Θα εκτυπώσει 15 return 0; }
Η εντολή *p = *p + 10; είναι το ίδιο με x = x + 10;
Αλλάζουμε τον x "έμμεσα" μέσω του δείκτη!
Εκφώνηση: Γράψε συνάρτηση void addOne(int *p) που:
Κάλεσέ την από το main με μία μεταβλητή x και εκτύπωσε το αποτέλεσμα.
#include <stdio.h> // Συνάρτηση που αυξάνει μια τιμή κατά 1 void addOne(int *p) { *p = *p + 1; // Αύξηση της τιμής που δείχνει ο p } int main() { int x = 10; printf("Πριν τη συνάρτηση: x = %d\n", x); addOne(&x); // Περνάμε τη διεύθυνση του x (όχι την τιμή!) printf("Μετά τη συνάρτηση: x = %d\n", x); return 0; }
Εδώ φαίνεται η χρήση δεικτών για να αλλάζουμε τιμές μέσα σε συνάρτηση!
Χωρίς δείκτες, η συνάρτηση θα άλλαζε μόνο ένα αντίγραφο του x.
Εκφώνηση: Γράψε συνάρτηση:
void swap(int *a, int *b);
που ανταλλάσσει τις τιμές δύο ακεραίων. Στο main:
x και yswap(&x, &y)#include <stdio.h> // Συνάρτηση που ανταλλάσσει δύο τιμές void swap(int *a, int *b) { int temp; // Προσωρινή μεταβλητή temp = *a; // Αποθηκεύουμε την τιμή που δείχνει το a *a = *b; // Βάζουμε στο a την τιμή του b *b = temp; // Βάζουμε στο b την παλιά τιμή του a } int main() { int x = 3; int y = 7; printf("Πριν το swap: x = %d, y = %d\n", x, y); swap(&x, &y); // Περνάμε τις διευθύνσεις των x και y printf("Μετά το swap: x = %d, y = %d\n", x, y); return 0; }
Αποτέλεσμα: Οι τιμές ανταλλάχτηκαν!
Εκφώνηση: Να γραφτεί πρόγραμμα που:
int arr[5] = {1, 2, 3, 4, 5};p που δείχνει στην αρχή του πίνακα(Χωρίς χρήση του arr[i] μέσα στη printf.)
#include <stdio.h> int main() { int arr[5] = {1, 2, 3, 4, 5}; // Δήλωση και αρχικοποίηση πίνακα int *p; // Δήλωση δείκτη int i; // Μετρητής για το loop p = arr; // Ο p δείχνει στο πρώτο στοιχείο (ίδιο με p = &arr[0]) printf("Στοιχεία του πίνακα:\n"); // Εκτύπωση με pointer arithmetic for (i = 0; i < 5; i++) { printf("Στοιχείο %d: %d\n", i, *(p + i)); // *(p + i) = arr[i] } return 0; }
p δείχνει στο arr[0]p + 1 δείχνει στο arr[1]p + 2 δείχνει στο arr[2]*(p + i) είναι το ίδιο με arr[i]| Έννοια | Σύνταξη | Περιγραφή |
|---|---|---|
| Δήλωση | int *p; |
Δείκτης σε ακέραιο |
| Address-of | p = &x; |
Ο p παίρνει τη διεύθυνση του x |
| Dereference | *p |
Η τιμή στη διεύθυνση που δείχνει ο p |
| Τροποποίηση | *p = 20; |
Αλλαγή τιμής μέσω δείκτη |
| Pointer Arithmetic | *(p + i) |
Πρόσβαση σε πίνακες |
| Call by Reference | func(&x) |
Πέρασμα διεύθυνσης σε συνάρτηση |
int *p; → δήλωση (p είναι δείκτης)*p = 10; → χρήση (πρόσβαση στην τιμή)Οι δείκτες είναι το "κλειδί" για προχωρημένο προγραμματισμό σε C:
Στα επόμενα μαθήματα: Θα δούμε πώς χρησιμοποιούνται οι δείκτες για δυναμική εκχώρηση μνήμης (malloc, free) και για τη δημιουργία πολύπλοκων δομών!