Στόχοι: μετά από αυτό το κεφάλαιο θα μπορείτε να εξηγείτε τι είναι το endianness και να το διαπιστώνετε με έναν
char *· να ξεχωρίζετε τη στοίβα από τον σωρό και να λέτε τι αποθηκεύεται στην καθεμία· να σχεδιάζετε τα stack frames μιας σειράς κλήσεων συναρτήσεων· να εξηγείτε γιατί η ατέρμονη αναδρομή και οι τεράστιοι τοπικοί πίνακες δίνουνSegmentation fault· και να δεσμεύετε, να μεγαλώνετε και να αποδεσμεύετε μνήμη στον σωρό μεmalloc,calloc,reallocκαιfree, και για δισδιάστατους πίνακες.Προαπαιτούμενα: Κεφάλαιο 3, Κεφάλαιο 11, Κεφάλαιο 12
Χρόνος μελέτης: ~3 ώρες
Μέχρι τώρα οι μεταβλητές μας «απλώς υπήρχαν»: τις δηλώναμε και ο μεταγλωττιστής
φρόντιζε για τη μνήμη τους. Η διάλεξη αυτή ανοίγει το καπό και δείχνει πού ακριβώς
ζουν τα δεδομένα ενός προγράμματος C. Ξεκινά με το endianness, δηλαδή τη σειρά των
bytes ενός ακεραίου, και περνά στις κατηγορίες μνήμης: τη στοίβα (stack), όπου κάθε
κλήση συνάρτησης παίρνει το δικό της stack frame, και τον σωρό (heap), από τον οποίο
δεσμεύουμε μνήμη όσο τρέχει το πρόγραμμα. Βλέπουμε γιατί η στοίβα έχει όριο (και
γιατί μια αναδρομή χωρίς τέλος «σκάει»), και μαθαίνουμε όλο τον κύκλο ζωής της
δυναμικής μνήμης: malloc/calloc, έλεγχος για NULL, realloc και free. Η
διαχείριση μνήμης είναι η βάση για τις δυναμικές δομές δεδομένων (λίστες, δέντρα)
του δεύτερου μισού του μαθήματος και απαραίτητη για την Εργασία #1 και την εξέταση.
Ένας int πιάνει πολλά bytes (συνήθως 4), οπότε πρέπει να αποφασιστεί με ποια
σειρά μπαίνουν στη μνήμη. Endianness λέμε αυτόν τον τρόπο αποθήκευσης: στο
little endian τα bytes αποθηκεύονται από το μικρότερο (λιγότερο σημαντικό) προς το
μεγαλύτερο, στο big endian από το μεγαλύτερο προς το μικρότερο.
Αν στη μνήμη υπάρχουν διαδοχικά τα bytes 68 65 6c 6c (ASCII 'h' 'e' 'l' 'l') και
τα διαβάσουμε ως έναν ακέραιο:
| Μηχάνημα | byte +0 | byte +1 | byte +2 | byte +3 | Ακέραιος |
|---|---|---|---|---|---|
| little endian | 68 |
65 |
6c |
6c |
0x6c6c6568 |
| big endian | 68 |
65 |
6c |
6c |
0x68656c6c |
Στο little endian το πρώτο byte στη μνήμη είναι το μικρότερο byte του αριθμού. Οι υπολογιστές x86 που χρησιμοποιούμε είναι little endian· το επιβεβαιώνουμε στο παράδειγμα «Βλέποντας τα bytes του 42». Το θέμα το είδατε πρώτη φορά στο Κεφάλαιο 12.
Υπενθύμιση: η μνήμη είναι μια σειρά από \(N\) bytes αριθμημένα από 0 έως \(N-1\), και κάθε byte έχει 8 bits. Το μέγεθός της μετράμε σε bytes: 1 KB (KiloByte) = 1.000 bytes, 1 MB (MegaByte) = 1.000.000 bytes, 1 GB (GigaByte) = 1.000.000.000 bytes. Ο αριθμός ενός byte είναι η διεύθυνσή του (address), αυτή που κρατά ένας pointer.
Ένα πρόγραμμα C χρησιμοποιεί τρεις κατηγορίες μνήμης:
malloc·Η διάλεξη καλύπτει τις δύο πρώτες και αφήνει την τρίτη για αργότερα. Σύμφωνα με τις
σημειώσεις, εκεί φυλάσσονται οι εξωτερικές (καθολικές) μεταβλητές και οι τοπικές
static, θέμα που συνδέεται με την εμβέλεια (Κεφάλαιο 14).
Η στοίβα είναι μια συνεχόμενη περιοχή της μνήμης όπου προστίθενται και αφαιρούνται στοιχεία με σειρά Last-In-First-Out (LIFO): το τελευταίο στοιχείο που προστέθηκε είναι το πρώτο που θα αφαιρεθεί, όπως σε μια στοίβα από πιάτα. Δεν μπορούμε να βγάλουμε ένα πιάτο από τη μέση.
Στο σχήμα των διαφανειών η στοίβα ξεκινά από το τέλος της μνήμης (byte 32000) και
μεγαλώνει προς τις μικρότερες διευθύνσεις. Μετά το
char a = 61; char b = 62; char c = 63; και, λίγο αργότερα, το char d = 64;:
| Διεύθυνση | Περιεχόμενο | Μεταβλητή |
|---|---|---|
| 31996 | (ελεύθερο) | |
| 31997 | 64 | d (μπήκε τελευταία) |
| 31998 | 63 | c |
| 31999 | 62 | b |
| 32000 | 61 | a (μπήκε πρώτη) |
Όταν τα d και c πάψουν να χρειάζονται, φεύγουν με την αντίστροφη σειρά: πρώτα
το d, μετά το c, και μένουν τα a, b. Η στοίβα λοιπόν μεγαλώνει και μικραίνει
μόνο από την κορυφή της, και γι’ αυτό η διαχείρισή της είναι απλή και γρήγορη.
Σε κάθε κλήση μιας συνάρτησης μπαίνουν στη στοίβα:
Ο χώρος αυτός δεσμεύεται σε κάθε κλήση και λέγεται διάγραμμα ενεργοποίησης (activation record ή stack frame) της συνάρτησης. Για τη συνάρτηση
int equalIgnoreCase(char char1, char char2) {
char lowerChar1 = tolower(char1);
char lowerChar2 = tolower(char2);
return lowerChar1 == lowerChar2;
}
το frame περιέχει (από κάτω προς τα πάνω) τα char1, char2, ένα προσωρινό
CompTmp1, τα lowerChar1, lowerChar2 και ένα προσωρινό CompTmp2. Τα ονόματα
CompTmp είναι σχηματικά· το τι ακριβώς κρατά ο μεταγλωττιστής εκεί δεν μας
απασχολεί.
Όταν η equalIgnoreCase καλεί την tolower, ένα νέο frame μπαίνει πάνω από το
δικό της, με το όρισμα c και τα προσωρινά της (CompTmp3–CompTmp5). Όταν εκτελεστεί η return της
tolower, το frame της αφαιρείται ολόκληρο, και η εκτέλεση συνεχίζει στην
equalIgnoreCase. Η δεύτερη κλήση tolower(char2) φτιάχνει ξανά ένα καινούργιο frame
στην ίδια θέση, και η return της equalIgnoreCase αφαιρεί και το δικό της. Έτσι οι
τοπικές μεταβλητές μιας συνάρτησης υπάρχουν μόνο όσο εκτελείται η κλήση της.
flowchart TD
A["Κλήση equalIgnoreCase: push frame<br/>char1, char2, lowerChar1, lowerChar2"]
A --> B["tolower(char1): push frame (c)"]
B --> C["return c: pop frame της tolower"]
C --> D["tolower(char2): push νέο frame (c)"]
D --> E["return c: pop frame της tolower"]
E --> F["return lowerChar1 == lowerChar2:<br/>pop frame της equalIgnoreCase"]
Σχήμα: η ζωή των stack frames κατά την εκτέλεση της equalIgnoreCase.
Στο Κεφάλαιο 11 είπαμε ότι η αναδρομή πρέπει να έχει
βάση. Τώρα φαίνεται το γιατί: κάθε αναδρομική κλήση προσθέτει ένα νέο frame στη
στοίβα, και τα frames αφαιρούνται μόνο όταν οι κλήσεις επιστρέψουν. Μια αναδρομή
χωρίς τέλος γεμίζει τη στοίβα μέχρι να ξεπεράσει το όριό της, και το λειτουργικό
σύστημα τερματίζει το πρόγραμμα με Segmentation fault (αυτό λέγεται και stack
overflow). Ισχύει και το αντίστροφο: επειδή κάθε κλήση έχει το δικό της frame, οι
μεταβλητές διαφορετικών κλήσεων της ίδιας αναδρομικής συνάρτησης δεν μπερδεύονται.
Στα περισσότερα συστήματα η στοίβα περιορίζεται σε μερικά megabytes (MBs), επειδή δεν
περιμένουμε εκατομμύρια εμφωλευμένες κλήσεις ή πολύ μεγάλα τοπικά δεδομένα. Το όριο
το βλέπουμε με την εντολή ulimit -s:
$ ulimit -s
8192
Η τιμή είναι σε KB, δηλαδή 8 MB. Ένας τοπικός πίνακας μεγαλύτερος από αυτό (π.χ.
char bomb[9000000];, 9 MB) δεν χωρά στη στοίβα και το πρόγραμμα κρασάρει πριν
καν τυπώσει κάτι. Με ulimit -s unlimited το όριο αίρεται, αλλά είναι γενικά κακή
πρακτική το πρόγραμμά μας να στηρίζεται σε unlimited stack: στον υπολογιστή που θα
το τρέξει κάποιος άλλος (ή ο αυτόματος βαθμολογητής) το όριο θα είναι το συνηθισμένο.
Για μεγάλους πίνακες η λύση είναι ο σωρός.
Ο σωρός είναι ένα σύνολο από τοποθεσίες μνήμης σε τυχαία σειρά, σαν ένας σωρός ρούχα: τα κομμάτια του δεν δεσμεύονται και δεν ελευθερώνονται με σειρά LIFO, αλλά όποτε και όπου τα ζητήσουμε. Σε αντίθεση με τη στοίβα, ο σωρός μπορεί να δεσμεύσει ολόκληρη τη διαθέσιμη μνήμη, άρα εκεί χωρούν οι μεγάλοι πίνακες. Το τίμημα είναι ότι τη μνήμη του σωρού τη διαχειριζόμαστε εμείς: τη ζητάμε ρητά και την επιστρέφουμε ρητά.
| Στοίβα (stack) | Σωρός (heap) | |
|---|---|---|
| Τι κρατά | τοπικές μεταβλητές, ορίσματα, προσωρινά | ό,τι δεσμεύουμε με malloc |
| Οργάνωση | συνεχόμενη, LIFO | τοποθεσίες σε τυχαία σειρά |
| Μέγεθος | λίγα MB (ulimit -s) |
έως όλη τη διαθέσιμη μνήμη |
| Ποιος αποδεσμεύει | αυτόματα, με την return |
εμείς, με free |
Με τη βοήθεια των pointers φτιάχνουμε δυναμικούς πίνακες: πίνακες των οποίων το μέγεθος καθορίζεται την ώρα που τρέχει το πρόγραμμα. Η γενική μορφή είναι
τύπος * όνομα = malloc(μέγεθος * sizeof(τύπος));
τύπος * όνομα: δήλωση pointer σε δεδομένα τύπος, ο τύπος κάθε στοιχείου.malloc (από το stdlib.h): δεσμεύει μνήμη στον σωρό και επιστρέφει τη
διεύθυνση όπου θα βάλουμε τα στοιχεία.μέγεθος * sizeof(τύπος): ο αριθμός των bytes που πρέπει να δεσμευτούν για να
χωρέσουν μέγεθος στοιχεία.Μετά το int * array = malloc(4 * sizeof(int)); ο pointer array ζει στη στοίβα
(είναι τοπική μεταβλητή) και δείχνει σε 4 συνεχόμενους ακεραίους στον σωρό, τα
array[0] έως array[3]. Με N αντί για 4 έχουμε πίνακα N ακεραίων, τα
array[0] έως array[N - 1].
Οι τιμές 1, 3, 3, 7 που δείχνουν οι διαφάνειες μέσα στο μπλοκ είναι ενδεικτικές: η
malloc δεν αρχικοποιεί τη μνήμη. Και το sizeof του pointer δίνει το μέγεθος
μιας διεύθυνσης (8 bytes σε σύστημα 64 bit), όχι του μπλοκ, οπότε το πλήθος των
στοιχείων το κρατάμε σε δική μας μεταβλητή.
Ο τύπος void («κενό») δηλώνει το κενό σύνολο, έναν τύπο που δεν μπορεί να έχει
τιμές. Μια συνάρτηση με τύπο επιστροφής void δεν επιστρέφει τίποτα (η return;
απλώς τερματίζει). Δήλωση μεταβλητής void a; δεν επιτρέπεται.
Ο τύπος void * («δείκτης σε κενό») είναι ένας pointer σε «κάτι», δηλαδή απλώς
μια διεύθυνση χωρίς πληροφορία για το τι βρίσκεται εκεί. Γι’ αυτό τον επιστρέφουν οι
συναρτήσεις δέσμευσης:
void *malloc(size_t size);
void *calloc(size_t nmemb, size_t size);
Το αποτέλεσμα ανατίθεται σε int *, double *, char *, ανάλογα με τη χρήση· όλοι
οι pointers έχουν το ίδιο μέγεθος. Στη C η μετατροπή από void * γίνεται αυτόματα,
οπότε cast ((int *) malloc(...)) δεν χρειάζεται. Η calloc κάνει ό,τι και η
malloc για nmemb στοιχεία μεγέθους size το καθένα, αλλά επιπλέον μηδενίζει
όλη τη μνήμη πριν επιστρέψει τον pointer.
Αν δεν υπάρχει αρκετή μνήμη, η malloc επιστρέφει τον NULL pointer. Αν
συνεχίσουμε σαν να μη συνέβη τίποτα, η πρώτη εγγραφή στον «πίνακα» γίνεται στη
διεύθυνση NULL και το πρόγραμμα κρασάρει με Segmentation fault. Γι’ αυτό μετά από
κάθε malloc (και calloc, realloc) ελέγχουμε την τιμή επιστροφής:
int * array = malloc(4 * sizeof(int));
if (!array) { // ισοδύναμα: if (array == NULL)
fprintf(stderr, "array allocation failed\n");
exit(1);
}
Το μήνυμα λάθους πάει στο stderr και η exit(1) (από το stdlib.h) τερματίζει το
πρόγραμμα με κωδικό αποτυχίας. Εναλλακτικά, η perror("array allocation") τυπώνει
στο stderr το μήνυμά μας μαζί με την περιγραφή του λάθους του συστήματος. Σε μια
συνάρτηση που δεν θέλει να τερματίσει όλο το πρόγραμμα, ο χειρισμός («fail
gracefully») μπορεί να είναι μια return με κωδικό λάθους.
Η μνήμη που δεσμεύτηκε με malloc/calloc απελευθερώνεται με τη συνάρτηση free
(πάλι από το stdlib.h):
void free(void *ptr);
Ως μόνο όρισμα παίρνει τον pointer στη μνήμη που δεσμεύτηκε αρχικά. Φροντίζουμε
κάθε κλήση malloc να συνοδεύεται από μία free. Αν δεν απελευθερώσουμε μνήμη
που δεν χρειαζόμαστε πια, έχουμε διαρροή μνήμης (memory leak): το πρόγραμμα
κρατά όλο και περισσότερη μνήμη που δεν μπορεί να ξαναχρησιμοποιήσει.
Μετά το free(array); τα 4 κελιά του σωρού επιστρέφουν στο σύστημα, αλλά ο pointer
array στη στοίβα εξακολουθεί να κρατά την ίδια διεύθυνση: δείχνει πλέον σε
μνήμη που δεν είναι δική μας (dangling pointer). Δύο πράγματα απαγορεύονται:
array[0] = 4; μετά το
free(array);·free(array);.Και στις δύο περιπτώσεις, στην καλύτερη το πρόγραμμα θα κρασάρει, και στη χειρότερη μπορεί να έχουμε security vulnerability, δηλαδή κενό ασφαλείας που εκμεταλλεύεται κάποιος επιτιθέμενος. Το χειρότερο είναι ότι συχνά το πρόγραμμα φαίνεται να δουλεύει.
Όσο τρέχει το πρόγραμμα μπορεί να χρειαστούμε περισσότερη (ή λιγότερη) μνήμη. Με την
realloc προσπαθούμε να αλλάξουμε το μέγεθος ενός δυναμικού πίνακα:
array = realloc(array, 8192 * sizeof(int)); // από 4 σε 8192 ακεραίους
if (!array) {...}
realloc μπορεί να αλλάξει τη διεύθυνση του μπλοκ μέσα στον σωρό (αν δεν
χωρά να μεγαλώσει εκεί που είναι, το αντιγράφει αλλού). Γι’ αυτό αναθέτουμε το
αποτέλεσμα ξανά στον pointer και δεν κρατάμε αντίγραφα της παλιάς διεύθυνσης.NULL αποτέλεσμα.Στο τέλος, μία free(array); αποδεσμεύει το (νέο) μπλοκ.
Για έναν πίνακα ακεραίων M × N με διαστάσεις γνωστές μόνο την ώρα της εκτέλεσης,
δεσμεύουμε πρώτα έναν πίνακα από M pointers (int *), έναν για κάθε γραμμή, και
μετά, για κάθε γραμμή, έναν πίνακα από N ακεραίους:
int ** array = malloc(M * sizeof(int*));
for (int i = 0; i < M; i++)
array[i] = malloc(N * sizeof(int)); // + έλεγχος για NULL σε κάθε κλήση
Το array είναι int ** (δείκτης σε δείκτη, Κεφάλαιο 12).
Το array[i] είναι ο pointer της γραμμής i και το array[i][j] το στοιχείο της
στήλης j σε αυτήν, ακριβώς όπως σε έναν στατικό δισδιάστατο πίνακα. Η διαφορά είναι
ότι οι γραμμές δεν είναι πια συνεχόμενες στη μνήμη: η καθεμία είναι ένα
ξεχωριστό μπλοκ στον σωρό.
flowchart LR
A["array (int **)"] --> P["array[0] | array[1] | array[2] | array[3] | array[4]"]
P -- "array[0]" --> R0["1 | 2 | 3 | 4 | 5 | 6"]
P -- "array[1]" --> R1["7 | 8 | 9 | 10 | 11 | 12"]
P -- "array[2]" --> R2["13 | 14 | 15 | 16 | 17 | 18"]
P -- "array[3], array[4]" --> R3["..."]
Σχήμα: δυναμικός πίνακας με M = 5, N = 6· το array[1][3] είναι 10 και το
array[2][2] είναι 15.
Η αποδέσμευση γίνεται με την αντίστροφη σειρά: πρώτα οι υποπίνακες (οι
γραμμές) και μετά ο πίνακας των pointers. Αν κάνουμε πρώτα free(array), δεν
μπορούμε πια να διαβάσουμε τα array[i] για να τα αποδεσμεύσουμε (θα ήταν use after
free).
for (int i = 0; i < M; i++)
free(array[i]);
free(array);
Εφαρμόζει το «Endianness». Τι θα τυπώσει το παρακάτω;
#include <stdio.h>
int main() {
int x = 42;
char * bytes = (char*)&x;
int i;
for(i = 0; i < sizeof(int) / sizeof(char); i++)
printf("%02x\n", bytes[i]);
return 0;
}
$ ./int
2a
00
00
00
Ο pointer bytes δείχνει στο πρώτο byte του x, και επειδή sizeof(char) == 1
προχωρά ένα byte τη φορά. Το 42 είναι 0x0000002a· πρώτο στη μνήμη βρίσκεται το
λιγότερο σημαντικό byte 2a, άρα ο υπολογιστής είναι little endian (σε big endian θα
βλέπαμε 00 00 00 2a).
Εφαρμόζει το «Τι αποθηκεύεται στη στοίβα». Οι διαφάνειες γράφουν μόνες τους μια
tolower (αντί για αυτή του ctype.h) για να φαίνεται και το frame της. Ακολουθήστε
την εκτέλεση και σχεδιάστε τη στοίβα σε κάθε βήμα (το gcc ίσως προειδοποιήσει
για σύγκρουση με την ενσωματωμένη tolower):
#include <stdio.h>
char tolower(char c) {
if (c >= 'A' && c <= 'Z')
return c + ('a' - 'A');
return c;
}
int equalIgnoreCase(char char1, char char2) {
char lowerChar1 = tolower(char1);
char lowerChar2 = tolower(char2);
return lowerChar1 == lowerChar2;
}
int main() {
printf("%d\n", equalIgnoreCase('Q', 'q'));
return 0;
}
Το πρόγραμμα τυπώνει 1. Η στοίβα σε κάθε βήμα είναι αυτή του σχήματος της
«Θεωρίας».
Εφαρμόζει το «Γιατί η αναδρομή πρέπει να τελειώνει».
void recurse() {
recurse();
}
int main() {
recurse();
return 0;
}
$ gcc -o rec rec.c
$ ./rec
Segmentation fault
Κάθε κλήση της recurse βάζει ένα frame στη στοίβα και καμία δεν επιστρέφει, οπότε η
στοίβα ξεπερνά τα 8 MB της.
Εφαρμόζει το «Μέγεθος της στοίβας». Τι θα κάνει το ακόλουθο πρόγραμμα;
#include <stdio.h>
int main() {
char bomb[9000000];
printf("Hello World\n");
return 0;
}
$ gcc -o hello hello.c
$ ulimit -s
8192
$ ./hello
Segmentation fault
$ ulimit -s unlimited
$ ulimit -s
unlimited
$ ./hello
Hello World
Ο τοπικός πίνακας των 9 MB δεν χωρά στα 8 MB της στοίβας, γι’ αυτό δεν τυπώνεται
καν το Hello World. Με unlimited stack δουλεύει, αλλά όπως είπαμε δεν
στηριζόμαστε σε αυτό.
Εφαρμόζει τον «Σωρό» και τον «Έλεγχο της malloc». Με
char *bomb = malloc(sizeof(char) * 9000000); στη θέση του τοπικού πίνακα το
πρόγραμμα τυπώνει Hello World: τα 9 MB στον σωρό δεν είναι πρόβλημα. Αν ζητήσουμε
900000000000L bytes (900 GB, περισσότερα από τη μνήμη του υπολογιστή), πάλι
τυπώνει Hello World: η malloc αποτυγχάνει ήσυχα και επιστρέφει NULL, που απλώς
δεν το χρησιμοποιούμε. Μόλις όμως προσθέσουμε bomb[0] = 'A';, το πρόγραμμα γράφει
στη διεύθυνση NULL:
$ ./heap
Segmentation fault
Η σωστή εκδοχή ελέγχει το αποτέλεσμα και βγαίνει με κατανοητό μήνυμα:
#include <stdio.h>
#include <stdlib.h>
int main() {
char *bomb = malloc(sizeof(char) * 900000000000L);
if (!bomb) {
fprintf(stderr, "array allocation failed\n");
exit(1);
}
bomb[0] = 'a';
printf("Hello World\n");
free(bomb);
return 0;
}
(Οι διαφάνειες παραλείπουν το free(bomb);· εδώ το προσθέσαμε, αφού κάθε malloc
συνοδεύεται από μία free.)
Εφαρμόζει τους «Δυναμικούς δισδιάστατους πίνακες», με τις perror της σελ. 64.
Γεμίζουμε τον πίνακα με 1, 2, 3, … όπως στο σχήμα και τυπώνουμε τα δύο στοιχεία που
σημειώνει η διάλεξη:
#include <stdio.h>
#include <stdlib.h>
int main() {
int M = 5, N = 6;
int ** array = malloc(M * sizeof(int*));
if (!array) {
perror("array allocation");
exit(1);
}
for (int i = 0; i < M; i++) {
array[i] = malloc(N * sizeof(int));
if (!array[i]) {
perror("array[i] failed");
exit(1);
}
for (int j = 0; j < N; j++)
array[i][j] = i * N + j + 1;
}
printf("%d %d\n", array[1][3], array[2][2]);
for (int i = 0; i < M; i++)
free(array[i]);
free(array);
return 0;
}
$ ./dyn2d
10 15
Αυτό ακριβώς χρειάζεστε στην άσκηση mines.c του εργαστηρίου 7, όπου οι διαστάσεις
διαβάζονται από την είσοδο. Στην τελική εξέταση θα χρειαστεί να διαχειριστείτε
δεδομένα που το μέγεθός τους δεν ξέρετε εκ των προτέρων.
Το κουίζ στο τέλος της διάλεξης δείχνει τρεις διαδοχικές εκδοχές μιας void bar()
που φτιάχνει δυναμικά πίνακα 5 × 5 (δείτε την άσκηση «Κουίζ: πίνακας 5x5 σε
συνάρτηση»). Η πρώτη δεν ελέγχει για NULL και δεν αποδεσμεύει τίποτα. Η δεύτερη
ελέγχει κάθε malloc και κάνει return; σε αποτυχία. Η τρίτη προσθέτει στο τέλος
τις free με τη σωστή σειρά. Ακόμη και η τρίτη έχει ένα κενό: αν αποτύχει η
malloc της γραμμής 3, η return; αφήνει δεσμευμένα το array και τις γραμμές 0–2.
Κάθε έξοδος από μια συνάρτηση πρέπει να αποδεσμεύει ό,τι έχει δεσμευτεί ως εκείνη
τη στιγμή.
Segmentation fault.ulimit -s, συνήθως 8192 KB)· μεγάλοι τοπικοί πίνακες
δεν χωρούν, και είναι κακή πρακτική να στηριζόμαστε σε ulimit -s unlimited.τύπος * όνομα = malloc(μέγεθος * sizeof(τύπος)); φτιάχνει στον σωρό πίνακα με
μέγεθος που αποφασίζεται κατά την εκτέλεση· χρειάζεται #include <stdlib.h>.malloc/calloc επιστρέφουν void *, μια διεύθυνση που ανατίθεται σε pointer
οποιουδήποτε τύπου· η calloc επιπλέον μηδενίζει τη μνήμη.malloc/calloc/realloc για NULL, αλλιώς η
πρώτη χρήση δίνει Segmentation fault.malloc συνοδεύεται από μία free· μνήμη που δεν αποδεσμεύεται είναι
διαρροή μνήμης (memory leak).free και η διπλή free απαγορεύονται: στην καλύτερη
κρασάρουν, στη χειρότερη ανοίγουν κενό ασφαλείας.realloc αλλάζει το μέγεθος ενός δυναμικού πίνακα, διατηρεί τα περιεχόμενα
και μπορεί να αλλάξει τη διεύθυνσή του.M × N είναι ένας int ** προς M pointers γραμμών, με
κάθε γραμμή από δική της malloc· αποδεσμεύουμε πρώτα τις γραμμές και μετά τον
πίνακα των pointers.| Ελληνικά | English | Σύντομος ορισμός |
|---|---|---|
| σειρά bytes | endianness | Η σειρά αποθήκευσης των bytes ενός ακεραίου. |
| στοίβα | stack | Συνεχόμενη μνήμη LIFO για τοπικές μεταβλητές και ορίσματα. |
| διάγραμμα ενεργοποίησης | activation record / stack frame | Ο χώρος στη στοίβα για μία κλήση συνάρτησης. |
| υπερχείλιση στοίβας | stack overflow | Η στοίβα ξεπερνά το όριό της, π.χ. από ατέρμονη αναδρομή. |
| σωρός | heap | Μνήμη που δεσμεύουμε και αποδεσμεύουμε ρητά. |
| παγκόσμια / στατική μνήμη | global / static memory | Μνήμη για μεταβλητές που ζουν όσο το πρόγραμμα. |
| δυναμικός πίνακας | dynamic array | Πίνακας με μέγεθος που αποφασίζεται κατά την εκτέλεση. |
| κενός τύπος | void |
Τύπος χωρίς τιμές· π.χ. συνάρτηση που δεν επιστρέφει τίποτα. |
| δείκτης σε κενό | void * |
Pointer σε «κάτι»: σκέτη διεύθυνση. |
| κενός δείκτης | null pointer (NULL) |
Pointer που δεν δείχνει πουθενά· η malloc τον δίνει σε αποτυχία. |
| διαρροή μνήμης | memory leak | Μνήμη που δεσμεύτηκε και δεν αποδεσμεύτηκε ποτέ. |
| χρήση μετά την αποδέσμευση | use after free | Πρόσβαση σε μνήμη μετά την free της. |
| διπλή αποδέσμευση | double free | Δεύτερη free στον ίδιο pointer. |
malloc
σελ. 32–40· void και void * σελ. 41–42· έλεγχος για NULL σελ. 43–46· free
σελ. 47–51· realloc σελ. 52–55· δυναμικοί δισδιάστατοι πίνακες σελ. 56–58 και 64·
κουίζ σελ. 61–63.int **).array.c, mines.c, και το παράρτημα «Σφάλματα διαχείρισης μνήμης» με
την άσκηση my_prog.c (εντοπισμός σφάλματος μνήμης με τον gdb).man 3 malloc (καλύπτει malloc, calloc, realloc, free).char bomb[9000000]; ή ένα int grid[5000][5000];
μέσα σε συνάρτηση δίνει Segmentation fault πριν την πρώτη εντολή. Δεσμεύστε τον
με malloc.Segmentation fault
μετά από πολλές κλήσεις. Ελέγξτε ότι κάθε κλήση πλησιάζει τη βάση.NULL. Όταν η malloc αποτύχει, το bomb[0] = 'A'; κρασάρει.
Γράφετε πάντα if (!p) { ... } αμέσως μετά.#include <stdlib.h>. Ο gcc λέει
implicit declaration of function 'malloc' (ή 'exit', 'free').sizeof(int) αντί για sizeof(int*) στον πίνακα των γραμμών. Σε σύστημα 64 bit
ένας pointer είναι 8 bytes και ένας int 4, οπότε δεσμεύεται η μισή μνήμη. Για
int ** γράφετε malloc(M * sizeof(int*)).malloc χωρίς αρχικοποίηση. Τυπώνονται «σκουπίδια».
Αρχικοποιήστε, ή χρησιμοποιήστε calloc αν θέλετε μηδενικά.malloc χωρίς αντίστοιχη free, ή μια πρόωρη return που
προσπερνά τις free (όπως στο κουίζ). Ελέγξτε κάθε δρόμο εξόδου.free ή διπλή free. Η glibc συχνά σταματά το πρόγραμμα με
free(): double free detected ή double free or corruption· άλλες φορές το
πρόγραμμα «δουλεύει» τυχαία. Μετά την free μην αγγίζετε τον pointer.free(array); πριν από τα
free(array[i]); διαβάζει αποδεσμευμένη μνήμη. Πρώτα οι γραμμές, μετά ο πίνακας.realloc. Ένας δεύτερος pointer που κρατούσε την παλιά
διεύθυνση μπορεί να δείχνει πλέον σε αποδεσμευμένη μνήμη. Χρησιμοποιείτε μόνο ό,τι
επέστρεψε η realloc. Επιπλέον, αν η realloc αποτύχει επιστρέφει NULL αλλά το
παλιό μπλοκ μένει δεσμευμένο, οπότε το array = realloc(array, ...) το «χάνει»· σε
πρόγραμμα που δεν τερματίζει αμέσως, κρατήστε το αποτέλεσμα πρώτα σε προσωρινό
pointer.void a;. Δεν μεταγλωττίζεται (variable or field 'a' declared void): void
δεν είναι τύπος μεταβλητής, μόνο void * είναι.Από τα Kahoot των διαλέξεων: οι ερωτήσεις όπου μια λάθος απάντηση μάζεψε πολλές ψήφους, με το ποσοστό σωστών απαντήσεων.
~8, θεωρώντας ότι το 8192 είναι bytes. Το ulimit -s μετράει σε KB, άρα η στοίβα είναι 8 MB.free. Η free είναι μόνο για μνήμη του σωρού· οι τοπικές μεταβλητές αποδεσμεύονται αυτόματα όταν επιστρέφει η συνάρτηση.char bomb[9000000]; κρασάρει ως τοπική μεταβλητή, ενώ ο ίδιος πίνακας
με malloc όχι;[^q3]free(array); τι τιμή έχει ο array;[^q5]realloc αναθέτουμε το αποτέλεσμα ξανά στον pointer;[^q6]malloc και πόσες free χρειάζεται ένας δυναμικός πίνακας
M × N με int **;[^q7]Ερωτήσεις που παίχτηκαν στις διαλέξεις, με το ποσοστό των φοιτητών που απάντησαν σωστά.
slides-lec13-after-freeslides-lec13-infinite-recursionslides-lec13-stack-framesslides-lec13-dynamic-2dslides-lec13-heap-bombslides-lec13-quiz-5x5slides-lec13-stack-bomblab-lab07-mineslab-lab07-pacmanexam-2023-fall-ex1-q4exam-2023-fall-ex9-q4exam-2026-jan-q6exam-2023-fall-ex1-q3exam-2023-fall-ex13-q2exam-2023-fall-ex8-q3exam-2023-fall-ex9-q3hw-2023-hw2-fauxtoshoplab-lab07-arrayslides-lec12-endiannessslides-lec12-malloc-sizeofexam-2025-jan-q2exam-2025-jan-q5exam-2023-fall-ex7-q3exam-2025-jan-q6lab-lab07-olafslides-lec16-heap-2d-arrayexam-2026-jan-q2exam-2023-fall-ex0-q3exam-2023-fall-ex10-q3exam-2023-fall-ex2-q3exam-2023-fall-ex6-q4exam-2023-fall-ex8-q4hw-2024-hw2-futurelab-lab09-personexam-2024-sep-q5exam-2025-sep-q4exam-2026-sep-q4hw-2023-hw3-zoombaexam-2023-dec-q4exam-2024-dec-q3exam-2026-sep-q5Last-In-First-Out: βγαίνει πρώτο ό,τι μπήκε τελευταίο. Η συνάρτηση που
κλήθηκε τελευταία είναι η πρώτη που επιστρέφει.
[^q2]: Οι τοπικές μεταβλητές, τα ορίσματα και προσωρινά δεδομένα του μεταγλωττιστή.
[^q3]: Η στοίβα είναι περιορισμένη (συνήθως 8 MB) ενώ ο σωρός μπορεί να δεσμεύσει
όλη τη διαθέσιμη μνήμη.
[^q4]: Μνήμη που δεσμεύτηκε και δεν αποδεσμεύτηκε· την αποφεύγουμε με μία free για
κάθε malloc, σε κάθε δρόμο εξόδου.
[^q5]: Την ίδια διεύθυνση με πριν, που όμως δεν είναι πια δική μας (dangling
pointer).
[^q6]: Γιατί η realloc μπορεί να μετακινήσει το μπλοκ σε άλλη διεύθυνση.
[^q7]: \(M + 1\) κλήσεις malloc και \(M + 1\) κλήσεις free. ↩