Skip to content

9. Διαχείριση μνήμης

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

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

9.1 Δυναμική δέσμευση μνήμης

9.1.1 Συναρτήσεις της γλώσσας C

Η γλώσσα C μέσω του stdlib.h διαθέτει τις βασικές συναρτήσεις malloc() και free() για τη διαχείριση δυναμικών μεταβλητών. Η πρώτη χρησιμοποιείται για να δεσμευθεί ο χώρος μνήμης όπου θα αποθηκευτούν τα περιεχόμενα μιας νέας μεταβλητής και η δεύτερη για τη διαγραφή/αποδέσμευση της μνήμης. Ένα παράδειγμα χρήσης αυτών των συναρτήσεων παρουσιάζεται στον κώδικα 9.1.

Κώδικας 9.1: ch9_p1.c - ένα απλό παράδειγμα δέσμευσης και αποδέσμευσης μνήμης.
#include <stdio.h>
#include <stdlib.h>

int main(void) {
  int *ptr = NULL;
  printf("Pointer address: %p\n", (void *)ptr);
  ptr = malloc(sizeof(int));
  *ptr = 100;
  printf("Pointer address: %p value: %d\n", (void*)ptr, *ptr);
  free(ptr);
  return 0;
}

Η συνάρτηση malloc() έχει την ακόλουθη δήλωση:

void *malloc(size_t size);

Δέχεται ως όρισμα το πλήθος των bytes που θα δεσμεύσει και επιστρέφει ως αποτέλεσμα έναν δείκτη προς τη δεσμευμένη μνήμη που καταλαμβάνει συνεχόμενες θέσεις μνήμης στο τμήμα της μνήμης που ονομάζεται σωρός (heap). Από τη στιγμή αυτή και μετά έχει δημιουργηθεί μια νέα μεταβλητή και ο προγραμματιστής μπορεί να τη χρησιμοποιήσει όπως και κάθε άλλη μεταβλητή με τη διαφορά ότι για να γίνει ανάθεση σε αυτήν την περιοχή μνήμης απαιτείται αποαναφορά. Όταν η μεταβλητή δεν θα είναι πλέον χρήσιμη για τη συνέχεια του προγράμματος μπορεί να γίνει αποδέσμευση της μνήμης που είχε δεσμευθεί με τη συνάρτηση free(). Ένα παράδειγμα εκτέλεσης του κώδικα 9.1 παρουσιάζεται στη συνέχεια.

Pointer address: 0x0
Pointer address: 0x5402040 value: 100

Στην πρώτη γραμμή της εξόδου παρουσιάζεται η τιμή που έχει ανατεθεί στον δείκτη που είναι 0 (NULL). Στη δεύτερη γραμμή της εξόδου ο δείκτης δείχνει σε μια υπαρκτή θέση μνήμης όπου έχει εισαχθεί η τιμή 100. Αν η συνάρτηση malloc() επιστρέψει 0, τότε δεν θα έχει βρεθεί διαθέσιμη μνήμη για να γίνει δέσμευσή της. Αυτό μπορεί να συμβεί σε περιπτώσεις όπου τα προγράμματα δεσμεύουν διαρκώς μεγάλες ποσότητες μνήμης. Η συνάρτηση free() διαγράφει τον δείκτη και από τη στιγμή που η free() κληθεί δεν θα πρέπει πλέον να γίνεται αποαναφορά στον δείκτη, εκτός και αν φυσικά ο δείκτης λάβει τιμή που θα τον κάνει να δείχνει σε κάποια άλλη έγκυρη θέση μνήμης. Σχηματικά, οι λειτουργίες που εκτελέστηκαν με τη malloc() και τη free() φαίνονται στα Σχήματα 9.1 και 9.2 αντίστοιχα.

Σχήμα 9.1: Δέσμευση μνήμης στον σωρό με τη malloc() και ανάθεση τιμής.
Σχήμα 9.2: Αποδέσμευση μνήμης (που είχε δεσμευθεί στον σωρό) με τη free().

9.1.2 Δυναμικοί πίνακες

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

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

int main(void) {
  int n;
  int *x = NULL;
  printf("Input the dimension of the array: ");
  scanf("%d", &n);
  x = malloc(sizeof(int) * n);
  for (int i = 0; i < n; i++) {
    x[i] = i + 1;
  }
  for (int i = 0; i < n; i++) {
    printf("%d ", x[i]);
  }
  free(x);
  return 0;
}

Η δημιουργία δυναμικών πινάκων γίνεται με την εντολή malloc(n * sizeof(TYPE)) όπου TYPE είναι ο επιθυμητός τύπος δεδομένων του πίνακα και n το πλήθος των στοιχείων του πίνακα. Η διαγραφή του πίνακα, όπως και στην περίπτωση των απλών μεταβλητών, γίνεται με κλήση της συνάρτησης free().
Οι δυναμικοί πίνακες έχουν τις ίδιες ιδιότητες όπως και οι στατικοί πίνακες. Για παράδειγμα μπορούν να περάσουν ως ορίσματα σε συναρτήσεις. Στο παράδειγμα του κώδικα 9.3 παρουσιάζεται η εύρεση του μέσου όρου σε δυναμικό πίνακα με χρήση βοηθητικής συνάρτησης.

Κώδικας 9.3: ch9_p3.c - υπολογισμός και εμφάνιση μέσου όρου στοιχείων δυναμικού πίνακα.
#include <stdio.h>
#include <stdlib.h>

double avg(int *x, int n) {
  double sum = 0.0;
  for (int i = 0; i < n; i++) {
    sum += x[i];
  }
  return sum / n;
}

int main(void) {
  int n;
  int *v;
  printf("Input the number of elements: ");
  scanf("%d", &n);
  v = malloc(n * sizeof(int));
  for (int i = 0; i < n; i++) {
    printf("Input value for element %d/%d: ", i + 1, n);
    scanf("%d", &v[i]);
  }
  printf("Mean value: %lf \n", avg(v, n));
  free(v);
  return 0;
}

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

Input the number of elements: 5
Input value for element 1/5: 10
Input value for element 2/5: 20
Input value for element 3/5: 30
Input value for element 4/5: 40
Input value for element 5/5: 50
Mean value: 30.000000

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

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

int main(void) {
  int n, i, element;
  int *v, *save_pointer;
  printf("Input number of elements: ");
  scanf("%d", &n);
  v = malloc(n * sizeof(int));
  save_pointer = v;
  printf("Start: pointer to the start of the array: %p\n", (void *)v);
  for (i = 0; i < n; i++) {
    printf("Input value of element %d: ", i);
    scanf("%d", &v[i]);
  }
  printf("Search for element: ");
  scanf("%d", &element);
  i = 0;
  while (i != n && *v++ != element) {
    i++;
  }
  if (i != n) {
    printf("Element %d is found at %d position \n", element, i);
  } else {
    printf("Element %d not found\n", element);
  }
  v = save_pointer;
  printf("Finish: pointer to the start of the array: %p\n", (void *)v);
  free(v);
  return 0;
}

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

Input number of elements: 5
Start: pointer to the start of the array: 0x139704080
Input value of element 0: 10
Input value of element 1: 20
Input value of element 2: 30
Input value of element 3: 40
Input value of element 4: 50
Search for element: 33
Element 33 not found
Finish: pointer to the start of the array: 0x139704080

Αν ωστόσο στη γραμμή 19 του κώδικα 9.4 δεν υπήρχε στη συνθήκη το i != n τότε η αναζήτηση θα αποτύγχανε. Αυτό θα συνέβαινε διότι αν ο χρήστης αναζητούσε τιμή που δεν υπήρχε στον πίνακα, η αναζήτηση θα συνεχίζονταν και σε στοιχεία μετά το τέλος του πίνακα. Η πιθανότερη κατάληξη θα ήταν να εμφανιστεί ένα σφάλμα χρόνου εκτέλεσης με μήνυμα segmentation fault.

9.1.3 Δημιουργία πινάκων δύο διαστάσεων

Στην περίπτωση που ο προγραμματιστής θέλει να δημιουργήσει έναν δυναμικό πίνακα δύο διαστάσεων, ακολουθείται μια διαδικασία που είναι πιο περίπλοκη, καθώς ένας πίνακας δύο διαστάσεων μπορεί να θεωρηθεί ως ένας πίνακας πινάκων ή εναλλακτικά ως ένας πίνακας δεικτών. Για παράδειγμα στον κώδικα 9.5, κατασκευάζεται ένας πίνακας που περιέχει 5 δείκτες. Κάθε δείκτης με τη σειρά του αρχικοποιείται ως ένας δυναμικός πίνακας. Στο συγκεκριμένο παράδειγμα οι πίνακες που αντιστοιχούν σε κάθε γραμμή του δισδιάστατου πίνακα έχουν διαφορετικά πλήθη στοιχείων και κάθε στοιχείο τους είναι μια ακέραια τιμή στο διάστημα [0,100).

Κώδικας 9.5: ch9_p5.c - δημιουργία ενός «πριονωτού» πίνακα.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void fill_random(int *x, int n) {
  for (int i = 0; i < n; i++) {
    x[i] = rand() % 100; // τυχαίος ακέραιος από το 0 έως το 99
  }
}

void print_array(int *x, int n) {
  for (int i = 0; i < n; i++) {
    printf("%4d", x[i]);
  }
  printf("\n");
}

int main(void) {
  int *dynamic[5];
  srand(time(NULL)); // αρχικοποίηση της συνάρτησης rand()
  // δημιουργία ενός δισδιάστατου πίνακα 5 γραμμών
  // με 5 στοιχεία στην πρώτη γραμμή, 4 στη δεύτερη κ.ο.κ.
  for (int i = 0; i < 5; i++) {
    int elements = 5 - i;
    dynamic[i] = malloc(elements * sizeof(int));
    fill_random(dynamic[i], elements);
    print_array(dynamic[i], elements);
  }
  for (int i = 0; i < 5; i++) {
    free(dynamic[i]);
  }
  return 0;
}

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

29  71  20  42  23
16  80  39  0
67  28  40
33  19
73

Καθένας από τους 5 πίνακες του παραδείγματος είναι ανεξάρτητος από τους άλλους και για αυτόν τον λόγο είναι δυνατή η κλήση των συναρτήσεων fill_random() και print_array() σε καθέναν ξεχωριστά. Ωστόσο, ένας πίνακας μπορεί να θεωρηθεί ότι είναι δείκτης και άρα ο πίνακας dynamic του παραδείγματος θα μπορούσε να θεωρηθεί ως δείκτης σε δείκτες. Επομένως αν το παράδειγμα γενικευτεί, μπορεί να κατασκευαστεί ένας πίνακας δύο διαστάσεων με χρήση διπλού δείκτη, όπως παρουσιάζεται στον κώδικα 9.6. Ο χρήστης εισάγει τον επιθυμητό αριθμό γραμμών και στηλών για έναν πίνακα δύο διαστάσεων. Ο πίνακας αυτός έχει δηλωθεί ως διπλός δείκτης ή αλλιώς δείκτης σε δείκτη με τη δήλωση double **table. Σε αυτήν την περίπτωση η δημιουργία του πίνακα δύο διαστάσεων γίνεται σε δύο βήματα. Στο πρώτο βήμα δημιουργείται ένας πίνακας δεικτών με την ανάθεση:

table=malloc(sizeof(double *) * rows);

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

table[i] = malloc(sizeof(double) * cols);

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

Κώδικας 9.6: ch9_p6.c - συμπλήρωση τιμών σε έναν δισδιάστατο πίνακα, κάθε στοιχείο λαμβάνει ως τιμή το άθροισμα των δεικτών του.
#include <stdio.h>
#include <stdlib.h>

int main(void) {
  int rows, cols;
  double **table;
  printf("Input number of rows: ");
  scanf("%d", &rows);
  printf("Input number of columns: ");
  scanf("%d", &cols);
  table = malloc(sizeof(double *) * rows);
  for (int i = 0; i < rows; i++) {
    table[i] = malloc(sizeof(double) * cols);
  }
  for (int i = 0; i < rows; i++) {
    for (int j = 0; j < cols; j++) {
      table[i][j] = i  + j ;
    }
  }
  for (int i = 0; i < rows; i++) {
    for (int j = 0; j < cols; j++) {
      printf("%.1lf\t", table[i][j]);
    }
    printf("\n");
  }
  for (int i = 0; i < rows; i++) {
    free(table[i]);
  }
  free(table);
  return 0;
}

Ακολουθεί ένα παράδειγμα εκτέλεσης του κώδικα όπου ζητείται η δημιουργία ενός πίνακα 5 γραμμών και 4 στηλών.

Input number of rows: 5
Input number of columns: 4
0.0 1.0 2.0 3.0
1.0 2.0 3.0 4.0
2.0 3.0 4.0 5.0
3.0 4.0 5.0 6.0
4.0 5.0 6.0 7.0

9.1.4 Δυναμική δημιουργία αλφαριθμητικών

Τα αλφαριθμητικά είναι πίνακες από μεταβλητές τύπου char και μπορούν να θεωρηθούν ως δείκτες προς χαρακτήρες. Αυτό έχει ως συνέπεια τη δυνατότητα κατασκευής δυναμικών αλφαριθμητικών όπως στην περίπτωση των δυναμικών πινάκων. Στο παράδειγμα του κώδικα 9.7 ο χρήστης εισάγει τον μέγιστο αριθμό χαρακτήρων ενός αλφαριθμητικού και στη συνέχεια με τη συνάρτηση malloc() δεσμεύεται στον σωρό ο χώρος όπου θα αποθηκευτεί το αντίστοιχο αλφαριθμητικό. Αξίζει να σημειωθεί πως δεσμεύεται μνήμη για n + 1 χαρακτήρες, καθώς πρέπει να αποθηκευτεί και ο χαρακτήρας τερματισμού.

Κώδικας 9.7: ch9_p7.c - δυναμική δημιουργία αλφαριθμητικού και αρχικοποίηση των επιμέρους χαρακτήρων του με τον χαρακτήρα ’Α’.
#include <stdio.h>
#include <stdlib.h>

int main(void) {
  int n;
  char *st;
  printf("Input the number of characters for a string: ");
  scanf("%d", &n);
  st = malloc((n + 1) * sizeof(char));
  for (int i = 0; i < n; i++) {
    st[i] = 'A';
  }
  st[n] = '\0';
  printf("Result: %s\n", st);
  free(st);
  return 0;
}
Input the number of characters for a string: 10
Result: AAAAAAAAAA

Ένα ακόμα παράδειγμα χρήσης δυναμικών αλφαριθμητικών παρουσιάζεται στον κώδικα 9.8. Ο χρήστης εισάγει έναν θετικό ακέραιο αριθμό και στη συνέχεια αποθηκεύεται στον πίνακα binary η δυαδική αναπαράσταση του αριθμού. Για να γίνει αυτό πρώτα γίνεται καταμέτρηση των δυαδικών ψηφίων του αριθμού με τη συνάρτηση count_binary() και στη συνέχεια δημιουργείται ο αντίστοιχος πίνακας binary με τη χρήση της malloc(). Η συνάρτηση convert() μετατρέπει τον αριθμό σε δυαδική μορφή και στη συνέχεια επειδή τα δυαδικά ψηφία έχουν αποθηκευτεί στον πίνακα binary σε αντίστροφη σειρά, γίνεται αντιστροφή του binary με τη συνάρτηση reverse().

Κώδικας 9.8: ch9_p8.c - μετατροπή ενός δεκαδικού αριθμού σε δυαδικό.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Υπολογισμός αριθμού δυαδικών ψηφίων του n
int count_binary(int n) {
  int count = 0;
  while (n != 0) {
    n = n / 2;
    count++;
  }
  return count;
}

// Μετατροπή ενός ακεραίου σε δυαδική μορφή
void convert(int n, char *b) {
  int pos = 0;
  while (n != 0) {
    b[pos++] = '0' + (n % 2);
    n = n / 2;
  }
  b[pos] = '\0';
}

// Αντιστροφή μιας συμβολοσειράς
void reverse(char *b) {
  int length = strlen(b);
  for (int i = 0; i < length / 2; i++) {
    char temp = b[i];
    b[i] = b[length - i - 1];
    b[length - i - 1] = temp;
  }
}

int main(void) {
  int number;
  char *binary;
  printf("Input a positive number: ");
  scanf("%d", &number);
  int digits = count_binary(number);
  binary = malloc((digits + 1) * sizeof(char));
  convert(number, binary);
  reverse(binary);
  printf("Binary representation of %d: %s\n", number, binary);
  free(binary);
  return 0;
}

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

Input a positive number: 45
Binary representation of 45: 101101

9.1.5 Οι συναρτήσεις calloc() και realloc()

Η συνάρτηση calloc() χρησιμοποιείται κατά κύριο λόγο για τη δημιουργία δυναμικών πινάκων και η σύνταξή της είναι:

void *calloc(size_t n, size_t size);

όπου n είναι το πλήθος των στοιχείων του νέου πίνακα και size είναι το μέγεθος σε bytes για κάθε στοιχείο του. Μια σημαντική διαφορά με τη malloc() είναι πως η συνάρτηση calloc() αρχικοποιεί στην τιμή μηδέν τα στοιχεία του δυναμικού πίνακα που δημιουργεί, κάτι που δεν κάνει η malloc(). Ένας άλλος τρόπος αρχικοποίησης ενός δυναμικού πίνακα είναι με τη συνάρτηση memset()(1). Ένα παράδειγμα χρήσης της calloc() για τη δημιουργία και χρήση ενός δυναμικού πίνακα δεκαδικών τιμών παρουσιάζεται στον κώδικα 9.9. Στο παράδειγμα αυτό αρχικά εκτυπώνονται τα στοιχεία του πίνακα που έχει αρχικοποιηθεί με την calloc() και συνεπώς έχουν όλα την τιμή μηδέν. Στη συνέχεια ανατίθεται σε κάθε στοιχείο του πίνακα ένας τυχαίος αριθμός στο διάστημα [0, 1).

  1. Η συνάρτηση void memset(void str, int c, size_t n) αντιγράφει τον χαρακτήρα c στους n πρώτους χαρακτήρες του αλφαριθμητικού στο οποίο δείχνει η παράμετρος str.
Κώδικας 9.9: ch9_p9.c - αρχικοποίηση δυναμικού πίνακα πραγματικών 10 στοιχείων με μηδενικά, συμπλήρωση των πρώτων 5 στοιχείων με τυχαίες τιμές.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void) {
  srand(time(NULL)); // αρχικοποίηση της συνάρτησης rand()
  double *d = calloc(
      10, sizeof(double)); // δέσμευση μνήμης για 10 double με αρχική τιμή 0
  for (int i = 0; i < 5; i++) {
    d[i] = rand() * 1.0 / RAND_MAX; // τυχαίος αριθμός από το 0 έως το 1
  }
  for (int i = 0; i < 10; i++) {
    printf("%.4lf ", d[i]);
  }
  free(d);
  return 0;
}

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

0.0648 0.7522 0.2825 0.2246 0.1117 0.0000 0.0000 0.0000 0.0000 0.0000

Η συνάρτηση realloc() μπορεί να χρησιμοποιηθεί για να αλλάξει το ποσό της μνήμης που έχει «δοθεί» σε έναν δείκτη, για παράδειγμα να μειώσει ή να αυξήσει το πλήθος στοιχείων ενός δυναμικού πίνακα. Η σύνταξη της συνάρτησης έχει ως εξής:

void *realloc(ptr, size_t newsize);

Στη realloc(), ptr είναι ο δείκτης για τον οποίο είναι επιθυμητό να γίνει αλλαγή μεγέθους στην ποσότητα μνήμης που δείχνει και newsize είναι το νέο μέγεθος σε bytes. Ένα παράδειγμα χρήσης παρουσιάζεται στον κώδικα 9.10, όπου ένας πίνακας μεγαλώνει επαναληπτικά μέχρι να φτάσει στο διπλάσιο του αρχικού του μεγέθους. Στο παράδειγμα αυτό ένας τυχαίος αριθμός τοποθετείται σε κάθε νέο στοιχείο που δημιουργείται στην επανάληψη do...while.

Κώδικας 9.10: ch9_p10.c - σταδιακή αύξηση μεγέθους πίνακα με τη realloc().
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ISIZE 8

void print_array(int *x, int n) {
  printf("Array of size %d: ", n);
  for (int i = 0; i < n; i++) {
    printf("%d:%d ", i, x[i]);
  }
  printf("\n");
}

int main(void) {
  srand(time(NULL)); // αρχικοποίηση της συνάρτησης rand()
  int *d = malloc(ISIZE * sizeof(int));
  int current_size = ISIZE;
  for (int i = 0; i < ISIZE; i++) {
    d[i] = rand() % 10; // τυχαίος ακέραιος από το 0 έως το 9
  }
  print_array(d, current_size);
  do {
    current_size++;
    d = realloc(d, current_size * sizeof(int));
    d[current_size - 1] = rand() % 10; // τυχαίος ακέραιος από το 0 έως το 9
    print_array(d, current_size);
  } while (current_size < 2 * ISIZE);
  free(d);
  return 0;
}
Array of size 8: 0:7 1:9 2:5 3:5 4:2 5:3 6:7 7:6
Array of size 9: 0:7 1:9 2:5 3:5 4:2 5:3 6:7 7:6 8:3
Array of size 10: 0:7 1:9 2:5 3:5 4:2 5:3 6:7 7:6 8:3 9:5
Array of size 11: 0:7 1:9 2:5 3:5 4:2 5:3 6:7 7:6 8:3 9:5 10:2
Array of size 12: 0:7 1:9 2:5 3:5 4:2 5:3 6:7 7:6 8:3 9:5 10:2 11:0
Array of size 13: 0:7 1:9 2:5 3:5 4:2 5:3 6:7 7:6 8:3 9:5 10:2 11:0 12:1
Array of size 14: 0:7 1:9 2:5 3:5 4:2 5:3 6:7 7:6 8:3 9:5 10:2 11:0 12:1 13:8
Array of size 15: 0:7 1:9 2:5 3:5 4:2 5:3 6:7 7:6 8:3 9:5 10:2 11:0 12:1 13:8 14:0
Array of size 16: 0:7 1:9 2:5 3:5 4:2 5:3 6:7 7:6 8:3 9:5 10:2 11:0 12:1 13:8 14:0 15:9

9.1.6 Δυναμικές δομές

Οι δείκτες μπορούν να χρησιμοποιηθούν και για τη δημιουργία δυναμικών δομών με παρόμοιο τρόπο όπως με τους δυναμικούς πίνακες. Στο παράδειγμα του κώδικα 9.11 παρουσιάζεται η δημιουργία μιας δυναμικής δομής που περιγράφει άτομα με πεδία όνομα, επώνυμο και ηλικία. Η δημιουργία της δυναμικής μεταβλητής γίνεται παρόμοια με την περίπτωση των δυναμικών μεταβλητών και πινάκων όπως και η διαγραφή της μνήμης με χρήση της συνάρτησης free(). Η πρόσβαση στα πεδία της δομής γίνεται με τη χρήση του τελεστή ‐>.

Κώδικας 9.11: ch9_p11.c - δυναμική δημιουργία δομής, εμφάνιση θέσεων μνήμης που καταλαμβάνουν τα επιμέρους πεδία της δομής.
#include <inttypes.h> // για τη PRIuPTR (για μορφοποίηση μεταβλητών uintptr_t)
#include <stdint.h>   // για το uintptr_t
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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

void print_details(person x) {
  printf("%s %s %d \n", x.name, x.lastname, x.age);
  printf("(HEX) Address of the struct:%p [.name:%p .lastname:%p .age:%p]\n",
         (void *)&x, (void *)&x.name, (void *)&x.lastname, (void *)&x.age);
  printf("(DEC) Address of the struct:%" PRIuPTR " [.name:%" PRIuPTR
         " .lastname:%" PRIuPTR " .age:%" PRIuPTR "]\n",
         (uintptr_t)&x, (uintptr_t)&x.name, (uintptr_t)&x.lastname,
         (uintptr_t)&x.age);
}

int main(void) {
  person *mperson;
  mperson = malloc(sizeof(person));
  strcpy(mperson->name, "John");
  strcpy(mperson->lastname, "Doe");
  mperson->age = 20;
  print_details(*mperson);
  free(mperson);
  return 0;
}

Ακολουθεί η έξοδος του προγράμματος σε μια εκτέλεσή του. Παρατηρήστε ότι η έξοδος του προγράμματος εμφανίζει τη διεύθυνση μνήμης όπου βρίσκεται η δυναμική δομή που δημιουργήθηκε καθώς και τις διευθύνσεις μνήμης των επιμέρους πεδίων της δομής. Για την εμφάνιση των διευθύνσεων μνήμης στο δεκαδικό σύστημα χρησιμοποιείται ο προσδιοριστής μορφοποίησης PRIuPTR που ορίζεται στο inttypes.h.

John Doe 20
(HEX) Address of the struct:0x16b0d6dec [.name:0x16b0d6dec .lastname:0x16b0d6e50
    ↪ .age:0x16b0d6eb4]
(DEC) Address of the struct:6091009516 [.name:6091009516 .lastname:6091009616
    ↪ .age:6091009716]

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

Κώδικας 9.12: ch9_p12.c - δομή με πεδίο (grades) που πρέπει δημιουργηθεί δυναμικά.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
  char name[100], lastname[100];
  int id;
  int lessons;
  double *grades;
} student;

double get_average(student *t) {
  double s = 0.0;
  for (int i = 0; i < t->lessons; i++) {
    s = s + t->grades[i];
  }
  return s / t->lessons;
}

void print_student(student *t) {
  printf("Id:%d name:%s %s average grade:%.2lf \n", t->id, t->name, t->lastname,
         get_average(t));
}

int main(void) {
  student *s1 = malloc(sizeof(student));
  strcpy(s1->name, "Jane");
  strcpy(s1->lastname, "Doe");
  s1->id = 100;
  s1->lessons = 3;
  s1->grades = malloc(s1->lessons * sizeof(double));
  s1->grades[0] = 5.5;
  s1->grades[1] = 7.;
  s1->grades[2] = 8.;
  print_student(s1);
  free(s1->grades);
  free(s1);
  return 0;
}

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

Id:100 name:Jane Doe average grade:6.83

9.2 Ασκήσεις

Άσκηση 1
Γράψτε ένα πρόγραμμα που να ζητά από τον χρήστη να εισάγει δεδομένα σε έναν δισδιάστατο τετραγωνικό πίνακα ακεραίων και να υπολογίζει και να εμφανίζει το άθροισμα των στοιχείων της διαγωνίου του. Για την αρχικοποίηση του πίνακα να χρησιμοποιείται η συνάρτηση calloc(). Ο χρήστης να εισάγει τη διάσταση του πίνακα και στη συνέχεια μόνο τα στοιχεία της διαγωνίου του πίνακα και το πρόγραμμα να εμφανίζει τον πίνακα και το άθροισμα της διαγωνίου.

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

int main(void) {
  int n, i, j, sum = 0;
  printf("Enter the dimension of the square matrix: ");
  scanf("%d", &n);
  int **array = calloc(n, sizeof(int *));
  for (i = 0; i < n; i++) {
    array[i] = calloc(n, sizeof(int));
  }
  printf("Enter the elements of the matrix diagonal:\n");
  for (i = 0; i < n; i++) {
    printf("Element [%d,%d]: ", i + 1, i + 1);
    scanf("%d", &(array[i][i]));
    sum += array[i][i];
  }
  printf("The matrix is:\n");
  for (i = 0; i < n; i++) {
    for (j = 0; j < n; j++) {
      printf("%d ", array[i][j]);
    }
    printf("\n");
  }
  printf("The sum of the diagonal elements is: %d\n", sum);
  for (i = 0; i < n; i++) {
    free(array[i]);
  }
  free(array);
  return 0;
}

Άσκηση 2
Γράψτε ένα πρόγραμμα που να δημιουργεί έναν πίνακα ακεραίων, διαστάσεων 𝑁 × 𝑀. Ο χρήστης να εισάγει τις διαστάσεις του πίνακα. Χρησιμοποιήστε τη συνάρτηση memset() για να αρχικοποιήσετε όλα τα στοιχεία του πίνακα με μηδενικά και εμφανίστε τον πίνακα. Ζητήστε από τον χρήστη να εισάγει έναν ακέραιο αριθμό n και έναν χαρακτήρα x. Χρησιμοποιήστε ξανά τη συνάρτηση memset() για να αντικαταστήσετε τα πρώτα n bytes του πίνακα με τον δεκαδικό κωδικό του χαρακτήρα x. Εμφανίστε ξανά τον πίνακα στην οθόνη για επίδειξη των αλλαγών. Προσπαθήστε να αιτιολογήσετε τα αποτελέσματα που εμφανίζονται.
Υπόδειξη: Διασφαλίστε ότι η τιμή που εισάγει ο χρήστης για το n είναι μικρότερη ή ίση από τον αριθμό bytes του πίνακα.

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

int main(void) {
    int n, m, num; 
    char character_value;
    printf("Enter the dimensions N (rows) and M (columns) of the array: ");
    scanf("%d %d", &n, &m);

    int total_size = n * m; // Συνολικό πλήθος στοιχείων του πίνακα.

    int *array = malloc(total_size * sizeof(int));
    if (array == NULL) {
        fprintf(stderr, "Memory allocation failed.\n");
        return 1;
    }

    memset(array, 0, total_size * sizeof(int)); // αρχικοποίηση με 0.

    printf("Array after initialization with memset:\n");
    for (int i = 0; i < total_size; i++) {
        if (i > 0 && i % m == 0) {
            printf("\n");
        }
        printf("%d ", array[i]);
    }
    printf("\n");

    printf("Enter an integer n (number of elements to replace): ");
    scanf("%d", &num);
    printf("Enter an ASCII character value for replacement: ");
    scanf(" %c", &character_value);

    if (num > 0 && num <= total_size) {
        // χρήση της memset για αντικατάσταση των num πρώτων στοιχείων.
        memset(array, character_value, num * sizeof(char));
    } else {
        printf("The number n is beyond the array's bounds.\n");
    }

    printf("Array after replacement:\n");
    for (int i = 0; i < total_size; i++) {
        if (i > 0 && i % m == 0) {
            printf("\n"); 
        }
        printf("%d ", array[i]);
    }
    printf("\n");

    free(array);
    return 0;
}

Άσκηση 3
Γράψτε ένα πρόγραμμα το οποίο θα ζητάει από τον χρήστη να καθορίσει το αρχικό μέγεθος ενός πίνακα ακεραίων. Χρησιμοποιήστε τη συνάρτηση malloc() για να δεσμεύσετε την αρχική μνήμη για τον πίνακα. Στη συνέχεια, το πρόγραμμα θα πρέπει να εισάγει στοιχεία στον πίνακα από τον χρήστη μέχρι να γεμίσει. Αφού γεμίσει ο πίνακας, ζητήστε από τον χρήστη αν επιθυμεί να προσθέσει περισσότερα στοιχεία. Εάν ο χρήστης επιθυμεί να συνεχίσει, χρησιμοποιήστε τη συνάρτηση realloc() για να επεκτείνετε τον πίνακα, αυξάνοντας το μέγεθός του κατά έναν αριθμό που θα καθορίζεται επίσης από τον χρήστη. Επαναλάβετε αυτήν τη διαδικασία μέχρι ο χρήστης να επιλέξει να μην προσθέσει περισσότερα στοιχεία. Τέλος, εμφανίστε τα στοιχεία του πίνακα και ελευθερώστε τη μνήμη.

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

int main(void) {
    int* array;
    int current_size, new_size, i;
    char choice;

    printf("Enter the initial size of the array: ");
    scanf("%d", &current_size);

    array = (int*)malloc(current_size * sizeof(int));
    if(array == NULL) {
        fprintf(stderr, "Memory allocation failed\n");
        return 1;
    }

    for(i = 0; i < current_size; i++) {
        printf("Enter element %d: ", i);
        scanf("%d", &array[i]);
    }

    do {
        printf("Do you want to add more elements? (Y/N): ");
        scanf(" %c", &choice); 

        if(choice == 'Y' || choice == 'y') {
            printf("Enter the number of additional elements: ");
            scanf("%d", &new_size);

            // επέκταση του πίνακα με χρήση της realloc.
            int* temp = realloc(array, (current_size + new_size) * sizeof(int));
            if(temp == NULL) {
                fprintf(stderr, "Reallocation failed\n");
                free(array);
                return 1;
            }
            array = temp;

            // εισαγωγή των νέων στοιχείων στο τέλος του πίνακα.
            for(i = current_size; i < current_size + new_size; i++) {
                printf("Enter element %d: ", i);
                scanf("%d", &array[i]);
            }
            current_size += new_size;
        }
    } while(choice == 'Y' || choice == 'y');

    printf("Array elements are:\n");
    for(i = 0; i < current_size; i++) {
        printf("%d ", array[i]);
    }
    printf("\n");

    free(array);
    return 0;
}

Άσκηση 4
Γράψτε ένα πρόγραμμα που να διαχειρίζεται βιβλία μιας υποθετικής βιβλιοθήκης χρησιμοποιώντας δομές και δυναμική δέσμευση μνήμης. Ειδικότερα, ορίστε μια δομή book με τα ακόλουθα πεδία: id, title, author, year. Γράψτε μια συνάρτηση που να δεσμεύει δυναμικά μνήμη για έναν πίνακα με εγγραφές τύπου book. Το μέγεθος του πίνακα να δίνεται από τον χρήστη. Υλοποιήστε επιπλέον τις ακόλουθες λειτουργίες:

  1. Προσθήκη ενός βιβλίου: Να επιτρέπει στον χρήστη να εισάγει τις πληροφορίες ενός βιβλίου στον πίνακα. Ο πίνακας βιβλίων να έχει αρχικά μέγεθος 2 και κάθε φορά που γεμίζει, με την realloc(), το μέγεθος του πίνακα να διπλασιάζεται.
  2. Εμφάνιση όλων των βιβλίων: Να εμφανίζει λίστα με τις πληροφορίες όλων των βιβλίων που βρίσκονται στον πίνακα.
  3. Έξοδος: Πριν την έξοδο να αποδεσμεύεται όλη η μνήμη που έχει δεσμευθεί κατά τη λειτουργία του προγράμματος.

Η αλληλεπίδραση του χρήστη με το πρόγραμμα θα γίνεται μέσω μενού με τις παραπάνω επιλογές.

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

typedef struct {
  int id;
  char title[100];
  char author[100];
  int year;
} book;

void add_book(book **books, int *current_count, int *max_size) {
  if (*current_count == *max_size) {
    *max_size *= 2;
    *books = realloc(*books, sizeof(book) * (*max_size));
    if (*books == NULL) {
      printf("Memory allocation failed.\n");
      exit(1);
    }
    printf("Library size doubled. New size is %d books.\n", *max_size);
  }

  printf("Enter book ID: ");
  scanf("%d", &((*books)[*current_count].id));
  printf("Enter book title: ");
  scanf(" %99s", (*books)[*current_count].title);
  printf("Enter author's name: ");
  scanf(" %99s", (*books)[*current_count].author);
  printf("Enter year of publication: ");
  scanf(" %d", &((*books)[*current_count].year));

  (*current_count)++;
}

void display_books(book *books, int current_count) {
  printf("Current books in the library (%d):\n", current_count);
  for (int i = 0; i < current_count; i++) {
    printf("ID: %d, Title: %s, Author: %s, Year: %d\n", books[i].id,
           books[i].title, books[i].author, books[i].year);
  }
}

int main(void) {
  int max_size = 2; // αρχικό μέγεθος της βιβλιοθήκης.
  int current_count = 0;
  book *books = malloc(sizeof(book) * max_size);

  if (books == NULL) {
    printf("Memory allocation failed.\n");
    return 1;
  }

  int choice;

  do {
    printf("\nLibrary Menu:\n");
    printf("1. Add a book\n");
    printf("2. Display all books\n");
    printf("3. Exit\n");
    printf("Enter your choice: ");
    scanf("%d", &choice);

    switch (choice) {
    case 1:
      add_book(&books, &current_count, &max_size);
      break;
    case 2:
      display_books(books, current_count);
      break;
    case 3:
      printf("Exiting the program.\n");
      break;
    default:
      printf("Invalid choice. Please try again.\n");
    }
  } while (choice != 3);

  free(books);
  return 0;
}