1.1 Δείκτες και Μνήμη
Κάθε μεταβλητή στη μνήμη του υπολογιστή έχει μια μοναδική διεύθυνση. Αυτή η διεύθυνση καθορίζει που ακριβώς αποθηκεύεται η τιμή της μεταβλητής στη μνήμη.
Όταν δηλώνεται μια μεταβλητή, ο μεταγλωττιστής δεσμεύει τις απαραίτητες συνεχόμενες θέσεις μνήμης (οκτάδες - bytes). Η διεύθυνση της μεταβλητής είναι η διεύθυνση της πρώτης θέσης μνήμης.
int a = 10; δεσμεύει 4 bytes στη μνήμη. Η διεύθυνση &a είναι η διεύθυνση του πρώτου byte.
1.1.1 Μεγέθη Τύπων στη Μνήμη
Κάθε τύπος μεταβλητής στη C καταλαμβάνει διαφορετικό μέγεθος μνήμης, και μάλιστα αυτό μπορεί να αλλάζει ανάλογα με την αρχιτεκτονική (32-bit / 64-bit) και τον compiler.
Σε γενικές γραμμές (στις περισσότερες σύγχρονες πλατφόρμες):
| Τύπος | Συνήθες Μέγεθος |
|---|---|
char |
1 byte |
short |
2 bytes |
int |
4 bytes |
long |
4 ή 8 bytes |
long long |
8 bytes |
float |
4 bytes |
double |
8 bytes |
pointer (π.χ. int*) |
4 bytes (32-bit) / 8 bytes (64-bit) |
👉 Το char είναι πάντα 1 byte από το standard της C. Όλα τα άλλα έχουν ελάχιστα όρια, αλλά το ακριβές μέγεθος εξαρτάται από το σύστημα.
1.1.2 Τι είναι το size_t;
Το size_t είναι ένας ακέραιος τύπος χωρίς πρόσημο (unsigned) που χρησιμοποιείται για:
- Το αποτέλεσμα της
sizeof - Μεγέθη πινάκων
- Πλήθος bytes μνήμης
Για παράδειγμα, η έκφραση sizeof(int) επιστρέφει τύπο size_t, όχι int.
%d;
Γιατί το %d είναι για int. Αν γράψεις:
Μπορεί να πάρεις warning, γιατί η sizeof(int) επιστρέφει size_t, ενώ το %d περιμένει int.
Ανάλυση του %zu:
%z→ λέει «το επόμενο είναιsize_t»u→ unsigned decimal (χωρίς πρόσημο, δεκαδικό)
✅ Πώς ελέγχεις τα μεγέθη μόνος σου; Με τον τελεστή sizeof:
sizeof αν χρειάζεστε το ακριβές μέγεθος, αντί να υποθέτετε.
1.1.3 Τελεστής Διεύθυνσης (&)
Ο τελεστής & (address-of operator) μας δίνει τη διεύθυνση μνήμης μιας μεταβλητής. Τη διεύθυνση τη βλέπουμε συνήθως ως δεκαεξαδικό αριθμό, χρησιμοποιώντας τον προσδιοριστή %p στην printf().
| Διεύθυνση | Περιεχόμενο | Μεταβλητή | Μέγεθος |
|---|---|---|---|
| 0x7fff...a7 | 'A' | letter | 1 byte |
| 0x7fff...a8 | 19.99 | price | 4 bytes |
| 0x7fff...ac | 42 | num | 4 bytes |
1.2 Δήλωση Δείκτη
Δείκτης (pointer) είναι μια μεταβλητή στην οποία αποθηκεύεται η διεύθυνση μνήμης μιας άλλης μεταβλητής. Δηλώνεται με τον τελεστή * πριν το όνομα:
- Ο τύπος δεδομένων αναφέρεται στον τύπο της μεταβλητής που δείχνει ο δείκτης.
- Δεσμεύονται πάντα 4 bytes (ή 8 σε 64-bit σύστημα) στη δήλωση ενός δείκτη, ανεξαρτήτως τύπου.
- Μπορούν να δηλωθούν μαζί με άλλες μεταβλητές ή μόνοι τους.
1.3 Απόδοση Τιμής σε Δείκτη
Η μεταβλητή και ο δείκτης πρέπει να είναι ίδιου τύπου (π.χ. και τα δύο int). Χρησιμοποιούμε τον τελεστή & για να πάρουμε τη διεύθυνση μιας μεταβλητής και να την αναθέσουμε στον δείκτη.
Ο ptr αποθηκεύει τη διεύθυνση 5000 → «δείχνει» στη μεταβλητή num
1.4 Η Τιμή NULL
Όταν δηλώνεται ένας δείκτης χωρίς αρχικοποίηση, περιέχει μια τυχαία τιμή που αντιστοιχεί σε κάποια (άγνωστη) θέση μνήμης. Αυτό είναι επικίνδυνο!
Αν θέλουμε να δηλώσουμε πως ένας δείκτης δε δείχνει πουθενά, του αναθέτουμε την τιμή NULL, που ισούται με το 0.
NULL. Ελέγχετε αν ο δείκτης είναι NULL πριν τον χρησιμοποιήσετε!
1.5 Χρήση Δείκτη (Dereference)
Για να αποκτήσουμε πρόσβαση στο περιεχόμενο της θέσης μνήμης που δείχνει ένας δείκτης, χρησιμοποιούμε τον τελεστή * πριν το όνομα του δείκτη. Αυτή η διαδικασία ονομάζεται dereference (αποαναφορά).
ptr = &a;) πριν χρησιμοποιήσουμε *ptr.
ΠΡΙΝ την εκτέλεση *p1 = 50; και *p2 = 60;
ΜΕΤΑ — Οι τιμές αλλάχτηκαν μέσω δεικτών!
| Σύμβολο | Ονομασία | Λειτουργία | Παράδειγμα |
|---|---|---|---|
& |
Τελεστής Διεύθυνσης | Δίνει τη διεύθυνση μεταβλητής | &a → διεύθυνση του a |
* (στη δήλωση) |
Δήλωση Δείκτη | Δηλώνει μεταβλητή ως δείκτη | int *ptr; |
* (στη χρήση) |
Dereference | Πρόσβαση στην τιμή που δείχνει | *ptr → τιμή |
1.6 Προσοχή στους Τελεστές * και &
Όταν χρησιμοποιούνται ταυτόχρονα, ο ένας αναιρεί τον άλλο. Δηλαδή:
*&a→ η τιμή στη διεύθυνση του a → ίδιο μεa&*ptr→ η διεύθυνση της τιμής που δείχνει ο ptr → ίδιο μεptr
1.7 Συνοπτικός Πίνακας
Έστω ότι δηλώνουμε τα παρακάτω και η μεταβλητή a αποθηκεύεται στη διεύθυνση 5000, ενώ ο δείκτης ptr στη διεύθυνση 6000:
| Έκφραση | Τι Δίνει | Τιμή | Εξήγηση |
|---|---|---|---|
a |
Τιμή μεταβλητής | 10 | Η τιμή που αποθηκεύσαμε |
&a |
Διεύθυνση μεταβλητής | 5000 | Πού βρίσκεται η a στη μνήμη |
ptr |
Τιμή δείκτη (= διεύθυνση) | 5000 | Η τιμή του ptr = η διεύθυνση του a |
&ptr |
Διεύθυνση δείκτη | 6000 | Πού βρίσκεται ο ptr στη μνήμη |
*ptr |
Τιμή μέσω δείκτη | 10 | Η τιμή στη διεύθυνση που δείχνει ο ptr |
1.8 Δείκτες και Συναρτήσεις (Pass by Reference)
Οι δείκτες μας επιτρέπουν να περνάμε μεταβλητές σε συναρτήσεις με αναφορά (pass by reference), ώστε η συνάρτηση να μπορεί να αλλάξει τις πραγματικές τιμές τους.
Παράδειγμα: Εναλλαγή δύο αριθμών (swap)
&a, &b) και όχι αντίγραφα τιμών. Η συνάρτηση αλλάζει τα δεδομένα απευθείας στη μνήμη!
Pass by Value (Πέρασμα με Τιμή)
Η συνάρτηση παίρνει αντίγραφα των μεταβλητών. Ό,τι αλλαγές κάνει, γίνονται στα αντίγραφα — όχι στις αρχικές.
Παράδειγμα (ΔΕΝ δουλεύει swap):
xπαίρνει αντίγραφο τουayπαίρνει αντίγραφο τουb
Η συνάρτηση αλλάζει τα x, y (αντίγραφα), όχι τα a, b (πρωτότυπα).
| Τρόπος | Κλήση | Η συνάρτηση παίρνει | Αλλάζει τα αρχικά; |
|---|---|---|---|
| Pass by Value | swap(a, b) | Αντίγραφα τιμών | ❌ ΟΧΙ |
| Pass by Reference | swap(&a, &b) | Διευθύνσεις μνήμης | ✅ ΝΑΙ |
Ένα τελευταίο παράδειγμα για πλήρη κατανόηση
main απευθείας:
👉 ΝΑΙ — εδώ θα αλλάξουν!
- Δουλεύεις κατευθείαν πάνω στα
aκαιb - ΔΕΝ υπάρχουν αντίγραφα — δεν καλείς συνάρτηση
- Όλα είναι στο ίδιο scope (
main)
Scope = η «περιοχή» του κώδικα όπου μια μεταβλητή υπάρχει και είναι προσβάσιμη. Κάθε ζευγάρι αγκίστρων { } ορίζει ένα scope.
- Μια μεταβλητή που δηλώνεται μέσα στη
main()→ υπάρχει μόνο μέσα στη main - Μια μεταβλητή που δηλώνεται μέσα στη
swap()→ υπάρχει μόνο μέσα στη swap - Η μία συνάρτηση δεν βλέπει τις μεταβλητές της άλλης
Γι' αυτό χρειαζόμαστε δείκτες: για να «δείξουμε» στη συνάρτηση πού βρίσκονται οι μεταβλητές στη μνήμη!
Γιατί όταν γράφεις void swap(int x, int y) και καλείς swap(a, b), η C δημιουργεί:
- 👉 Ίδιο scope → εργάζεσαι στις πραγματικές μεταβλητές → αλλάζουν
- 👉 Συνάρτηση χωρίς pointers → εργάζεσαι σε αντίγραφα → ΔΕΝ αλλάζουν
- 👉 Συνάρτηση με pointers → στέλνεις τη διεύθυνση → αλλάζουν!
«Αν θέλεις μια συνάρτηση να αλλάξει μια μεταβλητή, πρέπει να της δώσεις τη διεύθυνση — όχι την τιμή.»
1.9 Κοινά Λάθη με Δείκτες
1.10 Η Έννοια της Αναδρομής (Recursion)
Αναδρομή (recursion) είναι η τεχνική κατά την οποία μια συνάρτηση καλεί τον εαυτό της. Κάθε αναδρομική συνάρτηση πρέπει να έχει:
- Αναδρομική περίπτωση (recursive case): Η κλήση του εαυτού της με «μικρότερο» πρόβλημα.
- Βασική περίπτωση (base case): Η συνθήκη τερματισμού — πότε σταματάει η αναδρομή.
Ας το καταλάβουμε βήμα-βήμα, από το μηδέν.
Βήμα 1 — Τι πάμε να κάνουμε;
Θέλουμε μια συνάρτηση που ξανακαλεί τον εαυτό της. Αυτό είναι όλο.
Βήμα 2 — Ας γράψουμε την πιο απλή τέτοια συνάρτηση
Και στο main:
Τι γίνεται; Τυπώνει:
✋ Βήμα 3 — Βάζουμε φρένο!
Προσθέτουμε μία γραμμή:
Τελικός κώδικας:
Τώρα τυπώνει:
Και τέλος. Σταματά μόνη της.
Αυτό που μόλις έκανες είναι ΑΝΑΔΡΟΜΗ.
Βασική περίπτωση:
if (n == 0) return;Αναδρομική περίπτωση:
demo(n - 1);
1.10.1 Παράδειγμα: Παραγοντικό (n!)
Το παραγοντικό ορίζεται μαθηματικά ως: n! = n × (n-1) × (n-2) × ... × 1 και 0! = 1.
Αναδρομικά: n! = n × (n-1)!
Κάθε κλήση «στοιβάζεται» στη μνήμη (stack) μέχρι τη βασική περίπτωση, μετά «ξετυλίγεται» προς τα πίσω:
1.10.2 Παράδειγμα: Άθροισμα αριθμών 1 έως n
1.10.3 Παράδειγμα: Δύναμη αριθμού (xn)
1.10.4 Αναδρομή vs Επανάληψη
| Χαρακτηριστικό | Αναδρομή (Recursion) | Επανάληψη (Loop) |
|---|---|---|
| Τρόπος | Η συνάρτηση καλεί τον εαυτό της | Χρήση for / while |
| Τερματισμός | Βασική περίπτωση (base case) | Συνθήκη βρόχου |
| Μνήμη | Χρησιμοποιεί stack (κάθε κλήση) | Σταθερή μνήμη |
| Αναγνωσιμότητα | Πιο κομψή / κατανοητή | Μπορεί να είναι πιο σύνθετη |
| Κίνδυνος | Stack overflow (ατέρμων) | Ατέρμων βρόχος |
- Όταν το πρόβλημα «σπάει» φυσικά σε μικρότερα ίδια υποπροβλήματα
- Παραγοντικά, Fibonacci, δυνάμεις, αναζήτηση σε δέντρα
- Όταν η αναδρομική λύση είναι πιο καθαρή και κατανοητή
1.11 Ασκήσεις
Ασκήσεις Δεικτών
📝 Άσκηση 1: Κατανόηση Δεικτών
Τι θα εμφανίσει το παρακάτω πρόγραμμα; Γράψτε τις απαντήσεις πριν το τρέξετε!
📝 Άσκηση 2: Συνάρτηση με Δείκτη
Γράψτε μια συνάρτηση void tripleValue(int *p) που δέχεται δείκτη σε ακέραιο και τριπλασιάζει την τιμή του. Δοκιμάστε τη στη main().
📝 Άσκηση 3: Μέγιστο και Ελάχιστο
Γράψτε μια συνάρτηση void findMinMax(int a, int b, int *min, int *max) που δέχεται δύο αριθμούς και γεμίζει τις μεταβλητές min και max μέσω δεικτών.
Ασκήσεις Αναδρομής
📝 Άσκηση 4: Fibonacci
Γράψτε αναδρομική συνάρτηση int fibonacci(int n) που υπολογίζει τον n-οστό αριθμό Fibonacci. Υπενθύμιση: F(0)=0, F(1)=1, F(n) = F(n-1) + F(n-2).
📝 Άσκηση 5: Αντίστροφη Εμφάνιση
Γράψτε αναδρομική συνάρτηση void printReverse(int n) που εμφανίζει τα ψηφία ενός ακεραίου αντίστροφα. Π.χ. για n=1234 εμφανίζει: 4 3 2 1.
📝 Άσκηση 6: Αναδρομικό Άθροισμα Ψηφίων
Γράψτε αναδρομική συνάρτηση int digitSum(int n) που υπολογίζει το άθροισμα των ψηφίων ενός ακεραίου. Π.χ. digitSum(1234) = 1+2+3+4 = 10.
1.12 Σύνοψη Κεφαλαίου
- Τι είναι οι διευθύνσεις μνήμης και πώς λειτουργούν
- Πώς να δηλώνουμε δείκτες (
int *ptr;) - Πώς να αναθέτουμε διευθύνσεις (
ptr = &a;) - Πώς να χρησιμοποιούμε δείκτες (
*ptr) για πρόσβαση σε τιμές - Τη σημασία του NULL και τον έλεγχο πριν τη χρήση
- Τη σχέση μεταξύ
*και&(ο ένας αναιρεί τον άλλο) - Πώς να περνάμε μεταβλητές με αναφορά σε συναρτήσεις
- Τα κοινά λάθη με δείκτες και πώς να τα αποφύγουμε
- Τι είναι η αναδρομή (base case + recursive case)
- Πώς να γράφουμε αναδρομικές συναρτήσεις (παραγοντικό, άθροισμα, δύναμη)
- Τη σύγκριση αναδρομής με επανάληψη
Στο επόμενο κεφάλαιο θα δούμε τη σχέση δεικτών με πίνακες, την αριθμητική δεικτών, και τους δείκτες σε συμβολοσειρές!