Πανεπιστήμιο Πελοποννήσου - Τμήμα Ψηφιακών Συστημάτων

Μάθημα 8: Δείκτες (Pointers)

Κατανόηση Διευθύνσεων Μνήμης και Έμμεση Πρόσβαση σε Δεδομένα

📋 Περιεχόμενα μαθήματος

Οι δείκτες (pointers) είναι ένα από τα πιο ισχυρά χαρακτηριστικά της C. Μας επιτρέπουν να:

  • Έχουμε άμεση πρόσβαση στη μνήμη του υπολογιστή
  • Να περνάμε μεταβλητές σε συναρτήσεις και να τις αλλάζουμε (call by reference)
  • Να δουλεύουμε αποδοτικά με πίνακες και strings
  • Να δημιουργούμε πολύπλοκες δομές δεδομένων (linked lists, trees, κλπ.)

🧠 1. Βασική Ιδέα

Πώς αποθηκεύονται τα δεδομένα στη μνήμη;

Στη γλώσσα C, κάθε μεταβλητή αποθηκεύεται κάπου στη μνήμη του υπολογιστή. Αυτή η θέση έχει μια διεύθυνση (address).

Σκεφτείτε τη μνήμη σαν ένα μεγάλο κτίριο με δωμάτια:

  • Κάθε δωμάτιο έχει έναν μοναδικό αριθμό (διεύθυνση)
  • Μέσα σε κάθε δωμάτιο αποθηκεύεται μία τιμή
  • Ένας δείκτης είναι σαν ένα σημείωμα που γράφει: "Πήγαινε στο δωμάτιο Νο 1000"

➤ Τι είναι δείκτης;

Ένας δείκτης είναι μια μεταβλητή που αποθηκεύει τη διεύθυνση μιας άλλης μεταβλητής.

🔎 Σκεφτείτε τους δείκτες ως "δείκτες" που δείχνουν πού βρίσκεται μια πληροφορία.

📊 Οπτικοποίηση Μνήμης

Διεύθυνση: 0x1000
10
int x = 10;
👈
Διεύθυνση: 0x2000
0x1000
int *p = &x;

Ο δείκτης p (στη διεύθυνση 0x2000) δείχνει στον x (στη διεύθυνση 0x1000)

🧾 2. Δήλωση Δείκτη

Για να δηλώσουμε έναν δείκτη χρησιμοποιούμε το σύμβολο *.

int *p;     // p είναι δείκτης σε ακέραιο
float *f;   // f είναι δείκτης σε δεκαδικό
char *c;    // c είναι δείκτης σε χαρακτήρα

📝 ΣΗΜΑΝΤΙΚΟ!

Το * στη δήλωση δεν σημαίνει πολλαπλασιασμό — σημαίνει "δείκτης σε".

  • int *p; διαβάζεται: "Ο p είναι δείκτης σε int"
  • float *f; διαβάζεται: "Ο f είναι δείκτης σε float"
Δήλωση Τύπος Μεταβλητής Τι Αποθηκεύει
int x; Ακέραιος Μια ακέραια τιμή (π.χ. 10)
int *p; Δείκτης σε ακέραιο Διεύθυνση μνήμης όπου βρίσκεται ένας int

📥 3. Απόκτηση Διεύθυνσης με &

Ο τελεστής & (address-of operator) χρησιμοποιείται για να πάρουμε τη διεύθυνση μιας μεταβλητής.

int x = 5;    // Δηλώνουμε έναν ακέραιο με τιμή 5
int *p;       // Δηλώνουμε έναν δείκτη σε int

p = &x;        // Ο p τώρα δείχνει στον x (παίρνει τη διεύθυνση του x)

📌 Εξήγηση

Αυτό σημαίνει: Ο p δεν έχει την τιμή 5. Έχει τη διεύθυνση όπου βρίσκεται ο x στη μνήμη.

Βήμα-βήμα: int x = 5; int *p; p = &x;

Βήμα 1: int x = 5;

0x1000
5
x

Δημιουργείται ο x με τιμή 5 στη διεύθυνση 0x1000

Βήμα 2: int *p;

0x1000
5
x
0x2000
???
μη αρχικοποιημένος δείκτης

Δημιουργείται ο δείκτης p (δεν δείχνει ακόμα πουθενά)

Βήμα 3: p = &x;

0x1000
5
x
👈
0x2000
0x1000
p

Ο p αποθηκεύει τη διεύθυνση 0x1000 και δείχνει στον x!

📤 4. Ανάγνωση Τιμής μέσω Δείκτη (Dereferencing)

Για να πάρουμε την τιμή στη διεύθυνση που δείχνει ο δείκτης, χρησιμοποιούμε το * (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)

💻 5. Παράδειγμα Προγράμματος

Πλήρες Πρόγραμμα με Δείκτες

#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

Το %p χρησιμοποιείται για την εκτύπωση διευθύνσεων μνήμης σε δεκαεξαδική μορφή.

📤 Έξοδος Προγράμματος:

Τιμή του x: 10
Διεύθυνση του x: 0x7ffd5b3e4a1c
Διεύθυνση που αποθηκεύει ο p: 0x7ffd5b3e4a1c
Τιμή μέσω δείκτη p: 10

🔍 Παρατηρήσεις

  • Η διεύθυνση του x και η τιμή του p είναι ίδιες (ο p δείχνει στον x)
  • Η τιμή του x και η τιμή του *p είναι ίδιες (διαβάζουμε το ίδιο μέρος της μνήμης)
  • Οι διευθύνσεις θα είναι διαφορετικές κάθε φορά που τρέχει το πρόγραμμα

🔄 6. Τροποποίηση Τιμής μέσω Δείκτη

Αφού ο δείκτης δείχνει στον 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 = 20; (x έχει τιμή 10)

0x1000
10
x
👈
0x2000
0x1000
p

Μετά την εντολή *p = 20; (x άλλαξε σε 20)

0x1000
20
x (άλλαξε!)
👈
0x2000
0x1000
p

Μέσω του *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;
}
📤 Έξοδος:

Αρχική τιμή x: 10
Μετά το *p = 20, x = 20
Μετά το *p = *p + 5, x = 25

❗ 7. Δείκτες και Αρχικοποίηση

❌ ΛΑΘΟΣ: Δείκτης χωρίς διεύθυνση

int *p;      // Ο p δεν δείχνει πουθενά (wild pointer)
*p = 10;     // ⚠️ ΣΦΑΛΜΑ! Δεν ξέρουμε πού γράφουμε!

Αυτό είναι επικίνδυνο! Γράφουμε σε τυχαία θέση μνήμης!

✔ ΣΩΣΤΟ: Αρχικοποίηση πριν τη χρήση

Τρόπος 1: Αρχικοποίηση με υπάρχουσα μεταβλητή

int x = 5;
int *p = &x;   // ✅ Ο p δείχνει στον x
Κατάσταση Δείκτη Περιγραφή Ασφαλής;
Μη αρχικοποιημένος int *p; ❌ Επικίνδυνο
Δείχνει σε μεταβλητή int *p = &x; ✅ Ασφαλές

🧱 8. Δείκτες και Πίνακες

Σημαντική Σχέση: Πίνακες ≈ Δείκτες

Στη C, το όνομα ενός πίνακα είναι δείκτης στο πρώτο του στοιχείο!

int arr[3] = {1, 2, 3};
int *p = arr;  // ⚠️ Όχι &arr, γιατί arr είναι ήδη διεύθυνση!

// Το arr είναι το ίδιο με το &arr[0]

Μνήμη Πίνακα

0x1000
1
arr[0]
0x1004
2
arr[1]
0x1008
3
arr[2]

arr = 0x1000 (δείχνει στο πρώτο στοιχείο)

📖 Θεωρία: Τρόποι Πρόσβασης σε Στοιχεία Πίνακα

Πρόσβαση στο 2ο στοιχείο (τιμή 2)

🔸 Τρόπος 1: Κλασικά με πίνακα

arr[1]

Αυτό σημαίνει το στοιχείο με δείκτη 1 στον πίνακα → 2.

🔸 Τρόπος 2: Pointer arithmetic με το arr

*(arr + 1)
  • arr είναι η διεύθυνση του πρώτου στοιχείου
  • arr + 1 είναι η διεύθυνση του δεύτερου στοιχείου
  • *(arr + 1) σημαίνει διάβασε τη τιμή εκεί

🔸 Τρόπος 3: Pointer arithmetic με τον p

*(p + 1)

Αφού:

p = arr;

τότε p + 1 είναι η ίδια διεύθυνση με το arr + 1. Άρα και αυτό επιστρέφει 2.

📌 Πρόσβαση στο 3ο στοιχείο (τιμή 3)

Τρόπος Ισοδύναμο
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)

🔍 Πρόσβαση με Pointer Arithmetic

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 που:

  1. Ορίζει έναν πίνακα 5 ακεραίων με τις τιμές {4, 7, 1, 3, 9}.
  2. Δηλώνει έναν δείκτη p και τον αρχικοποιεί ώστε να δείχνει στο πρώτο στοιχείο του πίνακα.
  3. Εκτυπώνει όλα τα στοιχεία του πίνακα χρησιμοποιώντας αποκλειστικά τον δείκτη, με τη μορφή *(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
📤 Έξοδος Προγράμματος:

Στοιχεία πίνακα με χρήση δείκτη:
4 7 1 3 9

📨 9. Γιατί χρειαζόμαστε δείκτες σε συναρτήσεις;

Όταν περνάμε μια μεταβλητή σε μια συνάρτηση κανονικά (call by value), η συνάρτηση παίρνει αντίγραφο της τιμής. Αλλαγές στη συνάρτηση δεν επηρεάζουν την αρχική μεταβλητή.

Με δείκτες (call by reference), η συνάρτηση μπορεί να αλλάξει την πραγματική μεταβλητή!

Πέρασμα Παραμέτρων σε Συναρτήσεις

➤ Call by Value & Call by Reference

Όταν καλούμε μια συνάρτηση στη γλώσσα C, μπορούμε να της στείλουμε πληροφορίες. Αυτό ονομάζεται πέρασμα παραμέτρων. Υπάρχουν δύο βασικοί τρόποι με τους οποίους μπορεί να γίνει αυτό:

🔷 1. Πέρασμα κατά τιμή (Call by Value)

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

💬 Παρομοίωση: σαν να δίνεις σε κάποιον μια φωτοτυπία. Αν την τσαλακώσει, η αυθεντική παραμένει άθικτη.

🔶 2. Πέρασμα κατά αναφορά (Call by Reference)

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

💬 Παρομοίωση: σαν να δίνεις σε κάποιον το αυθεντικό έγγραφο. Αν το αλλάξει, αλλάζει το πραγματικό.

💡 Πώς γίνεται το Call by Reference στη C;

Η γλώσσα C δεν έχει μηχανισμό "call by reference" αυτόματα. Για να το πετύχουμε, χρησιμοποιούμε δείκτες (pointers). Με ένα δείκτη μπορούμε να περάσουμε στη συνάρτηση τη διεύθυνση μιας μεταβλητής, ώστε να μπορεί να την αλλάξει.

📌 Γιατί μας ενδιαφέρει αυτή η διαφορά;

Μέθοδος Τι δίνουμε στη συνάρτηση; Μπορεί να αλλάξει την αρχική τιμή;
Call by Value Αντίγραφο της τιμής ❌ Όχι
Call by Reference Τη διεύθυνση (με δείκτη) ✔ Ναι

🔎 Αυτό θα το χρησιμοποιήσουμε όταν θέλουμε η συνάρτηση να επιδράσει πραγματικά στα δεδομένα μας.

Πέρασμα Μεταβλητών σε Συναρτήσεις (Call by Reference)

❌ Παράδειγμα: Call by Value (χωρίς δείκτες)

#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;
}
📤 Έξοδος:

Πριν: x = 10
Μέσα στη συνάρτηση: x = 11
Μετά: x = 10 ← Δεν άλλαξε!

✅ Παράδειγμα: Call by Reference (με δείκτες)

#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;
}
📤 Έξοδος:

Πριν: x = 10
Μετά: x = 11 ← Άλλαξε!

Σύγκριση: Call by Value vs Call by Reference

Call by Value Call by Reference
void func(int x) {
    x = x + 1;
}

func(y);  // αντίγραφο
void func(int *p) {
    *p = *p + 1;
}

func(&y);  // διεύθυνση
Η συνάρτηση δουλεύει με αντίγραφο Η συνάρτηση δουλεύει με το πρωτότυπο
Δεν αλλάζει την αρχική μεταβλητή Αλλάζει την αρχική μεταβλητή

🔄 Κλασικό Παράδειγμα: Swap

Συνάρτηση που ανταλλάσσει τις τιμές δύο μεταβλητών:

#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;
}
📤 Έξοδος:

Πριν το swap: x = 3, y = 7
Μετά το swap: x = 7, y = 3

💡 10. Γιατί χρησιμοποιούμε δείκτες;

Λόγος Παράδειγμα Όφελος
Αποδοτική διαχείριση μνήμης Δυναμική εκχώρηση με malloc() Δημιουργία δομών μεταβλητού μεγέθους
Πέρασμα παραμέτρων κατά αναφορά Αλλαγή μεταβλητών μέσα σε συναρτήσεις Συναρτήσεις που αλλάζουν πολλές τιμές
Χειρισμός πινάκων και strings Επεξεργασία strings με char* Αποδοτική πρόσβαση σε μεγάλα δεδομένα
Χρήση δομών Linked lists, trees, stacks Δημιουργία σύνθετων δομών δεδομένων
Επιστροφή πολλών τιμών Συνάρτηση που επιστρέφει 2+ τιμές Ευελιξία στις συναρτήσεις

🎯 Παράδειγμα: Συνάρτηση που επιστρέφει 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;
}
📤 Έξοδος:

Άθροισμα: 8
Γινόμενο: 15

📝 11. Ασκήσεις Εξάσκησης με Λύσεις

Άσκηση 1

Βασική Χρήση Δεικτών

Εκφώνηση: Να γραφτεί πρόγραμμα που:

  1. Δηλώνει έναν ακέραιο x
  2. Δηλώνει έναν δείκτη p που δείχνει στον x
  3. Εκτυπώνει:
    • την τιμή του x
    • τη διεύθυνση του x
    • την τιμή του x μέσω του δείκτη (*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: 10
Διεύθυνση του x (&x): 0x7ffd5b3e4a1c
Διεύθυνση που αποθηκεύει ο p: 0x7ffd5b3e4a1c
Τιμή του x μέσω p (*p): 10
Άσκηση 2

Τροποποίηση Τιμής μέσω Δείκτη

Εκφώνηση: Να γραφτεί πρόγραμμα που:

  1. Δηλώνει έναν ακέραιο x = 5
  2. Δηλώνει έναν δείκτη p που δείχνει στον x
  3. Αυξάνει την τιμή του x κατά 10 μέσω του δείκτη
  4. Εκτυπώνει τη νέα τιμή του 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;
}
📤 Έξοδος:

Αρχική τιμή του x: 5
Νέα τιμή του x: 15

📝 Σχόλιο:

Η εντολή *p = *p + 10; είναι το ίδιο με x = x + 10;

Αλλάζουμε τον x "έμμεσα" μέσω του δείκτη!

Άσκηση 3

Συνάρτηση με Call by Reference

Εκφώνηση: Γράψε συνάρτηση void addOne(int *p) που:

  • Παίρνει ως όρισμα δείκτη σε int
  • Αυξάνει την τιμή της μεταβλητής που δείχνει ο δείκτης κατά 1

Κάλεσέ την από το 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 = 10
Μετά τη συνάρτηση: x = 11

🔑 Κλειδί:

Εδώ φαίνεται η χρήση δεικτών για να αλλάζουμε τιμές μέσα σε συνάρτηση!

Χωρίς δείκτες, η συνάρτηση θα άλλαζε μόνο ένα αντίγραφο του x.

Άσκηση 4

Συνάρτηση Swap

Εκφώνηση: Γράψε συνάρτηση:

void swap(int *a, int *b);

που ανταλλάσσει τις τιμές δύο ακεραίων. Στο main:

  • Δώσε τιμές σε δύο μεταβλητές x και y
  • Κάλεσε τη swap(&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;
}
📤 Έξοδος:

Πριν το swap: x = 3, y = 7
Μετά το swap: x = 7, y = 3

📝 Εξήγηση Αλγορίθμου Swap:

  1. Αποθηκεύουμε την τιμή του a σε προσωρινή μεταβλητή (temp = 3)
  2. Βάζουμε στο a την τιμή του b (a γίνεται 7)
  3. Βάζουμε στο b την παλιά τιμή του a που είχαμε στο temp (b γίνεται 3)

Αποτέλεσμα: Οι τιμές ανταλλάχτηκαν!

Άσκηση 5

Εκτύπωση Πίνακα με Δείκτη

Εκφώνηση: Να γραφτεί πρόγραμμα που:

  1. Δηλώνει έναν πίνακα int arr[5] = {1, 2, 3, 4, 5};
  2. Δηλώνει έναν δείκτη p που δείχνει στην αρχή του πίνακα
  3. Χρησιμοποιεί τον δείκτη για να εκτυπώσει όλα τα στοιχεία του πίνακα

(Χωρίς χρήση του 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;
}
📤 Έξοδος:

Στοιχεία του πίνακα:
Στοιχείο 0: 1
Στοιχείο 1: 2
Στοιχείο 2: 3
Στοιχείο 3: 4
Στοιχείο 4: 5

📝 Εξήγηση Pointer Arithmetic:

  • 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) Πέρασμα διεύθυνσης σε συνάρτηση

Συνηθισμένα λάθη που πρέπει να αποφύγετε

  1. Μη αρχικοποιημένοι δείκτες: Πάντα αρχικοποιείτε τους δείκτες πριν τη χρήση
  2. Dereference NULL: Μην κάνετε *p αν p = NULL
  3. Ξέχασμα του &: Όταν περνάτε μεταβλητή σε συνάρτηση για αλλαγή, χρειάζεται &
  4. Σύγχυση * στη δήλωση και στη χρήση:
    • int *p; → δήλωση (p είναι δείκτης)
    • *p = 10; → χρήση (πρόσβαση στην τιμή)

💡 Γιατί είναι σημαντικοί οι Δείκτες;

Οι δείκτες είναι το "κλειδί" για προχωρημένο προγραμματισμό σε C:

  • Επιτρέπουν αποδοτική διαχείριση μνήμης
  • Είναι απαραίτητοι για δυναμικές δομές δεδομένων
  • Κάνουν τις συναρτήσεις πιο ευέλικτες
  • Δίνουν άμεση πρόσβαση στη μνήμη του υπολογιστή

Στα επόμενα μαθήματα: Θα δούμε πώς χρησιμοποιούνται οι δείκτες για δυναμική εκχώρηση μνήμης (malloc, free) και για τη δημιουργία πολύπλοκων δομών!