Επικοινωνήστε με: Παναγιώτη Παύλου
e-mail: allos@mail.ntua.gr
Έστω ότι έχουμε μια ακέραια μεταβλητή x των 16bit. Αυτή αποθηκεύεται σε δύο θέσεις μνήμης.
Επειδή οι θέσεις μνήμης χωράνε 1 Byte η καθεμία και είναι αριθμημένες βλέπουμε ότι
η μεταβλητή είναι αποθηκευμένη στη θέση 0x1002
(αυτή είναι η πρώτη από
τις θέσεις που χρησιμοποιούνται). Με κώδικα αυτή την τιμή μπορούμε να την πάρουμε
με τον unary τελεστή &
πχ &x
#include <stdio.h>
int main()
{
short int x = 0x48F0;
printf("x = %0x\n&x = %0x\n", x, &x);
return 0;
}
Run online
Στο παράδειγμα της προηγούμενης διαφάνειας είδαμε τη μεταβλητή με τιμή 0x48F0
να αποθηκεύεται στη μνήμη στο πρώτο Byte το F0
και στο 2ο το 48
.
Η σειρά αυτή - για τη C - εξαρτάται από τον επεξεργαστή και θα μπορούσε να είναι διαφορετική.
Η σειρά αυτή ονομάζεται Endianess και σχετίζεται με την εσωτερική δομή
του επεξεργαστή. Όταν το λιγότερο σημαντικό Byte (LSB) αποθηκεύεται στη θέση μνήμης με τη
μικρότερη αριθμητικά διεύθυνση (και το περισσότερο σημαντικό σε αυτό με τη μεγαλύτερη), τότε
αυτή η μέθοδος αποθήκευσης ονομάζεται little endian Byte ordering ενώ το αντίθετο ονομάζεται
big endian Byte ordering. Οι Intel επεξεργαστές είναι τύπου little endian.
Για παράδειγμα δείτε σε little & big endian την long τιμή 0x10203040.
Little endian
Big endian
Η διεύθυνση της μνήμης στην οποία είναι αποθηκευμένη μια μεταβλητή, ονομάζεται δείκτης (pointer) της μεταβλητής. Είναι πολύ σημαντικός επειδή εάν τον γνωρίζουμε τότε μπορούμε να επιρρεάσουμε την ίδια τη μεταβλητή. Προφανώς για όλη τη διάρκεια ζωής της μεταβλητής η μεταβλητή δεν μετακινείται στη μνήμη, άρα και ο δείκτης σε αυτή έχει σταθερή τιμή.
Όμως πως μπορούμε να αποθηκεύσουμε έναν pointer; Σε τι τύπου μεταβλητή; Επιπλέον
ο pointer ως αριθμός μας λέει για την 1η διεύθυνση μνήμης που καταλαμβάνει η
μεταβλητή. Πως θα γνωρίζουμε πόσες θέσεις μνήμης χρειάζεται και πως "ερμηνεύονται"
τα δεδομένα σε αυτές; Χρειαζόμαστε και τον τύπο των δεδομένων λοιπόν. Έτσι τελικά
γράφουμε: short int *Xp;
ή short int *Xp = &x;
Η δήλωση ενός pointer διαβάζεται με δύο τρόπους:
(short int *) Xp
και (short int) (*Xp)
δηλαδή η μεταβλητή Xp
είναι δείκτης σε short int αλλά και το *Xp
είναι το ίδιο ένα short int. Πράγματι, όταν έχουμε μία μεταβλητή τύπου pointer σε "κάτι"
βάζοντας το *
(ως unary operator) μπροστά της συμπεριφέρεται ως απλή μεταβλητή
αποθηκευμένη σε αυτή τη θέση μνήμης. Δηλαδή εφαρμόζοντας το *
σε έναν δείκτη
αποκτούμε την πρόσβαση στο περιεχόμενο της θέσης μνήμης.
short int x=0x1234, *Xp = &x;
Δείκτης | Τιμή | |
0x1002 | 0x1234 | |
short x | &x | x |
short *Xp | Xp | *Xp |
Το Xp
με τη σειρά του είναι και αυτό μία μεταβλητή, άρα αποθηκεύεται και αυτό
κάπου στη μνήμη, οπότε έχει και αυτό έναν αντίστοιχο pointer &Xp
. Αυτό δηλώνεται
ως εξης:
short int ** Xpp = & Xp ;
και όταν έχουμε στη διάθεσή μας τη μεταβλητή Xpp μπορούμε να βρούμε την τιμή του x ώς
**Xpp
. Και αυτή η λογική μπορεί να συνεχίζεται επ'άπειρο.
Μια και το Xp είναι μια μεταβλητή μπορούμε να της θέσουμε την τιμή με απλή εκχώρηση,
άρα ο κώδικας Xp = 0x3100;
θα ήταν θεμιτός, όμως δεν ταιριάζει ο τύπος
δεδομένων καθώς ο δείκτης είναι short int *
και η τιμή int
.
Έτσι πρέπει να εφαρμόσουμε το casting που έχουμε δει σε προηγούμενη παρουσίαση:
Xp = (short int *)0x3100;
Να τονίσουμε ότι κάτι τέτοιο θα ήταν καταστροφή βέβαια, καθώς είναι ιδιαίτερα απίθανο
μια θέση μνήμης "δικής μας εμπνεύσεως" να επιτρέπεται να χρησιμοποιηθεί από το πρόγραμμά
μας.
Ας δοκιμάσουμε να δούμε ένα απλό σενάριο:
#include <stdio.h>
void alfa( int i ) { i++; }
void beta ( int *i ) { (*i)++; }
int main()
{
int i = 123, *I = &i;
*I = 234;
printf("%d\n", i);
alfa(i);
printf("%d\n", i);
beta(I);
printf("%d\n", i);
beta(&i);
printf("%d\n", i);
return 0;
}
Run online
Η κλήση με παράμετρο δείκτη αντί για απλή μεταβλητή ονομάζεται Κλήση με αναφορά ή Call by reference και είναι συνήθης τακτική στον προγραμματισμό σε C. Ουσιαστικά είναι ο τρόπος με τον οποίο "παραβιάζουμε" την προστασία που μας παρέχει η γλώσσα κατά την κλήση των συναρτήσεων, όμως είναι πλέον εμφανές το στοιχείο που δίνουμε και ότι αυτό υπόκειται σε αλλαγή. Έτσι ενώ γενικά η προστασία εξακολουθεί να ισχύει, μπορούμε όπου κρίνουμε να την "εγκαταλείπουμε" προκειμένου να πετύχουμε ένα άλλο επιθυμητό αποτέλεσμα.
Φυσικά η ίδια η μεταβλητή (ο δείκτης δηλαδή) που δίνεται στη συνάρτηση προστατεύεται. Μόνο το περιεχόμενο της μνήμης στην οποία δείχνει "εκτίθεται" σε αλλαγές. Για παράδειγμα δείτε τον παρακάτω κώδικα, όπου οι δείκτες Ι & J συνεχίζουν να δείχνουν στις i & j αντίστοιχα και μετά την κλήση της συνάρτησης:
#include <stdio.h>
void gama(int *a, int *b) {
*a = 111;
a = b;
*a = 888;
}
int main()
{
int i = 123, j = 234, *I = &i, *J = &j;
printf("Before: i=%d , j=%d\n", i, j);
gama (I,J);
printf(" After: i=%d , j=%d\n", i, j);
printf(" After: i=%d , j=%d\n", *I, *J);
return 0;
}
Run online
Χρησιμοποιώντας την κλήση με αναφορά μπορούμε να κατανοήσουμε πως διαβάζει μια συνάρτηση
από το πληκτρολόγιο δεδομένα. Η συνάρτηση αυτή είναι ομόλογη της printf
και
ονομάζεται scanf
. Τα ορίσματά της μπαίνουν ακριβώς όπως
της printf, με κρίσιμη "λεπτομέρεια" ότι εκτός από το αρχικό string, τα υπόλοιπα είναι
pointers προς τις μεταβλητές. Στο παρακάτω παράδειγμα "διαβάζονται" από το πληκτρολόγιο
το βάρος και το ύψος ενός ατόμου. Προσέξτε ότι το string της scanf
δεν
χρησιμοποιείται για να εμφανιστεί κάποιο μήνυμα, αλλά αντιστοιχεί στο αναμενόμενο κείμενο
στην είσοδο.
#include <stdio.h>
int main()
{
double weight = 0.0, height = 0.0, bmi;
printf("\nEnter your weight (in kilos) and press enter:");
scanf("%lf", &weight);
printf("\nNow enter your height (in meters) and press enter:");
scanf("%lf", &height);
if (height == 0) {
printf("\nYou have not entered your height! Bye bye!\n");
return;
}
bmi = weight / height / height;
printf("\nYour BMI is: %lf\n", bmi);
if (bmi < 18.5) printf("\nToo thin!\n");
else if (bmi < 25) printf("\nNormal!\n");
else if (bmi < 30) printf("\nOverweight!\n");
else printf("\nYou should see a doctor!\n");
return 0;
}
Run online
Μία άλλη κλασική χρήση κλήσης με αναφορά είναι η δημιουργία μιας συνάρτησης που να ανταλλάσσει τις τιμές δύο μεταβλητών μεταξύ τους. Η ονομασία της είναι συνήθως swap και δείτε μία υλοποίησή της από κάτω για μεταβλητές τύπου double.
#include <stdio.h>
void swap(double *a, double *b) {
double m;
m = *a;
*a = *b;
*b = m;
}
int main()
{
double x = 10.01, y = 20.02;
swap(&x, &y);
printf("x = %lf , y = %lf\n",x,y);
return 0;
}
Run online
Οι pointers είναι απαραίτητο εργαλείο για τη συγγραφή οποιουδήποτε αξιόλογου προγράμματος σε C. Όμως είναι απίστευτη ευκαιρία για bugs, τόσο καλά κρυμμένα που μπορούν - ειδικά σας πρώτα σας βήματα - να σας αποθαρρύνουν εντελώς. Δείτε!
#include <stdio.h>
int * gama(int *a, int *b) {
int x = 0; // This line is BUGGY!
x = (*a) + (*b);
printf("&x = %x\n\n",&x);
return &x;
}
void delta() {
char q = 100, w = 200;
int e = 300;
printf("&q = %x\n\n",&q);
printf("&w = %x\n\n",&w);
printf("&e = %x\n\n",&e);
}
int main()
{
int i = 123, j = 234, *I = &i, *J = &j;
int *a;
a = gama(I,J);
delta();
printf("See: *a=%d\n", *a);
return 0;
}
Run online
Ενώ ο προγραμματιστής μάλλον αναμένει να δει στο *a
την τιμή 357
συνήθως βρίσκει την τιμή 300 ή και κάποια άλλη φαινομενικά τυχαία, ενώ το χειρότερο απ'όλα είναι
ότι κάποιες φορές μπορεί να βρει όντως την τιμή 357.
Αν πραγματικά υπάρχει κάποιος λόγος ο παραπάνω κώδικας να λειτουργήσει είναι η μεταβλητή
x
μέσα στη συνάρτηση, να δηλωθεί ως static
καθώς έτσι με την
ολοκλήρωση της συνάρτησης δεν απελευθερώνεται προς επαναχρησιμοποίηση, το οποίο είναι και
το λάθος που προκαλεί το πρόβλημα.
Έχουμε αναφέρει ότι κάθε στοιχείο ενός πίνακα λειτουργεί ακριβώς όπως μια μεταβλητή, άρα μπορούμε με τον ίδιο τρόπο να πάρουμε και τον δείκτη σε αυτό! Επίσης θυμόμαστε από όσα ξέρουμε για τους πίνακες ότι τα στοιχεία είναι αποθηκευμένα σε διαδοχικές θέσεις μνήμης. Ας δούμε έναν πίνακα short με τιμές 0x10, 0x20, 0x30, 0x40 και 0x50.
Έτσι π.χ. το στοιχείο a[2]
έχει για δείκτη (&a[2]
)
την τιμή 0x2004
.
Από την άλλη και ο πίνακας είναι μία μεταβλητή, ο δείκτης της οποίας θεωρούμε ότι δείχνει στο
1o Byte του πίνακα. Άρα ο δείκτης του πίνακα a
είναι
ο &a[0]
. Επίσης όμως στη C το όνομα του πίνακα είναι
ο δείκτης του δηλαδή είναι πάντα αληθές ότιa == &a[0]
.
Αυτό σημαίνει ότι οι μεταβλητές τύπου πίνακα στην πραγματικότητα, εσωτερικά, είναι pointers στο 1ο
στοιχείο του πίνακα.
#include <stdio.h>
short a[] = { 0x10, 0x20, 0x30, 0x40, 0x50 };
int main() {
printf("&a[2] = %x\n", &a[2]);
printf("&a[0] = %x\n", &a[0]);
printf(" a = %x\n", a);
return 0;
}
Run online
Εφόσον εσωτερικά ένας δείκτης είναι ένας αριθμός, δηλαδή η διεύθυνση μιας θέσης μνήμης, τότε μπορούμε να κάνουμε πάνω του αριθμητικές πράξεις;
Ναι, αλλά μόνο τις +
, -
, ++
, --
και
από συγκρίσεις, μόνο το ==
και !=
.
Προσέξτε όμως! Είτε προσθέσετε (+), αφαιρέσετε (-), αυξήσετε (++) ή μειώσετε (--) κατά ένα
κάποιον δείκτη τότε η τιμή του αλλάζει όχι κατά μία θέση μνήμης, αλλά κατά όσο χρειάζεται
για να δείχνει στην επόμενη/προηγούμενη μεταβλητή του ίδιου τύπου, ή ισοδύναμα
για την περίπτωση ενός πίνακα, στο επόμενο/προηγούμενο στοιχείο.
#include <stdio.h>
int main() {
int i;
short int a[] = { 100,200,300,400,500 };
short int *A = a;
printf("PTR vs IDX\n");
for (i=0; i<5; i++, A++) {
printf("%d -- %d\n",a[i], *A);
}
printf("\nPTR vs IDX (II)\n");
for (i=0, A=a; i<5; i++, A++) {
printf("%x -- %x\n",&a[i], A);
}
return 0;
}
Run online
Αφού μια μεταβλητή τύπου πίνακα είναι pointer, τότε και ένας pointer μπορεί με τις αγκύλες
να χρησιμοποιηθεί για αναφορά συνεχόμενων θέσεων μνήμης. Το παρακάτω αληθεύει πάντα:
a[i] == *(a+i)
όπως και &a[i] == a+i
Με το παρακάτω παράδειγμα μπορούμε να μετρήσουμε το μήκος μιας συμβολοσειράς. Θυμίζουμε ότι πάντα μετά το τελευταίο σύμβολο ακολουθεί ένα ακόμα στοιχείο του πίνακα που έχει την τιμή 0.
#include <stdio.h>
int mystrlen(char *a) {
int len = 0;
while (*(a++)) // first dereferences and THEN it increments
// despite the parentheses that take care to increment the pointer rather than value
len++;
return len;
}
int main() {
char *txt1 = "Hello there!";
char *txt2 = "I Love C!";
char *txt3 = "";
printf("\"%s\" length is: %d\n",txt1, mystrlen(txt1));
printf("\"%s\" length is: %d\n",txt2, mystrlen(txt2));
printf("\"%s\" length is: %d\n",txt3, mystrlen(txt3));
return 0;
}
Run online
Με τον παρακάτω κώδικα μπορούμε να αντιγράψουμε μια συμβολοσειρά στη θέση ενός άλλου πίνακα χαρακτήρων. Υποθέτουμε ότι ο πίνακας αυτός έχει αρκετό χώρο ώστε να χωρέσει την αρχική συμβολοσειρά.
#include <stdio.h>
void mystrcopy(char *from, char *to) {
while (*(to++) = *(from++))
;
}
int main() {
char to[100];
char *from1 = "Hello there!!";
char *from2 = "Nice to see you";
mystrcopy(from1, to);
printf("%s\n",to);
mystrcopy(from2, to);
printf("%s\n",to);
return 0;
}
Run online
Στο παρακάτω παράδειγμα δημιουργούμε μία συνάρτηση που με δεδομένη μια συμβολοσειρά,
την "σπάει" σε λέξεις (χωρίς να τις μετακινήσει σε άλλο σημείο της μνήμης, άρα
"καταστρέφοντας" την αρχική). Ως λέξεις θεωρούμε ακολουθίες χαρακτήρων A-Z a-z 0-9
,
ενώ οτιδήποτε άλλο θεωρούμε ότι διαχωρίζει λέξεις. Άρα πρέπει να αντικαταστήσουμε τους
χαρακτήρες που δεν ανήκουν σε λέξεις με το 0 και να "μαζέψουμε" τις αρχές των λέξεων
σε έναν πίνακα κειμένων. Για τώρα θα θεωρήσουμε ότι είναι επαρκώς μεγάλος.
#include <stdio.h>
#include <ctype.h>
int split2words(char *t, char *w[]) {
int wc = 0;
int in_word = 0;
while (*t) {
if ( isalnum(*t) ){
if ( !in_word ) { // We found the beginning of a new word
w[wc] = t;
wc++;
in_word = 1;
}
} else {
*t = '\0';
in_word = 0;
}
t++;
}
return wc;
}
int main() {
int i, W;
char txt[100] = "Hello, this is a dummy-simple test!";
// ^^^^^^^^ This cant be char *txt since the "..." cannot be written to
char *words[100];
W = split2words(txt, words);
if (W == 0) {
printf("No words found in the text!\n");
return 0;
} else for (i=0; i<W; i++) { // Μην ψαρώνετε!!
printf("%s\n", words[i]);
}
return 0;
}
Run online
Στο προηγούμενο παράδειγμα χρησιμοποιήσαμε κάτι "λογικό", έναν πίνακα που το
κάθε στοιχείο του είναι pointer. Εάν ο κάθε ένας από αυτούς τους pointers δείχνει
σε έναν μονοδιάστατο πίνακα (και όλοι αυτοί οι μονοδιάστατοι πίνακες) έχουν το
ίδιο μήκος, τότε αυτό που προκύπτει είναι ένας πίνακας δύο διαστάσεων. Και μπορούμε
να τον δηλώσουμε αυτό με οποιονδήποτε τρόπο από τους παρακάτω:
float **f1;
float *f2[];
float f3[][];
Δείτε ένα παράδειγμα:
#include <stdio.h>
int main() {
int a1[5] = {1,2,3,4,5};
int a2[5] = {2,3,4,5,6};
int a3[5] = {3,4,5,6,7};
int a4[5] = {4,5,6,7,8};
int *x[4] = {
a1,
a2,
a3,
a4
};
int **O = x;
int i,j;
for(i=0; i<4; i++) {
for(j=0; j<5; j++)
printf("%d ", O[i][j]);
printf("\n");
}
return 0;
}
Run online
Αρχίζοντας να χρησιμοποιείτε πίνακες (ή και ακόμα πιο περίπλοκες δομές) στα προγράμματά σας, θα δείτε ότι συχνά δεν μπορείτε να γνωρίζετε εκ των προτέρων την ποσότητα της μνήμης (αριθμό πινάκων, αριθμό κελιών στον πίνακα, κλπ) που θα απαιτηθεί κατά την εκτέλεση του κώδικά σας. Αυτό είναι απολύτως φυσιολογικό. Επειδή όμως πρέπει να δηλώνετε τις μεταβλητές σας στην αρχή του κώδικα, αλλά και για κάποιους πιο περίπλοκους λόγους που θα δούμε στη συνέχεια, δεν είναι πάντα εφικτό ή βολικό να καλύψετε και τις δύο αυτές απαιτήσεις ταυτόχρονα. Έτσι χρειάζεται ένας μηχανισμός που να σας επιτρέπει να ζητάτε μνήμη από το σύστημα όποτε σας χρειάζεται και να μπορείτε να την επιστρέφετε σε αυτό όταν δεν την χρειάζεστε πλέον. Ο μηχανισμός αυτός στη C σας δίνεται μέσα από τη χρήση των εντολών:
malloc
και free
Αυτές οι συναρτήσεις ανήκουν στην standard library, άρα εσείς πρέπει όταν θέλετε να τις
χρησιμοποιήσετε να περιλάβετε και το #include <stdlib.h>
στον κώδικά σας. Η πρώτη δέχεται ένα όρισμα που περιγράφει τον αριθμό από τα
bytes που θέλουμε να δεσμεύσουμε για χρήση και επιστρέφει είτε έναν δείκτη σε αυτά
με τον τύπο void *
, είτε NULL
(δηλαδή 0). Η δεύτερη δέχεται για όρισμα το (μη μηδενικό) αποτέλεσμα της πρώτης,
αποδεσμεύοντας όλη την αντίστοιχη μνήμη που είχε αποδοθεί με την κλήση της πρώτης.
Συγχωνεύοντας την
#include <stdio.h>
#include <stdlib.h>
int mystrlen(char *a) {
int len = 0;
while (*(a++)) len++;
return len;
}
char *mystrcopy(char *from) {
int L;
char *to, *hlp;
L = mystrlen(from);
hlp = to = (char *)malloc(L+1); // +1 for the zero/null last byte
if (!to) {
return to;
}
while (*(to++) = *(from++))
;
return hlp;
}
int main() {
char *res1, *txt1 = "Aha! That's how it works!!";
res1 = mystrcopy(txt1);
if (res1) {
printf("%s\n", res1);
free(res1);
}
return 0;
}
Run online
Αν και αυτό το κομμάτι δεν αφορά αποκλειστικά τη C, έχει μεγάλη σημασία και είναι εξαιρετικά χρήσιμο να το γνωρίζετε. Τα παρακάτω είναι μια μικρή υπεραπλουστευμένη σύνοψη του άρθρου http://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap από το StackOverflow. Όλη η συζήτηση γίνεται επειδή οι τοπικές μεταβλητές της C αποθηκεύονται όλες στο Stack, ενώ η μνήμη που μας παρέχει η malloc προέρχεται από το Heap.
Το Stack είναι ένα τμήμα της μνήμης που ανατίθεται σε κάθε εκτελέσιμο για άμεση και συνεχή χρήση του. Στις τυπικές δομές δεδομένων σε επόμενη συνάντηση θα δούμε πως υλοποιείται ένα stack για γενική χρήση. Η δέσμευση και η απελευθέρωση από αυτό είναι πολύ γρήγορη (μία πρόσθεση ή αφαίρεση με σε/από έναν pointer) και υποστηρίζονται από τον επεξεργαστή. Το ποσό αυτό της μνήμης είναι σχετικά περιορισμένο καθώς το μέγεθός του stack είναι κοινό για όλα τα εκτελέσιμα σε ένα λειτουργικό σύστημα και κάθε στιγμή είναι αρκετές δεκάδες από αυτά φορτωμένα στη μνήμη του Η/Υ, αφήνοντας ταυτόχρονα (έτσι θα πρέπει) αρκετή μνήμη για τα τα εκτελέσιμα που τη χρειάζονται. Όταν ένα εκτελέσιμο τερματίζεται το stack, όπως είχε ανατεθεί από το σύστημα, έτσι και απελευθερώνεται από αυτό. Τέλος το stack επειδή είναι πιο μικρό, αλλά και το "δραστήριο" κομμάτι του είναι σαφώς μικρότερο από την cache μνήμη του επεξεργαστή, κατά συνέπεια συχνά καταλήνει να είναι φορτωμένο μέσα σε αυτή καθιστώντας την προσπέλαση ακόμα ταχύτερη.
Το Heap αντίθετα είναι γενικής χρήσης και μπορεί να χρησιμοποιείται ταυτόχρονα από πολλά εκτελέσιμα. Η διαδικασία δέσμευσης και απελευθέρωσης μνήμης είναι πιο χρονοβόρα καθώς θα πρέπει να λάβει υπόψη της περισσότερες παραμέτρους, ενώ ταυτόχρονα - λόγω του multitasking - θα πρέπει να γίνει σε "αρμονία" με τα υπόλοιπα εκτελέσιμα του συστήματος. Τέλος όταν τερματιστεί ένα εκτελέσιμο δεν είναι δεδομένο ότι η μνήμη αυτή θα ελευθερωθεί αυτόματα. (Αν και αυτό για τα μοντέρνα λειτουργικά δεν ισχύει). Όμως το heap είναι "απεριόριστο" στο μέγεθος της δεσμευμένης μνήμης που μπορεί να μας παρέχει.χ
Με βάση όλα όσα είδαμε παραπάνω ένας πίνακας Ν διαστάσεων αντιστοιχεί σε Ν-οστής τάξης δείκτη. Όμως στην προηγούμενη παρουσίασή μας είχαμε αναφέρει ότι τα στοιχεία των πολυδιάστατων πινάκων είναι αποθηκευμένα ανά γραμμή σε διαδοχικές θέσεις μνήμης, και οι υπόλοιπες διαστάσεις πχ γραμμές είναι και αυτές μεταξύ τους διαδοχικά τοποθετημένες. Αυτά τα δύο είναι δύο διαφορετικοί τρόποι να υλοποιηθεί ένας πίνακας N διαστάσεων στη C. Η γραφή τους είναι η ίδια, όχι όμως και ο μηχανισμός τους. Για να είναι εφικτό το σενάριο της διαδοχικής αποθήκευσης όλων των διαστάσεων, θα πρέπει το μέγεθος της κάθε μίας να είναι γνωστό και δεδομένο κατά τη δήλωσή της. Η γενική μορφή αποθήκευσης με τη χρήση πολλαπλών επιπέδων δεικτών έχει τα θετικά και αρνητικά της.
realloc
(δείτε
εδώ),
υπάρχει δυνατότητα αναπροσαρμογής κάποιας διάστασης του πίνακα.
Για εξάσκηση μπορείτε να δοκιμάσετε να γράψετε τις παρακάτω συναρτήσεις: