Εισαγωγή στη C (#3)

Επικοινωνήστε με: Παναγιώτη Παύλου

e-mail: allos@mail.ntua.gr

Αποθήκευση μεταβλητών στη μνήμη

Έστω ότι έχουμε μια ακέραια μεταβλητή x των 16bit. Αυτή αποθηκεύεται σε δύο θέσεις μνήμης.

0x1000
0x1002
0x1004
0x1006
F0
48

Επειδή οι θέσεις μνήμης χωράνε 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
"10 little endians"

Στο παράδειγμα της προηγούμενης διαφάνειας είδαμε τη μεταβλητή με τιμή 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
0x1000
0x1002
0x1004
0x1006
40
30
20
10
Big endian
0x1000
0x1002
0x1004
0x1006
10
20
30
40
Δείκτες - Pointers

Η διεύθυνση της μνήμης στην οποία είναι αποθηκευμένη μια μεταβλητή, ονομάζεται δείκτης (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;
0x1000
0x1002
0x1004
0x1006
34
12
Δείκτης Τιμή
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;
Να τονίσουμε ότι κάτι τέτοιο θα ήταν καταστροφή βέβαια, καθώς είναι ιδιαίτερα απίθανο μια θέση μνήμης "δικής μας εμπνεύσεως" να επιτρέπεται να χρησιμοποιηθεί από το πρόγραμμά μας.

Call by reference

Ας δοκιμάσουμε να δούμε ένα απλό σενάριο:

#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.

0x2000
0x2002
0x2004
0x2006
0x2008
10
00
20
00
30
00
40
00
50
00

Έτσι π.χ. το στοιχείο 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
Εφαρμογές II

Στο παρακάτω παράδειγμα δημιουργούμε μία συνάρτηση που με δεδομένη μια συμβολοσειρά, την "σπάει" σε λέξεις (χωρίς να τις μετακινήσει σε άλλο σημείο της μνήμης, άρα "καταστρέφοντας" την αρχική). Ως λέξεις θεωρούμε ακολουθίες χαρακτήρων 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
Stack 'n' Heap

Αν και αυτό το κομμάτι δεν αφορά αποκλειστικά τη 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. Η γραφή τους είναι η ίδια, όχι όμως και ο μηχανισμός τους. Για να είναι εφικτό το σενάριο της διαδοχικής αποθήκευσης όλων των διαστάσεων, θα πρέπει το μέγεθος της κάθε μίας να είναι γνωστό και δεδομένο κατά τη δήλωσή της. Η γενική μορφή αποθήκευσης με τη χρήση πολλαπλών επιπέδων δεικτών έχει τα θετικά και αρνητικά της.

  • - Χρειάζεται περισσότερη μνήμη καθώς εκτός από τα δεδομένα του πίνακα πρέπει να αποθηκευθούν και οι δείκτες.
  • + Οι πρόσθετες διαστάσεις δεν είναι απαραίτητο να έχουν τον ίδιο αριθμό στοιχείων, για παράδειγμα σε έναν διαγώνιο πίνακα δύο διαστάσεων μπορούν να παραληφθούν τα στοιχεία μετά τη διαγώνιο καταλήγοντας σε μειωμένη χρήση μνήμης.
  • - Χρειάζονται πολλαπλές δεσμεύσεις μνήμης για να καλύψουμε κάθε υποπίνακα κάποιας διάστασης.
  • + Δεν απαιτείται όλη η ποσότητα της μνήμης συνεχόμενη. Η συνεχόμενη διαθέσιμη ποσότητα μνήμης συχνά είναι δυσεύρετη.
  • + Μπορείτε να αποδεσμεύσετε μόνο μέρος του πίνακα.
  • + Η μετάθεση ενός υποπίνακα (πχ γραμμής σε 2D πίνακα) γίνεται απλά με την αλλαγή 1-2 δεικτών.
  • + Αν επικεντρωθούμε στη διαχείριση κειμένων όπου κάθε γραμμή μπορεί να είναι μία συμβολοσειρά (= μονοδιάστατος πίνακας) ενώ ένα κείμενο είναι ένας πίνακας γραμμών, και με δεδομένο ότι οι γραμμές εν γένει μεταξύ τους δεν έχουν το ίδιο μήκος, προκύπτει σημαντικό όφελος σε μνήμη.
  • + Εάν το κάθε στοιχείο του πίνακα (βλέπε επόμενη ενότητα) είναι μεγαλύτερο από λίγα Bytes (ακόμα και τα 8 ενός double) η οικονομία σε στοιχεία γίνεται ακόμα πιο σημαντική.
  • + Με τη χρήση της εντολής realloc (δείτε εδώ), υπάρχει δυνατότητα αναπροσαρμογής κάποιας διάστασης του πίνακα.
Ασκήσεις

Για εξάσκηση μπορείτε να δοκιμάσετε να γράψετε τις παρακάτω συναρτήσεις:

  • Μία συνάρτηση που με δεδομένο έναν πινακα συμβολοσειρών, και μία ακόμα που λειτουργεί ως ενωτικό, επιστρέφει μία νέα συμβολοσειρά που αποτελεί την ένωση των συμβολοσειρών με το ενωτικό ανάμεσα στις λέξεις. Πχ από τον πίνακα [ "a1", "b2", "c3", "d4" ] και το " - " ως ενωτικό πρέπει να προκύπτει το: "a1 - b2 - c3 - d4"
  • Μία συνάρτηση που δέχεται για όρισμα μια συμβολοσειρά και επιστρέφει την πρώτη λέξη από το κείμενο αντιγράφοντάς την σε πίνακα κατάλληλου μήκους. Εάν την ξανακαλέσουμε με μηδενικό (NULL) όρισμα, τότε να επιστρέφει την επόμενη λέξη, κοκ είτε μέχρι να τελειώσουν οι λέξεις οπότε θα επιστρέφει NULL, είτε να ξανακληθεί με μη μηδενικό όρισμα.