Κεφάλαιο 1: Δείκτες Ι & Αναδρομή
Προγραμματισμός στη C II — Δήλωση, Απόδοση Τιμής, Χρήση & Αναδρομικές Συναρτήσεις

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(int) επιστρέφει τύπο size_t, όχι int.

📌 Γιατί δεν γράφουμε %d;

Γιατί το %d είναι για int. Αν γράψεις:

printf("%d", sizeof(int)); // ⚠️ Warning! sizeof → size_t, αλλά %d → περιμένει int

Μπορεί να πάρεις warning, γιατί η sizeof(int) επιστρέφει size_t, ενώ το %d περιμένει int.

✅ Σωστός τρόπος:
printf("%zu\n", sizeof(int)); // ✅ Σωστό! %zu = unsigned decimal για size_t

Ανάλυση του %zu:

✅ Πώς ελέγχεις τα μεγέθη μόνος σου; Με τον τελεστή sizeof:

#include <stdio.h> int main() { // Εμφάνιση μεγέθους κάθε τύπου σε bytes printf("char: %zu bytes\n", sizeof(char)); // Πάντα 1 byte printf("short: %zu bytes\n", sizeof(short)); // Συνήθως 2 bytes printf("int: %zu bytes\n", sizeof(int)); // Συνήθως 4 bytes printf("long: %zu bytes\n", sizeof(long)); // 4 ή 8 bytes printf("float: %zu bytes\n", sizeof(float)); // Συνήθως 4 bytes printf("double: %zu bytes\n", sizeof(double)); // Συνήθως 8 bytes printf("int*: %zu bytes\n", sizeof(int*)); // 4 (32-bit) ή 8 (64-bit) return 0; }
char: 1 bytes short: 2 bytes int: 4 bytes long: 8 bytes float: 4 bytes double: 8 bytes int*: 8 bytes
⚠️ Σημαντικό: Η έξοδος μπορεί να διαφέρει στο δικό σας μηχάνημα! Χρησιμοποιήστε πάντα sizeof αν χρειάζεστε το ακριβές μέγεθος, αντί να υποθέτετε.

1.1.3 Τελεστής Διεύθυνσης (&)

Ο τελεστής & (address-of operator) μας δίνει τη διεύθυνση μνήμης μιας μεταβλητής. Τη διεύθυνση τη βλέπουμε συνήθως ως δεκαεξαδικό αριθμό, χρησιμοποιώντας τον προσδιοριστή %p στην printf().

#include <stdio.h> int main() { int num = 42; float price = 19.99; char letter = 'A'; printf("Μεταβλητή num:\n"); printf(" Τιμή: %d\n", num); printf(" Διεύθυνση: %p\n", &num); printf(" Μέγεθος: %zu bytes\n\n", sizeof(num)); printf("Μεταβλητή price:\n"); printf(" Τιμή: %.2f\n", price); printf(" Διεύθυνση: %p\n", &price); printf(" Μέγεθος: %zu bytes\n\n", sizeof(price)); printf("Μεταβλητή letter:\n"); printf(" Τιμή: %c\n", letter); printf(" Διεύθυνση: %p\n", &letter); printf(" Μέγεθος: %zu bytes\n", sizeof(letter)); return 0; }
Μεταβλητή num: Τιμή: 42 Διεύθυνση: 0x7fff5fbff6ac Μέγεθος: 4 bytes Μεταβλητή price: Τιμή: 19.99 Διεύθυνση: 0x7fff5fbff6a8 Μέγεθος: 4 bytes Μεταβλητή letter: Τιμή: A Διεύθυνση: 0x7fff5fbff6a7 Μέγεθος: 1 bytes
📊 Διάγραμμα Μνήμης — Πώς αποθηκεύονται οι μεταβλητές
Διεύθυνση Περιεχόμενο Μεταβλητή Μέγεθος
0x7fff...a7 'A' letter 1 byte
0x7fff...a8 19.99 price 4 bytes
0x7fff...ac 42 num 4 bytes

1.2 Δήλωση Δείκτη

Δείκτης (pointer) είναι μια μεταβλητή στην οποία αποθηκεύεται η διεύθυνση μνήμης μιας άλλης μεταβλητής. Δηλώνεται με τον τελεστή * πριν το όνομα:

int *ptr; // Δείκτης σε int double *ptr2; // Δείκτης σε double char *ptr3; // Δείκτης σε char
📌 Σημαντικό:
  • Ο τύπος δεδομένων αναφέρεται στον τύπο της μεταβλητής που δείχνει ο δείκτης.
  • Δεσμεύονται πάντα 4 bytes (ή 8 σε 64-bit σύστημα) στη δήλωση ενός δείκτη, ανεξαρτήτως τύπου.
  • Μπορούν να δηλωθούν μαζί με άλλες μεταβλητές ή μόνοι τους.
// Δήλωση δείκτη μαζί με μεταβλητή int a, *ptr; // Δήλωση δείκτη μόνος του int *ptr; // ⚠️ Προσοχή! Μόνο η p1 είναι δείκτης, η p2 είναι int int *p1, p2; // ✅ Και οι δύο είναι δείκτες int *p1, *p2;

1.3 Απόδοση Τιμής σε Δείκτη

Η μεταβλητή και ο δείκτης πρέπει να είναι ίδιου τύπου (π.χ. και τα δύο int). Χρησιμοποιούμε τον τελεστή & για να πάρουμε τη διεύθυνση μιας μεταβλητής και να την αναθέσουμε στον δείκτη.

#include <stdio.h> int main() { int num = 100; int *ptr; // Δήλωση δείκτη σε int ptr = &num; // Ανάθεση διεύθυνσης του num στον ptr printf("Τιμή του num: %d\n", num); printf("Διεύθυνση του num (&num): %p\n", &num); printf("Τιμή του ptr (= διεύθυνση): %p\n", ptr); printf("Διεύθυνση του ptr (&ptr): %p\n", &ptr); printf("Τιμή που δείχνει ο ptr (*ptr): %d\n", *ptr); return 0; }
Τιμή του num: 100 Διεύθυνση του num (&num): 0x7fff5000 Τιμή του ptr (= διεύθυνση): 0x7fff5000 Διεύθυνση του ptr (&ptr): 0x7fff6000 Τιμή που δείχνει ο ptr (*ptr): 100
📊 Πώς λειτουργεί ο Δείκτης στη Μνήμη
ΜΕΤΑΒΛΗΤΗ
num
100
📍 Διεύθυνση: 5000
◄━━━
ΔΕΙΚΤΗΣ
ptr
5000
📍 Διεύθυνση: 6000

Ο ptr αποθηκεύει τη διεύθυνση 5000 → «δείχνει» στη μεταβλητή num


1.4 Η Τιμή NULL

Όταν δηλώνεται ένας δείκτης χωρίς αρχικοποίηση, περιέχει μια τυχαία τιμή που αντιστοιχεί σε κάποια (άγνωστη) θέση μνήμης. Αυτό είναι επικίνδυνο!

Αν θέλουμε να δηλώσουμε πως ένας δείκτης δε δείχνει πουθενά, του αναθέτουμε την τιμή NULL, που ισούται με το 0.

#include <stdio.h> int main() { int *ptr = NULL; // Ο δείκτης δε δείχνει πουθενά // Έλεγχος πριν τη χρήση if (ptr != NULL) { printf("Ο δείκτης δείχνει: %d\n", *ptr); } else { printf("Ο δείκτης δε δείχνει πουθενά!\n"); } // Τώρα αναθέτουμε μια διεύθυνση int a = 42; ptr = &a; if (ptr != NULL) { printf("Ο δείκτης δείχνει: %d\n", *ptr); } return 0; }
Ο δείκτης δε δείχνει πουθενά! Ο δείκτης δείχνει: 42
⚠️ Καλή πρακτική: Πάντα αρχικοποιήστε τους δείκτες — είτε με τη διεύθυνση μιας μεταβλητής, είτε με NULL. Ελέγχετε αν ο δείκτης είναι NULL πριν τον χρησιμοποιήσετε!

1.5 Χρήση Δείκτη (Dereference)

Για να αποκτήσουμε πρόσβαση στο περιεχόμενο της θέσης μνήμης που δείχνει ένας δείκτης, χρησιμοποιούμε τον τελεστή * πριν το όνομα του δείκτη. Αυτή η διαδικασία ονομάζεται dereference (αποαναφορά).

⚠️ Προσοχή: Πρέπει πάντα πριν να έχουμε αποδώσει στον δείκτη μια διεύθυνση μεταβλητής (ptr = &a;) πριν χρησιμοποιήσουμε *ptr.
#include <stdio.h> int main() { int a = 10, b = 20; int *p1, *p2; p1 = &a; // p1 δείχνει στην a p2 = &b; // p2 δείχνει στην b printf("Πριν την αλλαγή:\n"); printf("a = %d, b = %d\n\n", a, b); // Αλλαγή τιμών μέσω δεικτών *p1 = 50; // Αλλάζει την τιμή του a σε 50 *p2 = 60; // Αλλάζει την τιμή του b σε 60 printf("Μετά την αλλαγή:\n"); printf("a = %d, b = %d\n", a, b); return 0; }
Πριν την αλλαγή: a = 10, b = 20 Μετά την αλλαγή: a = 50, b = 60
📊 Αλλαγή Τιμών μέσω Δεικτών — Πριν και Μετά

ΠΡΙΝ την εκτέλεση *p1 = 50; και *p2 = 60;

ΜΕΤΑΒΛΗΤΗ
a
10
◄━ p1
ΜΕΤΑΒΛΗΤΗ
b
20
◄━ p2

ΜΕΤΑ — Οι τιμές αλλάχτηκαν μέσω δεικτών!

ΑΛΛΑΓΜΕΝΗ ✏️
a
50
◄━ p1
ΑΛΛΑΓΜΕΝΗ ✏️
b
60
◄━ p2
📌 Βασικά Σύμβολα Δεικτών:
Σύμβολο Ονομασία Λειτουργία Παράδειγμα
& Τελεστής Διεύθυνσης Δίνει τη διεύθυνση μεταβλητής &a → διεύθυνση του a
* (στη δήλωση) Δήλωση Δείκτη Δηλώνει μεταβλητή ως δείκτη int *ptr;
* (στη χρήση) Dereference Πρόσβαση στην τιμή που δείχνει *ptr → τιμή

1.6 Προσοχή στους Τελεστές * και &

Όταν χρησιμοποιούνται ταυτόχρονα, ο ένας αναιρεί τον άλλο. Δηλαδή:

#include <stdio.h> int main() { int a = 10; int *ptr = &a; // Και οι τρεις εμφανίζουν τη ΔΙΕΥΘΥΝΣΗ του a printf("&a = %p\n", &a); printf("ptr = %p\n", ptr); printf("&*ptr = %p\n\n", &*ptr); // Και οι τρεις εμφανίζουν την ΤΙΜΗ του a printf("a = %d\n", a); printf("*ptr = %d\n", *ptr); printf("*&a = %d\n", *&a); return 0; }
&a = 0x7fff5000 ptr = 0x7fff5000 &*ptr = 0x7fff5000 a = 10 *ptr = 10 *&a = 10

1.7 Συνοπτικός Πίνακας

Έστω ότι δηλώνουμε τα παρακάτω και η μεταβλητή a αποθηκεύεται στη διεύθυνση 5000, ενώ ο δείκτης ptr στη διεύθυνση 6000:

int a; int *ptr; a = 10; ptr = &a;
📊 Οπτική Αναπαράσταση
ΜΕΤΑΒΛΗΤΗ
a
10
📍 Διεύθυνση: 5000
◄━━━
ΔΕΙΚΤΗΣ
ptr
5000
📍 Διεύθυνση: 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)

#include <stdio.h> // Η συνάρτηση δέχεται δείκτες (pass by reference) void swap(int *x, int *y) { int temp = *x; // temp = τιμή που δείχνει ο x *x = *y; // βάζουμε στο x την τιμή του y // Τι σημαίνει *x; // *x σημαίνει: «πήγαινε στη διεύθυνση που κρατάει ο x // και διάβασε / γράψε την τιμή εκεί» *y = temp; // βάζουμε στο y την παλιά τιμή του x } int main() { int a = 5, b = 10; printf("Πριν: a = %d, b = %d\n", a, b); swap(&a, &b); // Περνάμε τις ΔΙΕΥΘΥΝΣΕΙΣ printf("Μετά: a = %d, b = %d\n", a, b); return 0; }
Πριν: a = 5, b = 10 Μετά: a = 10, b = 5
✅ Γιατί λειτουργεί; Επειδή περνάμε τις διευθύνσεις (&a, &b) και όχι αντίγραφα τιμών. Η συνάρτηση αλλάζει τα δεδομένα απευθείας στη μνήμη!

Pass by Value (Πέρασμα με Τιμή)

Η συνάρτηση παίρνει αντίγραφα των μεταβλητών. Ό,τι αλλαγές κάνει, γίνονται στα αντίγραφα — όχι στις αρχικές.

Παράδειγμα (ΔΕΝ δουλεύει swap):

#include <stdio.h> // ❌ Pass by value - παίρνει ΑΝΤΙΓΡΑΦΑ! void swap(int x, int y) { int temp = x; // temp = αντίγραφο του a x = y; // αλλάζει το αντίγραφο x, ΟΧΙ το a y = temp; // αλλάζει το αντίγραφο y, ΟΧΙ το b } int main() { int a = 5, b = 10; swap(a, b); // Περνάμε ΤΙΜΕΣ (αντίγραφα) printf("a = %d, b = %d\n", a, b); return 0; }
a = 5, b = 10
❗ Δεν άλλαξε τίποτα! Γιατί;
  • x παίρνει αντίγραφο του a
  • y παίρνει αντίγραφο του b

Η συνάρτηση αλλάζει τα x, y (αντίγραφα), όχι τα a, b (πρωτότυπα).

📊 Pass by Value vs Pass by Reference
Τρόπος Κλήση Η συνάρτηση παίρνει Αλλάζει τα αρχικά;
Pass by Value swap(a, b) Αντίγραφα τιμών ❌ ΟΧΙ
Pass by Reference swap(&a, &b) Διευθύνσεις μνήμης ✅ ΝΑΙ

Ένα τελευταίο παράδειγμα για πλήρη κατανόηση

❓ Αν γράψω στο main απευθείας:
#include <stdio.h> int main() { int a = 5, b = 10; int temp = a; // temp = 5 (το πραγματικό a) a = b; // a = 10 (αλλάζει το πραγματικό a) b = temp; // b = 5 (αλλάζει το πραγματικό b) printf("a=%d b=%d\n", a, b); return 0; }

👉 ΝΑΙ — εδώ θα αλλάξουν!

a=10 b=5
🧠 Γιατί εδώ δουλεύει;
  • Δουλεύεις κατευθείαν πάνω στα a και b
  • ΔΕΝ υπάρχουν αντίγραφα — δεν καλείς συνάρτηση
  • Όλα είναι στο ίδιο scope (main)
📌 Τι σημαίνει «scope» (εμβέλεια);

Scope = η «περιοχή» του κώδικα όπου μια μεταβλητή υπάρχει και είναι προσβάσιμη. Κάθε ζευγάρι αγκίστρων { } ορίζει ένα scope.

  • Μια μεταβλητή που δηλώνεται μέσα στη main()υπάρχει μόνο μέσα στη main
  • Μια μεταβλητή που δηλώνεται μέσα στη swap()υπάρχει μόνο μέσα στη swap
  • Η μία συνάρτηση δεν βλέπει τις μεταβλητές της άλλης

Γι' αυτό χρειαζόμαστε δείκτες: για να «δείξουμε» στη συνάρτηση πού βρίσκονται οι μεταβλητές στη μνήμη!

❗ Γιατί τότε ΔΕΝ δουλεύει μέσα στη swap;

Γιατί όταν γράφεις void swap(int x, int y) και καλείς swap(a, b), η C δημιουργεί:

// Τι κάνει η C «κρυφά» όταν καλείς swap(a, b): int x = a; // x = αντίγραφο του a (= 5) int y = b; // y = αντίγραφο του b (= 10) // Μέσα στη swap αλλάζουν τα x, y // ΟΧΙ τα a, b!
🎯 Η μεγάλη διαφορά — Scope vs Συνάρτηση
ΣΤΟ MAIN
a, b
Πραγματικές μεταβλητές
✅ Αλλάζουν!
ΣΤΗ SWAP (χωρίς *)
x, y
Φωτοτυπίες των a, b
❌ Πετιούνται!
📌 Μνημονικός κανόνας:
  • 👉 Ίδιο scope → εργάζεσαι στις πραγματικές μεταβλητές → αλλάζουν
  • 👉 Συνάρτηση χωρίς pointers → εργάζεσαι σε αντίγραφα → ΔΕΝ αλλάζουν
  • 👉 Συνάρτηση με pointers → στέλνεις τη διεύθυνση → αλλάζουν!
🔥 Μία πρόταση που τα συνοψίζει όλα:

«Αν θέλεις μια συνάρτηση να αλλάξει μια μεταβλητή, πρέπει να της δώσεις τη διεύθυνση — όχι την τιμή

1.9 Κοινά Λάθη με Δείκτες

❌ Λάθος 1: Μη αρχικοποιημένος δείκτης
// ❌ ΛΑΘΟΣ - Ο δείκτης δεν δείχνει κάπου έγκυρο! int *ptr; *ptr = 10; // Undefined behavior! Crash! // ✅ ΣΩΣΤΟ - Πρώτα ανάθεση διεύθυνσης int num; int *ptr = &num; *ptr = 10; // OK! Αλλάζει την τιμή του num
❌ Λάθος 2: Ασυμβατότητα τύπων
// ❌ ΛΑΘΟΣ - Διαφορετικοί τύποι! float x = 3.14; int *ptr = &x; // Warning! Ο ptr είναι int* αλλά x είναι float // ✅ ΣΩΣΤΟ - Ίδιοι τύποι float x = 3.14; float *ptr = &x; // OK!

1.10 Η Έννοια της Αναδρομής (Recursion)

Αναδρομή (recursion) είναι η τεχνική κατά την οποία μια συνάρτηση καλεί τον εαυτό της. Κάθε αναδρομική συνάρτηση πρέπει να έχει:

Ας το καταλάβουμε βήμα-βήμα, από το μηδέν.

Βήμα 1 — Τι πάμε να κάνουμε;

Θέλουμε μια συνάρτηση που ξανακαλεί τον εαυτό της. Αυτό είναι όλο.

Βήμα 2 — Ας γράψουμε την πιο απλή τέτοια συνάρτηση

void demo(int n) { printf("%d\n", n); // Τύπωσε τον αριθμό demo(n - 1); // Κάλεσε τον εαυτό σου με n-1 }

Και στο main:

demo(3); // Ξεκινάμε με 3

Τι γίνεται; Τυπώνει:

3 2 1 0 -1 -2 ...
❌ ΔΕΝ σταματά ποτέ! Γιατί; 👉 Επειδή δεν είπαμε πότε να σταματήσει. Δεν έχουμε βασική περίπτωση (base case)!

✋ Βήμα 3 — Βάζουμε φρένο!

Προσθέτουμε μία γραμμή:

if (n == 0) return; // ΣΤΟΠ! Αν φτάσαμε στο 0, σταμάτα!

Τελικός κώδικας:

#include <stdio.h> void demo(int n) { if (n == 0) return; // Βασική περίπτωση (base case): ΣΤΑΜΑΤΑ! printf("%d\n", n); // Τύπωσε τον αριθμό demo(n - 1); // Αναδρομική περίπτωση (recursive case) } int main() { demo(3); return 0; }

Τώρα τυπώνει:

3 2 1

Και τέλος. Σταματά μόνη της.

🎯 Στάση εδώ.

Αυτό που μόλις έκανες είναι ΑΝΑΔΡΟΜΗ.
Βασική περίπτωση: if (n == 0) return;
Αναδρομική περίπτωση: demo(n - 1);
⚠️ Χωρίς βασική περίπτωση → Ατέρμων αναδρομή → Stack Overflow!

1.10.1 Παράδειγμα: Παραγοντικό (n!)

Το παραγοντικό ορίζεται μαθηματικά ως: n! = n × (n-1) × (n-2) × ... × 1 και 0! = 1.

Αναδρομικά: n! = n × (n-1)!

#include <stdio.h> // Αναδρομική συνάρτηση παραγοντικού int factorial(int n) { // Βασική περίπτωση: 0! = 1 if (n == 0) { return 1; } // Αναδρομική περίπτωση: n! = n * (n-1)! return n * factorial(n - 1); } int main() { int num = 5; printf("%d! = %d\n", num, factorial(num)); return 0; }
5! = 120
📊 Πώς εκτελείται η αναδρομή — factorial(5)

Κάθε κλήση «στοιβάζεται» στη μνήμη (stack) μέχρι τη βασική περίπτωση, μετά «ξετυλίγεται» προς τα πίσω:

📞 factorial(5) → 5 × factorial(4) → 5 × 24 = 120
📞 factorial(4) → 4 × factorial(3) → 4 × 6 = 24
📞 factorial(3) → 3 × factorial(2) → 3 × 2 = 6
📞 factorial(2) → 2 × factorial(1) → 2 × 1 = 2
📞 factorial(1) → 1 × factorial(0) → 1 × 1 = 1
🛑 factorial(0) → return 1 (βασική περίπτωση!)

1.10.2 Παράδειγμα: Άθροισμα αριθμών 1 έως n

#include <stdio.h> // Αναδρομικό άθροισμα: 1 + 2 + ... + n int sum(int n) { // Βασική περίπτωση if (n == 1) { return 1; } // Αναδρομική περίπτωση: sum(n) = n + sum(n-1) return n + sum(n - 1); } int main() { int num = 10; printf("Άθροισμα 1 έως %d = %d\n", num, sum(num)); return 0; }
Άθροισμα 1 έως 10 = 55

1.10.3 Παράδειγμα: Δύναμη αριθμού (xn)

#include <stdio.h> // Αναδρομική δύναμη: x^n = x * x^(n-1) int power(int x, int n) { // Βασική περίπτωση: x^0 = 1 if (n == 0) { return 1; } // Αναδρομική περίπτωση return x * power(x, n - 1); } int main() { printf("2^8 = %d\n", power(2, 8)); printf("3^4 = %d\n", power(3, 4)); printf("5^0 = %d\n", power(5, 0)); return 0; }
2^8 = 256 3^4 = 81 5^0 = 1

1.10.4 Αναδρομή vs Επανάληψη

Χαρακτηριστικό Αναδρομή (Recursion) Επανάληψη (Loop)
Τρόπος Η συνάρτηση καλεί τον εαυτό της Χρήση for / while
Τερματισμός Βασική περίπτωση (base case) Συνθήκη βρόχου
Μνήμη Χρησιμοποιεί stack (κάθε κλήση) Σταθερή μνήμη
Αναγνωσιμότητα Πιο κομψή / κατανοητή Μπορεί να είναι πιο σύνθετη
Κίνδυνος Stack overflow (ατέρμων) Ατέρμων βρόχος
📌 Πότε χρησιμοποιούμε αναδρομή;
  • Όταν το πρόβλημα «σπάει» φυσικά σε μικρότερα ίδια υποπροβλήματα
  • Παραγοντικά, Fibonacci, δυνάμεις, αναζήτηση σε δέντρα
  • Όταν η αναδρομική λύση είναι πιο καθαρή και κατανοητή

1.11 Ασκήσεις

Ασκήσεις Δεικτών

📝 Άσκηση 1: Κατανόηση Δεικτών

Τι θα εμφανίσει το παρακάτω πρόγραμμα; Γράψτε τις απαντήσεις πριν το τρέξετε!

#include <stdio.h> int main() { int x = 5; int *p = &x; printf("x = %d\n", x); printf("*p = %d\n", *p); *p = 20; printf("x = %d\n", x); x = 30; printf("*p = %d\n", *p); return 0; }

📝 Άσκηση 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)
  • Πώς να γράφουμε αναδρομικές συναρτήσεις (παραγοντικό, άθροισμα, δύναμη)
  • Τη σύγκριση αναδρομής με επανάληψη

Στο επόμενο κεφάλαιο θα δούμε τη σχέση δεικτών με πίνακες, την αριθμητική δεικτών, και τους δείκτες σε συμβολοσειρές!