Skip to content

7. Δείκτες

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

Προαπαιτούμενη γνώση Τύποι δεδομένων, είσοδος/έξοδος, δομές επιλογής και επανάληψης, συναρτήσεις, πίνακες, δομές.

7.1 Εμφάνιση διευθύνσεων μνήμης μεταβλητών

Κάθε μεταβλητή στη γλώσσα C βρίσκεται σε συγκεκριμένη θέση στη μνήμη του υπολογιστή. Αυτή η θέση είναι ένας ακέραιος αριθμός αυστηρά θετικός. Στην ουσία στον προγραμματισμό κάθε αναφορά σε μεταβλητή πάντοτε μεταφράζεται σε διευθύνσεις στη μνήμη. Η θέση αυτή ονομάζεται διεύθυνση και υπάρχει δυνατότητα να εντοπιστεί με τη χρήση του τελεστή & όπως παρουσιάζεται και στον κώδικα 7.1. Σε αυτό το παράδειγμα στην πρώτη γραμμή εκτύπωσης εμφανίζονται οι τιμές των μεταβλητών x και y (οι τιμές 100 και 200 αντίστοιχα) και στη δεύτερη γραμμή οι θέσεις τους στη μνήμη του υπολογιστή. Παρατηρήστε ότι για να εκτυπωθούν οι διευθύνσεις μνήμης χρησιμοποιήθηκε ο προσδιοριστής διαμόρφωσης %p και οι μεταβλητές που περιέχουν διευθύνσεις μνήμης μετατρέπονται σε void* καθώς περνούν ως ορίσματα στην printf().

Κώδικας 7.1: ch7_p1.c - εμφάνιση διευθύνσεων μνήμης που καταλαμβάνουν δύο μεταβλητές.
1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(void) {
  int x = 100;
  double y = 200.0;
  printf("x=%d y=%lf\n", x, y);
  printf("&x=%p, &y=%p \n", (void *)&x, (void *)&y);
  return 0;
}

Στη συνέχεια παρουσιάζονται δύο διαφορετικές εκτελέσεις του ίδιου προγράμματος.

x=100 y=200.000000
&x=0x16bc02e28 , &y=0x16bc02e20
x=100 y=200.000000
&x=0x16f7ab288 , &y=0x16f7ab2802

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

7.2 Μεταβλητές δείκτη

Οι διευθύνσεις μνήμης μπορούν να εκχωρηθούν σε μεταβλητές ειδικού σκοπού που ονομάζονται δείκτες. Οι μεταβλητές αυτές δηλώνονται ως TYPE *VARIABLE, όπου TYPE ένας οποιοσδήποτε τύπος της γλώσσας (π.χ. int, double κλπ.). Για παράδειγμα η δήλωση:

int *x;
δημιουργεί μια μεταβλητή δείκτη που δείχνει σε διευθύνσεις όπου θα αποθηκευτούν ακέραιες τιμές. Από την άλλη η δήλωση:

double *z;
δημιουργεί μια μεταβλητή δείκτη που δείχνει σε διευθύνσεις όπου μπορούν να τοποθετηθούν τιμές με δεκαδικά ψηφία. Ωστόσο, αν και οι δύο δείκτες έχουν διαφορετικό βασικό τύπο (int και double αντίστοιχα), στην πράξη και ο ένας δείκτης αλλά και ο άλλος θα έχουν το ίδιο μέγεθος σε bytes και θα μπορούν και οι δύο να αποθηκεύσουν διευθύνσεις. Αυτό φαίνεται και στο παράδειγμα του κώδικα 7.2, όπου εμφανίζεται στην οθόνη το μέγεθος σε bytes των δεικτών για δύο μεταβλητές διαφορετικού τύπου.

Κώδικας 7.2: ch7_p2.c - το μέγεθος των μεταβλητών δεικτών είναι ίδιο.
#include <stdio.h>

int main(void) {
  int a = 100;
  double b = 200;
  int *x = NULL; // είναι καλή πρακτική να αρχικοποιούμε τους δείκτες με την τιμή NULL
  double *z = NULL;
  printf("Sizes of variables %zu and %zu \n", sizeof(a), sizeof(b));
  printf("Sizes of pointers %zu and %zu \n", sizeof(x), sizeof(z));
  return 0;
}
Αν για παράδειγμα η εκτέλεση του προγράμματος γίνει σε ένα υπολογιστικό σύστημα 64bits, τότε είναι πολύ πιθανό να παραχθεί η ακόλουθη έξοδος:

Sizes of variables 4 and 8
Sizes of pointers 8 and 8
Το αποτέλεσμα υποδεικνύει ότι και στις δυο περιπτώσεις δεικτών θα χρειαστούν 8 bytes, μιας και τα συστήματα αρχιτεκτονικής 64bit χρησιμοποιούν διευθύνσεις μνήμης μεγέθους 64bits (8 bytes).
Στο παράδειγμα του κώδικα 7.3 γίνεται ανάθεση διευθύνσεων μεταβλητών σε δείκτες και στη συνέχεια γίνεται εμφάνιση αυτών των διευθύνσεων.

Κώδικας 7.3: ch7_p3.c - ανάθεση διευθύνσεων μεταβλητών σε δείκτες.
#include <stdio.h>

int main(void) {
  int x = 100;
  double y = 200;
  int *px = &x;
  double *py = &y;
  printf("x=%d y=%lf\n", x, y);
  printf("&px=%p &py=%p\n", (void *)px, (void *)py);
  return 0;
}
Μια πιθανή έξοδος κατά την εκτέλεση του προγράμματος είναι η ακόλουθη:

x=100 y=200.000000
&px=0x16d36ee28 &py=0x16d36ee20
Οι δείκτες μπορούν να χρησιμοποιηθούν για την απόδοση τιμών στις μεταβλητές στις οποίες δείχνουν, δηλαδή κάποιος μπορεί έμμεσα να αλλάξει την τιμή μιας μεταβλητής μέσω ενός δείκτη. Η διαδικασία αυτή ονομάζεται αποαναφορά (dereference). Ένα σχετικό παράδειγμα παρουσιάζεται στον κώδικα 7.4 όπου πραγματοποιείται έμμεση αλλαγή της τιμή μιας μεταβλητής, χωρίς απευθείας πρόσβαση στην ίδια τη μεταβλητή, αλλά με αποαναφορά στον δείκτη που δείχνει σε αυτήν τη μεταβλητή.

Κώδικας 7.4: ch7_p4.c - αποαναφορά δείκτη.
#include <stdio.h>

int main(void) {
  int x = 100;
  double y = 200;
  int *px = &x;
  double *py = &y;
  printf("x=%d y=%lf\n", x, y);
  *px = 5;
  *py = 20;
  printf("x=%d y=%lf\n", x, y);
  return 0;
}

Η εκτέλεση του προγράμματος εμφανίζει:

x=100 y=200.000000
x=5 y=20.000000

7.3 Δείκτες και συναρτήσεις

7.3.1 Κλήση με αναφορά

Οι συναρτήσεις στη γλώσσα C τυπικά δέχονται ορίσματα με τιμή, δηλαδή απλά γίνεται ανάθεση της τιμής που περνά η καλούσα συνάρτηση στο όρισμα και τίποτα παραπάνω. Έστω το παράδειγμα του κώδικα 7.5.

Κώδικας 7.5: ch7_p5.c - κλήση με τιμή (call by value).
#include <stdio.h>

void f(int x) {
  printf("F(x)=%d\n", x);
  x = x + 1;
  printf("F(x)=%d\n", x);
}

int main(void) {
  int mainX = 100;
  printf("MAIN(x)=%d\n", mainX);
  f(mainX);
  printf("MAIN(x)=%d\n", mainX);
  return 0;
}
Σε αυτό το παράδειγμα η συνάρτηση main() περνά την τιμή 100 στο όρισμα x της συνάρτησης f(). Η συνάρτηση αλλάζει το όρισμα και το εκτυπώνει και μετά επιστρέφει στη main(). Η εκτέλεση του παραπάνω προγράμματος δίνει τα ακόλουθα αποτελέσματα:

MAIN(x)=100
F(x)=100
F(x)=101
MAIN(x)=100
Η συνάρτηση f() όντως αλλάζει την τοπική μεταβλητή x κατά 1, αλλά αυτή η αλλαγή δεν μεταφέρεται και στη μεταβλητή mainX στη συνάρτηση main(). Αυτό συμβαίνει διότι η μεταβλητή mainX απλά αρχικοποίησε την τιμή του ορίσματος x και οι αλλαγές που μετέπειτα γίνονται στη x δεν μεταφέρονται στη mainX. Το πρόγραμμα θα είχε ακριβώς την ίδια συμπεριφορά ακόμα και αν η συνάρτηση f() καλούνταν με f(100) αντί για την κλήση f(mainX). Για τον λόγο αυτό λέμε πως έγινε κλήση με τιμή (call by value). Αν είναι επιθυμητό η συνάρτηση f() να αλλάξει και την τιμή της mainX υπάρχουν δύο επιλογές:

  • Nα επιστρέφει την τιμή με return.
  • Να γίνει κλήση με αναφορά.

Στην πρώτη περίπτωση θα πρέπει η συνάρτηση να έχει την εντολή return x ως τελευταία εντολή έτσι ώστε επιστραφεί η τιμή στη main(). Σε αυτήν την περίπτωση το πρόγραμμα θα λάβει τη μορφή που παρουσιάζεται στον κώδικα 7.6.

Κώδικας 7.6: ch7_p6.c - αλλαγή της τιμής της μεταβλητής mainX με εκχώρηση της τιμής που επιστρέφει η συνάρτηση f() στη mainX.
#include <stdio.h>

int f(int x) {
  printf("F(x)=%d\n", x);
  x = x + 1;
  printf("F(x)=%d\n", x);
  return x;
}

int main(void) {
  int mainX = 100;
  printf("MAIN(x)=%d\n", mainX);
  mainX = f(mainX);
  printf("MAIN(x)=%d\n", mainX);
  return 0;
}

Η συνάρτηση f() επιστρέφει το όρισμα x αλλαγμένο, επομένως και η main() θα μπορέσει να το λάβει με επιστροφή τιμής. Η εκτέλεση του παραπάνω προγράμματος παρουσιάζεται στη συνέχεια:

MAIN(x)=100
F(x)=100
F(x)=101
MAIN(x)=101

Δεν είναι πάντα εφικτό να χρησιμοποιείται η τιμή επιστροφής μιας συνάρτησης για την αλλαγή της τιμής ενός ορίσματός της, επομένως πρέπει να βρεθεί κάποιος εναλλακτικός τρόπος επιστροφής τιμής από συνάρτηση. Σε αυτόν τον εναλλακτικό τρόπο, αντί να περνά η τιμή μιας μεταβλητής σε μια συνάρτηση, περνά ένας δείκτης προς τη διεύθυνση της μεταβλητής και η αλλαγή στην τιμή της μεταβλητής γίνεται με αποαναφορά. Αυτό το είδος περάσματος παραμέτρων ονομάζεται κλήση με αναφορά (call by reference). Η νέα μορφή του κώδικα παρουσιάζεται στον κώδικα 7.7. Σε αυτήν την περίπτωση η συνάρτηση f() δέχεται ως όρισμα έναν δείκτη σε ακέραιο και όχι έναν ακέραιο όπως πριν. Ωστόσο, για να μπορέσει να αλλάξει την τιμή που δείχνει ο δείκτης πρέπει να γίνει αποαναφορά. Μετά την ολοκλήρωση της εκτέλεσης της συνάρτησης, η τιμή της mainX θα έχει αλλάξει επίσης, καθώς η συνάρτηση f() θα έχει αλλάξει το περιεχόμενο που έδειχνε ο δείκτης x και όχι τον ίδιο τον δείκτη.

Κώδικας 7.7: ch7_p7.c - αλλαγή της τιμής της μεταβλητής mainX μέσα στη συνάρτηση f() με αποαναφορά.
#include <stdio.h>

void f(int *x) {
  printf("F(x)=%d\n", *x);
  *x = *x + 1;
  printf("F(x)=%d\n", *x);
}

int main(void) {
  int mainX = 100;
  printf("MAIN(x)=%d\n", mainX);
  f(&mainX);
  printf("MAIN(x)=%d\n", mainX);
  return 0;
}

H εκτέλεση του προγράμματος με τη νέα αλλαγή θα εμφανίσει:

MAIN(x)=100
F(x)=100
F(x)=101
MAIN(x)=101

7.3.2 Επιστροφή πολλών τιμών από συνάρτηση

Με τη χρήση των δεικτών και τη δυνατότητα που δίνουν για έμμεση αναφορά σε μεταβλητές γίνεται εφικτή η επιστροφή παραπάνω από μια τιμών από μια συνάρτηση. Για παράδειγμα έστω ότι είναι επιθυμητό μια συνάρτηση να επιστρέφει μια τιμή από έναν υπολογισμό. Αυτό συμβαίνει στον κώδικα 7.8 όπου η συνάρτηση calc_min() δέχεται ως όρισμα μια ακέραια τιμή n, διαβάζει από το πληκτρολόγιο n αριθμούς και επιστρέφει τον μικρότερο από αυτούς.

Κώδικας 7.8: ch7_p8.c - παράδειγμα συνάρτησης που επιστρέφει μια τιμή.
#include <stdio.h>

int calc_min(int n) {
  int min = 0, x;
  for (int i = 0; i < n; i++) {
    printf("Input a value: ");
    scanf("%d", &x);
    if (i == 0 || x < min) {
      min = x;
    }
  }
  return min;
}

int main(void) {
  int min_value = calc_min(5);
  printf("Min = %d\n", min_value);
  return 0;
}

Ωστόσο, αν είναι επιθυμητό η συνάρτηση να επιστρέψει ταυτόχρονα και το μέγιστο και το ελάχιστο των n αριθμών, αυτό σημαίνει ότι θα πρέπει να οριστεί μια δομή με πεδία τα δύο αποτελέσματα και να επιστρέφεται από τη συνάρτηση μια εγγραφή αυτής της δομής. Ένας απλούστερος τρόπος είναι να πραγματοποιηθεί κλήση με αναφορά και η συνάρτηση να δεχθεί ως ορίσματα δύο δείκτες, με τον πρώτο να δείχνει στην ελάχιστη τιμή του πίνακα και τον δεύτερο στη μέγιστη τιμή, όπως συμβαίνει στον κώδικα 7.9. Ο δείκτης min δείχνει προς την ελάχιστη τιμή και ο δείκτης max στη μέγιστη. Για να επιστραφούν αυτές οι τιμές με αλλαγές, χρειάζεται να γίνει αποαναφορά δεικτών.

Κώδικας 7.9: ch7_p9.c - παράδειγμα συνάρτησης που επιστρέφει δύο τιμές μέσω των παραμέτρων της.
#include <stdio.h>

void calc_min_max(int n, int *min, int *max) {
  int x;
  for (int i = 0; i < n; i++) {
    printf("Input a value: ");
    scanf("%d", &x);
    if (i == 0 || x < *min) {
      *min = x;
    }
    if (i == 0 || x > *max) {
      *max = x;
    }
  }
}

int main(void) {
  int min_value, max_value;
  calc_min_max(5, &min_value, &max_value);
  printf("min: %d max: %d\n", min_value, max_value);
  return 0;
}
Ένα άλλο παράδειγμα της ίδιας τεχνικής είναι και αυτό της συνάρτησης που αντιμεταθέτει μεταβλητές. Μια πρώτη προσπάθεια να γίνει αυτό παρουσιάζεται στον κώδικα 7.10.

Κώδικας 7.10: ch7_p10.c - λανθασμένος κώδικας συνάρτησης αντιμετάθεσης μεταβλητών.
#include <stdio.h>

void swap(int a, int b) {
  int t = a;
  a = b;
  b = t;
}

int main(void) {
  int x = 100, y = 200;
  printf("Before swap: x=%d y=%d\n", x, y);
  swap(x, y);
  printf("After swap:  x=%d y=%d\n", x, y);
  return 0;
}
Προφανώς, ο παραπάνω κώδικας δεν πραγματοποιεί αντιμετάθεση, καθώς οι μεταβλητές περνούν στη συνάρτηση με τιμή και το μόνο που κάνει η συνάρτηση είναι να αντιμεταθέσει τις τοπικές μεταβλητές και όχι τα πραγματικά ορίσματα. Το πρόβλημα μπορεί να επιλυθεί με χρήση δεικτών όπως παρουσιάζεται στον κώδικα 7.11.

Κώδικας 7.11: ch7_p11.c - συνάρτηση που αντιμεταθέτει δύο ακέραιες μεταβλητές.
#include <stdio.h>

void swap(int *a, int *b) {
  int t = *a;
  *a = *b;
  *b = t;
}

int main(void) {
  int x = 100, y = 200;
  printf("Before swap: %d %d\n", x, y);
  swap(&x, &y);
  printf("After swap:  %d %d\n", x, y);
  return 0;
}

7.3.3 Δείκτες σε συναρτήσεις

Ένας δείκτης σε συνάρτηση (function pointer) είναι μια ειδική περίπτωση δείκτη όπου ο δείκτης δεν δείχνει σε μια θέση μνήμης που περιέχει μια μεταβλητή, αλλά στο σημείο εκκίνησης μίας συνάρτησης. Ένα παράδειγμα που επιδεικνύει τη χρήση δεικτών σε συναρτήσεις βρίσκεται στον κώδικα 7.12. Στο παράδειγμα αυτό δημιουργείται ένας δείκτης σε συνάρτηση που δέχεται ως ορίσματα δύο ακέραιες τιμές και επιστρέφει μια ακέραια τιμή. Έτσι με έμμεση αναφορά στην αντίστοιχη συνάρτηση μπορεί να καλείται είτε η συνάρτηση add() είτε η συνάρτηση sub().

Κώδικας 7.12: ch7_p12.c - δείκτης σε συνάρτηση.
#include <stdio.h>

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

int main(void) {
  int (*fpointer)(int, int);
  int v1, v2;
  int a = 200, b = 100;
  fpointer = add;
  v1 = fpointer(a, b);
  fpointer = sub;
  v2 = fpointer(a, b);
  printf("Calling indirectly add() gives %d\n", v1);
  printf("Calling indirectly sub() gives %d\n", v2);
  return 0;
}

Ακολουθεί το αποτέλεσμα της εκτέλεσης του κώδικα:

Calling indirectly add() gives 300
Calling indirectly sub() gives 100

7.4 Πίνακες και δείκτες

Στη γλώσσα C υπάρχει μια σύνδεση ανάμεσα στους πίνακες και τους δείκτες και γενικά κάθε αναφορά σε δείκτη μπορεί να θεωρηθεί και αναφορά σε πίνακα όπως και το αντίστροφο. Για παράδειγμα οι δηλώσεις:

int x[5];
int *px = &x[0];

δημιουργούν έναν πίνακα ακεραίων με 5 συνεχόμενα στοιχεία στη μνήμη του υπολογιστή και αναθέτουν στον δείκτη px τη διεύθυνση του πρώτου στοιχείου του πίνακα. Στα στοιχεία του πίνακα x μπορεί να γίνει αναφορά με τον γνωστό τρόπο, δηλαδή x[0], x[1], x[2], x[3], x[4] αλλά και με χρήση του δείκτη px, όπως φαίνεται και στον κώδικα 7.13.

Κώδικας 7.13: ch7_p13.c - δείκτης στο πρώτο στοιχείο πίνακα.
#include <stdio.h>

int main(void) {
  int x[5];
  int *px = &x[0];
  x[0] = 1;
  *(px + 1) = 10;
  *(px + 2) = 100;
  x[3] = 1000;
  x[4] = 10000;
  for (int i = 0; i < 5; i++) {
    printf("x[%d]=%d ", i, x[i]);
  }
  printf("\n");
  for (int i = 0; i < 5; i++) {
    printf("*(%p)=%d ", (void*)&x[i], x[i]);
  }
  printf("\n");
  return 0;
}

Η αναφορά *(px+1) αφορά το δεύτερο στοιχείο του πίνακα και η αναφορά *(px+2) το τρίτο στοιχείο του πίνακα. Η έξοδος του προγράμματος είναι:

x[0]=1 x[1]=10 x[2]=100 x[3]=1000 x[4]=10000
*(0x16d6f2e14)=1 *(0x16d6f2e18)=10 *(0x16d6f2e1c)=100 *(0x16d6f2e20)=1000
    ↪ *(0x16d6f2e24)=10000

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

Κώδικας 7.14: ch7_p14.c - εφαρμογή τελεστών μοναδιαίας αύξησης και μοναδιαίας μείωσης σε μεταβλητές δεικτών.
#include <stdio.h>

int main(void) {
  char c = 'A';
  char *p2c = &c;
  int i = 100;
  int *p2i = &i;
  printf("c=%c i=%d p2c=%p p2i=%p\n", c, i, (void*)p2c, (void*)p2i);
  p2c++;
  p2i++;
  printf("c=%c i=%d p2c=%p p2i=%p\n", c, i, (void*)p2c, (void*)p2i);
  p2i--;
  printf("c=%c i=%d p2c=%p p2i=%p\n", c, i, (void*)p2c, (void*)p2i);
  return 0;
}
Μια ενδεικτική εκτέλεση δίνει τα ακόλουθα αποτελέσματα:

c=A i=100 p2c=0x16bd42e2b p2i=0x16bd42e1c
c=A i=100 p2c=0x16bd42e2c p2i=0x16bd42e20
c=A i=100 p2c=0x16bd42e2c p2i=0x16bd42e1c
H χρήση μοναδιαίων τελεστών αύξησης σε δείκτες δεν έχει τα ίδια αποτελέσματα όπως στην περίπτωση των ακέραιων μεταβλητών. Στους δείκτες κάθε αύξηση ή μείωση μετακινεί τον δείκτη στην επόμενη ή στην προηγούμενη θέση μνήμης αντίστοιχα. Στην περίπτωση των δεικτών σε χαρακτήρα αυτό σημαίνει ότι η μετακίνηση γίνεται κατά μια θέση, αφού οι μεταβλητές χαρακτήρα απαιτούν 1 byte για αποθήκευση, ενώ στην περίπτωση των ακέραιων τιμών η μετακίνηση γίνεται κατά 4 θέσεις, εφόσον για κάθε ακέραιο απαιτούνται 4 bytes.

Οι δείκτες μπορούν να χρησιμοποιηθούν για τη διάσχιση πίνακα μέσω των τελεστών μοναδιαίας αύξησης και μείωσης. Στο παράδειγμα του κώδικα 7.15 σκοπός είναι να βρεθεί στον πίνακα x η θέση της τιμής που περιέχει η μεταβλητή element. Για την αναζήτηση χρησιμοποιείται δείκτης που δείχνει αρχικά στο πρώτο στοιχείο του πίνακα. Σε κάθε επανάληψη, όσο η τιμή δεν έχει ακόμα εντοπιστεί, ο δείκτης πηγαίνει στο επόμενο στοιχείο του πίνακα με χρήση του μοναδιαίου τελεστή ++. Σε κάθε επανάληψη ο δείκτης px μεταφέρεται στη διεύθυνση του επόμενου στοιχείου του πίνακα και με αποαναφορά γίνεται έλεγχος σχετικά με το εάν ο πίνακας περιέχει τη ζητούμενη τιμή.

Κώδικας 7.15: ch7_p15.c - διάσχιση πίνακα με δείκτη προς την αρχή του και εφαρμογή του τελεστή ++.
#include <stdio.h>

int main(void) {
  int x[] = {10, 20, 200, 300, 400}; // πίνακας 5 θέσεων
  int element = 200;                 // τιμή προς αναζήτηση
  int *px = &x[0];
  // ατέρμονας βρόχος που θα διακοπεί με break
  while (1) {
    if (*px == element) {
      printf("Value %d found at position %ld\n", element, px - &x[0]);
      break;
    }
    if (px == &x[4]) {
      printf("Value %d not found\n", element);
      break;
    }
    px++;
  }
  return 0;
}

7.4.1 Δείκτης σε δείκτη

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

Κώδικας 7.16: ch7_p16.c - «λήψη» ενός υποπίνακα με χρήση δεικτών.
#include <stdio.h>

int main(void) {
  int x[] = {10, 20, 200, 300, 400};
  int start_pos = 3;
  int *sub_x = &x[start_pos];
  for (int i = start_pos; i < 5; i++) {
    printf("Element at index %d of the subarray has value %d \n", i - start_pos,
           sub_x[i - start_pos]);
  }
  return 0;
}

Η έξοδος του παραπάνω προγράμματος είναι:

Element at index 0 of the subarray has value 300
Element at index 1 of the subarray has value 400

Επιπλέον, ένας δείκτης μπορεί να δείξει και σε άλλο δείκτη, όπως φαίνεται και στο παράδειγμα του κώδικα 7.17.

Κώδικας 7.17: ch7_p17.c - δείκτης σε δείκτη.
#include <stdio.h>

int main(void) {
  int var = 100;
  int *pt1 = &var;
  int **pt2 = &pt1;
  printf("1: var=%d pt1=%p pt2=%p\n", var, (void*)pt1, (void*)pt2);
  (*pt1)++;
  printf("2: var=%d pt1=%p pt2=%p\n", var, (void*)pt1, (void*)pt2);
  (**pt2)++;
  printf("3: var=%d pt1=%p pt2=%p\n", var, (void*)pt1, (void*)pt2);
  return 0;
}

Ο δείκτης pt1 δείχνει στη διεύθυνση της ακέραιας μεταβλητής var και ο δείκτης pt2 δείχνει στη διεύθυνση του δείκτη pt1. Μετά την εκτέλεση του προγράμματος, μια ενδεικτική εκτύπωση θα είναι η ακόλουθη:

1: var=100 pt1=0x16d6e2e28 pt2=0x16d6e2e20
2: var=101 pt1=0x16d6e2e28 pt2=0x16d6e2e20
3: var=102 pt1=0x16d6e2e28 pt2=0x16d6e2e20

Η πρώτη γραμμή της εξόδου εμφανίζει την τιμή της μεταβλητής και τις διευθύνσεις που έχουν οι δείκτες. Η αποαναφορά *pt1 επιστρέφει την τιμή 100 όπως είναι άλλωστε και η τιμή της var. Την ίδια τιμή θα επέστρεφε και η αποαναφορά του διπλού δείκτη **pt2. Συνεπώς, οι εντολές (*pt1)++ και (**pt2)++ προκαλούν αύξηση της τιμής που διατηρεί η μεταβλητή var κατά ένα και έτσι προκύπτει η έξοδος στη δεύτερη και τρίτη γραμμή της εξόδου.
Μια χρησιμότητα των δεικτών σε δείκτη εντοπίζεται στις περιπτώσεις συναρτήσεων που είναι επιθυμητή η αλλαγή ενός δείκτη και όχι του περιεχομένου του, όπως παρουσιάζεται στο παράδειγμα του κώδικα 7.18 όπου η συνάρτηση try1() και η συνάρτηση try2() στοχεύουν στο να αλλάξουν έμμεσα την τιμή που βρίσκεται στη θέση 3 του πίνακα.

Κώδικας 7.18: ch7_p18.c - αλλαγή ενός δείκτη που περνά ως όρισμα σε μια συνάρτηση.
#include <stdio.h>

void try1(int *x) {
  printf("BEFORE x=%p\n", (void *)x);
  x++;
  printf("AFTER x=%p\n", (void *)x);
}

void try2(int **x) { (*x)++; }

int main(void) {
  int x[] = {10, 20, 30, 40, 50};
  int *pt1 = &x[2];
  int *pt2 = &x[2];
  printf("pt1=%p, pt2=%p\n", (void *)pt1, (void *)pt2);

  try1(pt1);
  *pt1 = *pt1 + 1;
  for (int i = 0; i < 5; i++) {
    printf("%p=%d ", (void *)&x[i], x[i]);
  }
  printf("\n");

  try2(&pt2); // καλείται με τη διεύθυνση του δείκτη pt2
  *pt2 = *pt2 + 1;
  for (int i = 0; i < 5; i++) {
    printf("%p=%d ", (void *)&x[i], x[i]);
  }
  printf("\n");
  // δείκτης pt1 δεν αλλάζει περιεχόμενο ενώ ο δείκτης pt2 αλλάζει
  printf("pt1=%p, pt2=%p\n", (void *)pt1, (void *)pt2);
  return 0;
}
Η έξοδος του προγράμματος είναι:

pt1=0x16b5df068 , pt2=0x16b5df068
BEFORE x=0x16b5df068
AFTER x=0x16b5df06c
0x16b5df060=10 0x16b5df064=20 0x16b5df068=31 0x16b5df06c=40 0x16b5df070=50
0x16b5df060=10 0x16b5df064=20 0x16b5df068=31 0x16b5df06c=41 0x16b5df070=50
pt1=0x16b5df068 , pt2=0x16b5df06c
Όπως είναι φανερό, μόνο η συνάρτηση try2() που δέχεται δείκτη σε δείκτη μπορεί να αλλάξει την ίδια την τιμή του δείκτη.

7.4.2 Πέρασμα πινάκων σε συναρτήσεις

Οι δείκτες μπορούν να χρησιμοποιηθούν για το πέρασμα πινάκων σε συναρτήσεις. Στο παράδειγμα του κώδικα 7.19, τα στοιχεία ενός πίνακα αλλάζουν μέσα σε μια συνάρτηση.

Κώδικας 7.19: ch7_p19.c - τροποποίηση στοιχείων πίνακα που περνά ως όρισμα σε συνάρτηση.
#include <stdio.h>

void modify_table(int *x, int size) {
  int pos = 0;
  while (pos != size) {
    if (pos % 2 == 0) {
      *x++ = 100;
    } else {
      *x++ = 200;
    }
    pos++;
  }
}

int main(void) {
  int t[] = {10, 20, 30, 40, 50};
  modify_table(t, 5);
  for (int i = 0; i < 5; i++) {
    printf("%d ", t[i]);
  }
  printf("\n");
  return 0;
}
Η έξοδος του προγράμματος είναι:

100 200 100 200 100
Η αλλαγή στις τιμές έγινε με χρήση του μοναδιαίου τελεστή αύξησης. Η συνάρτηση modify_table() μπορεί να γραφεί και απλούστερα, όπως παρουσιάζεται στον κώδικα 7.20, όπου δεν χρειάζεται πλέον η διαδικασία της αποαναφοράς που παρουσιάστηκε προηγουμένως. Παρατηρήστε ότι η αποαναφορά *(x+i) είναι ισοδύναμη με τον κώδικα x[i].

Κώδικας 7.20: ch7_p20.c - τροποποίηση στοιχείων πίνακα μέσω συνάρτησης.
#include <stdio.h>

void modify_table(int *x, int size) {
  for (int i = 0; i < size; i++) {
    if (i % 2 == 0) {
      *(x + i) = 100;
    } else {
      *(x + i) = 200;
    }
  }
}

int main(void) {
  int t[] = {10, 20, 30, 40, 50};
  modify_table(t, 5);
  for (int i = 0; i < 5; i++) {
    printf("%d ", t[i]);
  }
  printf("\n");
  return 0;
}
Η έξοδος είναι ίδια όπως και στο προηγούμενο παράδειγμα.
Μια συνάρτηση μπορεί να χρησιμοποιηθεί ώστε να επιστρέψει πολλαπλές τιμές ως πεδία ενός πίνακα. Για παράδειγμα αν θέλουμε μια συνάρτηση να επιστρέφει την ελάχιστη τιμή, τη μέγιστη τιμή και τον μέσο όρο των τιμών ενός πίνακα, αυτό μπορεί να γίνει με χρήση 3 δεικτών, έναν για κάθε αποτέλεσμα. Ωστόσο, το ίδιο μπορεί να γίνει και με πίνακα όπως παρουσιάζεται στον κώδικα 7.21. Στη συνάρτηση get_stats() το στοιχείο του πίνακα stats[0] χρησιμοποιείται για την αποθήκευση της ελάχιστης τιμής, το stats[1] για αποθήκευση της μέγιστης τιμής και το stats[2] για αποθήκευση του μέσου όρου.

Κώδικας 7.21: ch7_p21.c - επιστροφή πολλών αποτελεσμάτων από συνάρτηση με χρήση ενός ορίσματος πίνακα.
#include <stdio.h>

#define SIZE 10

void get_stats(double *x, int n, double *stats) {
  stats[0] = stats[1] = stats[2] = x[0];
  for (int i = 1; i < n; i++) {
    if (x[i] < stats[0]) {
      stats[0] = x[i];
    }
    if (x[i] > stats[1]) {
      stats[1] = x[i];
    }
    stats[2] += x[i];
  }
  stats[2] /= n;
}

int main(void) {
  double table[SIZE] = {1, 2, 4, 5, 10, 11, 12, 19, 20, 21};
  double st[3];
  get_stats(table, SIZE, st);
  printf("min: %.2lf max: %.2lf mean: %.2lf\n", st[0], st[1], st[2]);
  return 0;
}

Ωστόσο, η ανάγκη απομνημόνευσης της σειράς που είναι αποθηκευμένα τα αποτελέσματα στον πίνακα προκαλεί δυσκολία στην κατανόηση του κώδικα. Στον κώδικα 7.22 παρουσιάζεται μια ισοδύναμη υλοποίηση που χρησιμοποιεί κατάλληλα ονοματισμένους δείκτες τοπικής εμβέλειας. Η ανάθεση τιμών στους βοηθητικούς δείκτες πραγματοποιείται στη γραμμή 6 του κώδικα.

Κώδικας 7.22: ch7_p22.c - χρήση δεικτών τοπικής εμβέλειας για ευανάγνωστο κώδικα.
#include <stdio.h>

#define SIZE 10

void get_stats(double *x, int n, double *stats) {
  double *min = &stats[0], *max = &stats[1], *avg = &stats[2];
  *min = x[0];
  *max = x[0];
  *avg = x[0];
  for (int i = 1; i < n; i++) {
    if (x[i] < *min) {
      *min = x[i];
    }
    if (x[i] > *max) {
      *max = x[i];
    }
    *avg += x[i];
  }
  *avg /= n;
}

int main(void) {
  double table[SIZE] = {1, 2, 4, 5, 10, 11, 12, 19, 20, 21};
  double st[3];
  get_stats(table, SIZE, st);
  printf("min: %.2lf max: %.2lf mean: %.2lf\n", st[0], st[1], st[2]);
  return 0;
}

7.4.3 Δείκτες και αλφαριθμητικά

Τα αλφαριθμητικά είναι και αυτά πίνακες και επομένως όσα έχουν αναφερθεί μέχρι στιγμής για τη σχέση δεικτών και πινάκων ισχύουν και για τα αλφαριθμητικά με την επιπλέον σημείωση πως στο τέλος των αλφαριθμητικών πρέπει να υπάρχει πάντα ο χαρακτήρας τερματισμού '\0'. Στο παράδειγμα του κώδικα 7.23 πραγματοποιείται αρχικοποίηση αλφαριθμητικών με χρήση δεικτών. Το αλφαριθμητικό s1 είναι στατικό και η ανάθεση τιμής σε αυτό γίνεται στη συνέχεια με τη συνάρτηση strcpy(). Το αλφαριθμητικό s2 αρχικοποιείται με ανάθεση τιμής κατά τη δήλωσή του. Το αλφαριθμητικό s3 είναι δείκτης που δείχνει σε μια δεσμευμένη περιοχή μνήμης όπου υπάρχουν οι χαρακτήρες της ακολουθίας.

Κώδικας 7.23: ch7_p23.c - αναθέσεις τιμών σε αλφαριθμητικά.
#include <stdio.h>
#include <string.h>

int main(void) {
  char s1[80];
  char s2[] = "Hello from phrase2";
  char *s3 = "Hello from phrase3";
  strcpy(s1, "Static String");
  printf("s1=%s s2=%s s4=%s \n", s1, s2, s3);
  return 0;
}

Οι δείκτες μπορούν να χρησιμοποιηθούν και για το πέρασμα αλφαριθμητικών ως ορισμάτων σε συναρτήσεις. Στον κώδικα 7.24 η συνάρτηση find_digits() επιστρέφει το πλήθος των χαρακτήρων ενός αλφαριθμητικού που είναι ψηφία. Η διαφορά με τις συναρτήσεις με ορίσματα πίνακες είναι πως στα αλφαριθμητικά δεν χρειάζεται να περάσει ως όρισμα το μήκος του αλφαριθμητικού, αφού αυτό υποδηλώνεται από το '\0' ή εναλλακτικά μπορεί να υπολογιστεί με τη συνάρτηση strlen() του string.h.

Κώδικας 7.24: ch7_p24.c - εύρεση του πλήθους των ψηφίων που βρίσκονται σε ένα αλφαριθμητικό.
#include <stdio.h>
#include <string.h>

int find_digits(char *s) {
  int count = 0;
  for (size_t i = 0; i < strlen(s); i++) {
    if (s[i] >= '0' && s[i] <= '9') {
      count++;
    }
  }
  return count;
}

int main(void) {
  char s1[80] = "leet 1337";
  printf("Digits: %d\n", find_digits(s1));
  return 0;
}

Ισοδύναμη συνάρτηση με τη strlen() μπορεί να γραφεί εύκολα κάνοντας χρήση αριθμητικής δεικτών, όπως φαίνεται στον κώδικα 7.25.

Κώδικας 7.25: ch7_p25.c - συνάρτηση εύρεσης του μήκους ενός αλφαριθμητικού.
#include <stdio.h>

int mylen(char *s) {
  int count = 0;
  // το '\0' είναι false
  while (*s++) {
    count++;
  }
  return count;
}

int main(void) {
  char s1[80] = "leet 1337";
  printf("Length: %d\n", mylen(s1));
  return 0;
}

7.5 Δείκτης σε δομή

Ένας δείκτης μπορεί να χρησιμοποιηθεί ακόμα και για να δείξει σε μια μεταβλητή δομής. Στο παράδειγμα του κώδικα 7.26 ένας δείκτης δείχνει σε μια δομή για πρόσωπα και με αποαναφορά πραγματοποιεί τροποποίηση των στοιχείων της δομής. Η αποαναφορά ενός δείκτη προς μία δομή προκειμένου να πραγματοποιηθεί πρόσβαση σε ένα πεδίο της δομής γίνεται με δύο τρόπους: με τον τελεστή τελείας, όπως στο παράδειγμα (*pt).name και εναλλακτικά με τον τελεστή ‐> όπως στο παράδειγμα pt‐>name. Και στις δύο περιπτώσεις που αναφέρθηκαν γίνεται αποαναφορά του δείκτη για πρόσβαση στο πεδίο name της δομής.

Κώδικας 7.26: ch7_p26.c - δείκτης σε δομή και πρόσβαση στα πεδία της δομής.
#include <stdio.h>
#include <string.h>

typedef struct {
  int id;
  char name[100];
  char lastname[100];
} person;

int main(void) {
  person a_person;
  person *pt = &a_person;
  printf("The pointer %p points to a 'person'\n", (void*)pt);
  printf("Each 'person' occupies %ld bytes\n", sizeof(person));
  strcpy((*pt).name, "Ioannis"); // α' τρόπος πρόσβασης σε πεδίο δομής
  strcpy(pt->lastname, "Pappas"); // β' τρόπος πρόσβασης σε πεδίο δομής
  pt->id = 1000; // β' τρόπος πρόσβασης σε πεδίο δομής
  printf("Id: %d, name: %s, last name: %s\n", a_person.id, a_person.name,
         a_person.lastname);
  return 0;
}

Μια ενδεικτική εκτέλεση του προγράμματος δίνει τα ακόλουθα αποτελέσματα:

The pointer 0x16b0a2d4c points to a 'person'
Each 'person' occupies 204 bytes
Id: 1000, name: Ioannis , last name: Pappas

7.6 Δείκτες και πίνακες

Οι πίνακες και οι δείκτες είναι σχετικές μεταξύ τους έννοιες άλλα όχι ταυτόσημες. Το όνομα ενός πίνακα είναι η διεύθυνση προς το πρώτο στοιχείο του πίνακα. Αυτό σημαίνει ότι, για παράδειγμα, για έναν πίνακα με όνομα a, πέντε ακεραίων, το a δείχνει στο πρώτο στοιχείο του πίνακα, δηλαδή στο a[0]. Συνεπώς, οι δύο τρόποι διάσχισης ενός πίνακα που παρουσιάζονται στον κώδικα 7.27 επιτυγχάνουν το ίδιο αποτέλεσμα.

Κώδικας 7.27: ch7_p27.c - διάσχιση πίνακα με δύο τρόπους.
#include <stdio.h>

int main(void) {
  int arr[5] = {1, 2, 3, 4, 5};
  int i;
  // Χρησιμοποιώντας δεικτοδότηση πίνακα
  printf("Using array indexing:\n");
  for (i = 0; i < 5; i++) {
    printf("%d ", arr[i]);
  }
  printf("\n");
  // Χρησιμοποιώντας αριθμητική δεικτών στον πίνακα
  printf("Using array pointer arithmetic:\n");
  for (i = 0; i < 5; i++) {
    printf("%d ", *(arr + i));
  }
  printf("\n");
  return 0;
}

Η έξοδος του προγράμματος είναι η ακόλουθη:

Using array indexing:
1 2 3 4 5
Using array pointer arithmetic:
1 2 3 4 5

Ωστόσο, οι πίνακες και οι δείκτες έχουν και διαφορές. Για παράδειγμα η χρήση του τελεστή sizeof σε δείκτη θα επιστρέψει το πλήθος των bytes που απαιτούνται για διευθυνσιοδότηση της μνήμης (π.χ. 8 bytes σε συστήματα αρχιτεκτονικής 64 bit), ενώ για έναν πίνακα θα επιστρέψει το συνολικό μέγεθος σε bytes του πίνακα. Ο κώδικας 7.28 δείχνει αυτήν τη διαφορά και ότι μπορεί να γίνει διάσχιση ενός πίνακα χρησιμοποιώντας έναν δείκτη προς το πρώτο στοιχείο του πίνακα και αριθμητική δεικτών για μετακίνηση από ένα στοιχείο του πίνακα στο επόμενο.

Κώδικας 7.28: ch7_p28.c - μερικές διαφορές δεικτών και πινάκων.
#include <stdio.h>

int main(void) {
  int arr[5] = {1, 2, 3, 4, 5};
  int *ptr = arr;
  // Μέγεθος πίνακα και δείκτη
  printf("Size of array (arr) = %zu bytes\n", sizeof(arr));
  printf("Size of pointer (ptr) = %zu bytes\n", sizeof(ptr));
  // Πρόσβαση στα στοιχεία μέσω δεικτοδότησης πίνακα και δείκτη
  printf("\nUsing array indexing:\n");
  for (int i = 0; i < 5; i++) {
    printf("arr[%d] = %d\n", i, arr[i]);
  }
  printf("\nUsing pointer:\n");
  for (int i = 0; i < 5; i++) {
    printf("*(ptr + %d) = %d\n", i, *(ptr + i));
  }
  // Αλλαγή διεύθυνσης δείκτη, αλλά όχι του πίνακα
  ptr++;
  // arr++; // Δεν γίνεται δεκτό από τον μεταγλωττιστή
  printf("\nAfter incrementing pointer:\n");
  printf("First element pointed by ptr = %d\n", *ptr);
  printf("First element of array = %d\n", arr[0]);
  return 0;
}

Η έξοδος του προγράμματος είναι η ακόλουθη:

Size of array (arr) = 20 bytes
Size of pointer (ptr) = 8 bytes

Using array indexing:
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4
arr[4] = 5

Using pointer:
*(ptr + 0) = 1
*(ptr + 1) = 2
*(ptr + 2) = 3
*(ptr + 3) = 4
*(ptr + 4) = 5

After incrementing pointer:
First element pointed by ptr = 2
First element of array = 1

Επίσης, στη γραμμή 20 έχει τοποθετηθεί σε σχόλια μια εντολή αλλαγής της διεύθυνσης που περιέχει το όνομα του πίνακα, κάτι το οποίο δεν επιτρέπεται. Αν η γραμμή 20 συμπεριληφθεί χωρίς σχόλιο, τότε ο μεταγλωττιστής θα εμφανίσει ένα μήνυμα λάθους της μορφής:

ch7_p28.c:24:8: error: cannot increment value of type 'int[5]'
    arr++; // Δεν γίνεται δεκτό από τον μεταγλωττιστή
    ~~~^
1 error generated.

Ένα ακόμα λεπτό σημείο που αφορά τη σχέση δεικτών και πινάκων είναι το λεγόμενο “decay to pointer” («αποσύνθεση» πίνακα σε δείκτη) και έχει να κάνει με το πέρασμα πινάκων σε συναρτήσεις. Ο κώδικας 7.29 δείχνει ότι στη main() το μέγεθος του πίνακα είναι το πραγματικό μέγεθός του σε bytes. Ωστόσο, μόλις o πίνακας περάσει ως όρισμα στη συνάρτηση print_array_size(), το μέγεθος του πίνακα πλέον είναι το μέγεθος ενός δείκτη. Αυτό δείχνει πως ο πίνακας «αποσυντίθεται» σε δείκτη όταν περνά ως όρισμα σε μια συνάρτηση.

Κώδικας 7.29: ch7_p29.c - αποσύνθεση πίνακα σε δείκτη.
#include <stdio.h>

// Δήλωση της συνάρτησης που δέχεται πίνακα ως όρισμα
void print_array_size(int arr[]) {
    // Εκτύπωση του μεγέθους του πίνακα εντός της συνάρτησης
    printf("Inside function: Array size = %zu bytes\n", sizeof(arr));
}

void print_elements(int arr[], int size) {
    printf("Array elements: ");
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main(void) {
    int numbers[5] = {1, 2, 3, 4, 5};
    // Εκτύπωση του μεγέθους του πίνακα εκτός της συνάρτησης
    printf("Outside function: Array size = %zu bytes\n", sizeof(numbers));
    print_array_size(numbers);
    // Εκτύπωση των στοιχείων του πίνακα
    print_elements(numbers, 5);
    return 0;
}
Outside function: Array size = 20 bytes
Inside function: Array size = 8 bytes
Array elements: 1 2 3 4 5

7.7 Ασκήσεις

Άσκηση 1
Να γραφεί συνάρτηση int last_found(int *x, int n, int element) που βρίσκει και επιστρέφει την τελευταία εμφάνιση του element στον πίνακα n στοιχείων, x. Αν το στοιχείο element δεν βρεθεί τότε η συνάρτηση να επιστρέφει την τιμή -1

Λύση άσκησης 1
#include <stdio.h>

int last_found(int *x, int n, int element) {
    for (int i = n-1; i >= 0; i--) {
        if (x[i] == element) {
            return i;
        }
    }
    return -1;
}

int main(void) {
    int arr[] = {1, 2, 3, 4, 3, 5, 6, 7, 3, 8, 9};
    int n = sizeof(arr) / sizeof(arr[0]); // Υπολογισμός του μεγέθους του πίνακα
    int element = 3; // Το στοιχείο που ψάχνουμε
    int position = last_found(arr, n, element);
    if (position != -1) {
        printf("Το στοιχείο %d βρέθηκε για τελευταία φορά στη θέση %d.\n", element, position);
    } else {
        printf("Το στοιχείο %d δεν βρέθηκε στον πίνακα.\n", element);
    }
    return 0;
}

Άσκηση 2
Nα γραφεί συνάρτηση int from_binary(char *s). Η συνάρτηση να λαμβάνει ως όρισμα το αλφαριθμητικό s που μπορεί να θεωρηθεί ότι περιέχει μόνο δυαδικά ψηφία (0,1) και να επιστρέφει την ισοδύναμη τιμή του στο δεκαδικό σύστημα, π.χ. αν s="1110", θα πρέπει να επιστρέψει 14.

Λύση άσκησης 2
#include <stdio.h>
#include <string.h>

int from_binary(char *s) {
  int length = strlen(s);
  int value = 0;

  // Διατρέχουμε το αλφαριθμητικό από το τέλος προς την αρχή
  for (int i = 0; i < length; i++) {
    // Αν το ψηφίο είναι '1', τότε προσθέτουμε στη δεκαδική τιμή το 2^i
    if (s[length - 1 - i] == '1') {
      value += (1 << i);
    }
  }

  return value;
}

int main(void) {
  char binary_string[] = "1110";
  int decimal_value = from_binary(binary_string);
  printf("Η δεκαδική τιμή του %s είναι %d.\n", binary_string, decimal_value);

  return 0;
}

Άσκηση 3
Γράψτε μια συνάρτηση με όνομα reverse_array που να αντιστρέφει ένα τμήμα ενός πίνακα. Η συνάρτηση να έχει ως πρωτότυπο το void reverse_array(int* start, int* end) και να αντιστρέφει το τμήμα του πίνακα από το στοιχείο του πίνακα στο οποίο δείχνει ο δείκτης start μέχρι και το στοιχείο που δείχνει ο δείκτης end. Γράψτε ένα πρόγραμμα που να επιδεικνύει τη λειτουργία της συνάρτησης εκτυπώνοντας έναν πίνακα πριν και μετά την κλήση της.

Λύση άσκησης 3
#include <stdio.h>

#define ARRAY_SIZE 10

// Συνάρτηση που αντιστρέφει τον πίνακα χρησιμοποιώντας δείκτες
void reverse_array(int* start, int* end) {
    while (start < end) {
        int temp = *start;
        *start = *end;
        *end = temp;
        start++;
        end--;
    }
}

int main(void) {
    int arr[ARRAY_SIZE] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    printf("Πίνακας πριν την αντιστροφή:\n");
    for (int i = 0; i < ARRAY_SIZE; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    // reverse_array(arr, arr + ARRAY_SIZE - 1);
    reverse_array(&arr[3], &arr[8]);

    printf("Πίνακας μετά την αντιστροφή:\n");
    for (int i = 0; i < ARRAY_SIZE; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

Άσκηση 4
Κατασκευάστε ένα πρόγραμμα που διαχειρίζεται τις βαθμολογίες των φοιτητών σε ένα μάθημα. Για κάθε φοιτητή, θα πρέπει να αποθηκεύεται το όνομά του, ο αριθμός μητρώου και ο βαθμός του στο μάθημα. Ορίστε μια δομή student που θα περιέχει τις παραπάνω πληροφορίες. Ορίστε μια δομή course που θα περιέχει έναν πίνακα (με μέγιστο μέγεθος 100) από εγγραφές της δομής student, και το πλήθος των φοιτητών που έχουν λάβει βαθμολογία στο μάθημα. Προσθέστε τις εξής λειτουργίες:

  1. Προσθήκη βαθμολογίας για έναν φοιτητή.
  2. Αφαίρεση βαθμολογίας φοιτητή με βάση έναν αριθμό μητρώου. Αν ο αριθμός μητρώου βρεθεί τότε ο βαθμός του φοιτητή να γίνεται μηδέν.
  3. Εκτύπωση όλων των βαθμολογιών των φοιτητών του μαθήματος.
Λύση άσκησης 4
#include <stdio.h>
#include <string.h>

#define MAX_STUDENTS 100
#define NAME_LENGTH 50

typedef struct {
    char name[NAME_LENGTH];
    int student_id;
    float grade;
} student;

typedef struct {
    student students[MAX_STUDENTS];
    int num_students;
} course;

void add_grade_by_id(course *c, int id, float grade) {
    for (int i = 0; i < c->num_students; i++) {
        if (c->students[i].student_id == id) {
            c->students[i].grade = grade;
            return;
        }
    }
    printf("Student with ID %d not found.\n", id);
}

void remove_grade_by_id(course *c, int id) {
    for (int i = 0; i < c->num_students; i++) {
        if (c->students[i].student_id == id) {
            c->students[i].grade = 0.0;  // Θεωρούμε ότι η αφαίρεση του βαθμού σημαίνει επαναφορά στο 0.0
            return;
        }
    }
    printf("Student with ID %d not found.\n", id);
}

void print_student_list(course *c) {
    for (int i = 0; i < c->num_students; i++) {
        printf("Name: %s, ID: %d, Grade: %.2f\n", c->students[i].name, c->students[i].student_id, c->students[i].grade);
    }
}

int main(void) {
    course my_course;
    my_course.num_students = 3;

    strcpy(my_course.students[0].name, "Nikos");
    my_course.students[0].student_id = 1;
    my_course.students[0].grade = 0.0;

    strcpy(my_course.students[1].name, "Maria");
    my_course.students[1].student_id = 2;
    my_course.students[1].grade = 0.0;

    strcpy(my_course.students[2].name, "Petros");
    my_course.students[2].student_id = 3;
    my_course.students[2].grade = 0.0;

    add_grade_by_id(&my_course, 2, 8.5);
    print_student_list(&my_course);  // Εκτύπωση της λίστας μετά την προσθήκη βαθμολογίας

    printf("\nRemoving grade for Maria...\n");
    remove_grade_by_id(&my_course, 2);
    print_student_list(&my_course);  // Εκτύπωση της λίστας μετά την αφαίρεση βαθμολογίας

    return 0;
}