Programmare in C – Strutture dati dinamiche: malloc e free

Supponiamo di dover allocare un certo quantitativo di memoria durante l'esecuzione di una nostra applicazione. Si può chiamare la funzione malloc in qualsiasi momento, e utilizzeremo pertanto un blocco di memoria nella memoria heap.

Il sistema operativo riserverà quindi un blocco di memoria per il nostro programma e lo potremo utilizzare come più ci piace. Quando non ci servirà più, ne restituiremo il completo controllo al sistema operativo con la chiamata alla funzione free. A questo punto altre applicazioni e programmi potranno chiedere di utilizzare la parte di memoria appena liberata.

Per esempio, il codice seguente illustra l'uso più semplice possibile della memoria heap:

int main()
{
        int *p;

        p = (int *)malloc(sizeof(int));
        if (p == 0)
        {
                printf("ERROR: Out of memory\n");
                return 1;
        }
        *p = 5;
        printf("%d\n", *p);
        free(p);
        return 0;
}

La prima linea di codice nel programma chiama la funzione malloc. Questa funzione fa essenzialmente tre cose:
1. L'istruzione malloc controlla la quantità di memoria disponibile nella memoria heap e chiede “c'è abbastanza memoria disponibile per allocare un blocco di memoria della dimensione richiesta?”. Il quantitativo di memoria necessario per allocare il blocco viene reso noto dal parametro passato nella funzione malloc – nel nostro caso sizeof(int) è di 4 bytes. Se non ci fosse spazio sufficiente in memoria, la funzione malloc restituirebbe il valore 0 (indirizzo 0) che indica un errore (Un altro nome per zero è NULL e vedrete che verrà utilizzato molto spesso in C). Se non vi sono “intoppi”, la funzione malloc procede

2. Se la memoria è disponibile, il sistema “alloca” ossia “riserva” un blocco della dimensione specificata, in modo che quel blocco sia utilizzato da un solo programma e che, inoltre, non vi siano più malloc che vadano ad occupare lo stesso blocco.

3. Il sistema, a questo punto, memorizza nella variabile puntatore (p nel nostro caso) l'indirizzo del blocco di memoria precedentemente allocato/riservato. La variabile puntatore contiene quindi un indirizzo, mentre il blocco allocato può contenere un valore del tipo specificato. Il puntatore, ovviamente, punta al blocco allocato.

Il diagramma seguente illustra lo stato di memoria dopo aver chiamato la funzione malloc: e il blocco sulla destra è il blocco di memoria allocato tramite malloc.

Il programma prosegue poi verificando che l'allocazione sia andata a buon fine, con l'istruzione di selezione if (p==0), che poteva anche essere scritta come if (p==NULL) oppure anche come if (!p). Se l'allocazione non è andata a buon termine (ossia p è zero) il programma termina. Se l'allocazione ha avuto successo, il programma inizializza allora il blocco allocato al valore 5, stampa il valore, e chiama la funzione free per liberare la memoria heap prima che il programma main abbia termine.

Non vi è una vera differenza fra questo codice e il precedente che imposta p all'indirizzo di un intero i. La sola distinzione è che nel caso della variabile i, la memoria esisteva come parte di memoria di programma già pre-allocata. La stessa variabile i, inoltre, ha due nomi: i e *p. Nel caso di memoria allocata nella parte heap, il blocco ha un solo nome, ossia *p. Il blocco, ovviamente, viene allocato durante l'esecuzione del programma.

Le due domande più comuni che ci si può porre a questo punto sono:
• E' veramente importante verificare sempre che il puntatore non sia zero per ogni allocazione richiesta? La risposta è sì. Poiché la memoria heap varia in dimensione a seconda dei programmi in esecuzione, non vi è nessuna garanzia che una chiamata alla funzione malloc avrà sempre successo. Si dovrebbe pertanto verificare lo stato del puntatore ogni volta che si richiede una allocazione tramite malloc.
• Che cosa succede se si dimentica di liberare un blocco di memoria prima che il programma termini la propria esecuzione? Quando un programma viene terminato, il sistema operativo “fa pulizia”, rilasciando lo spazio occupato dall'eseguibile, dallo stack, dallo spazio di memoria globale e ogni allocazione nello spazio heap. Di conseguenza, non vi dovrebbero essere conseguenze a lungo termine se non si libera la memoria, poiché al termine dell'esecuzione tutto verrà “automaticamente” liberato. Ad ogni modo, vi è da considerare che non liberare la memoria heap durante l'esecuzione può creare dei memory leaks, problematica di cui avremmo modo di discutere più avanti.

I due programmi che seguono mostrano due usi diversi dei puntatori: provate a distinguere fra l'uso di un puntatore e l'uso del valore del puntatore.

void main()
{
        int *p, *q;

        p = (int *)malloc(sizeof(int));
        q = p;
        *p = 10;
        printf("%d\n", *q);
        *q = 20;
        printf("%d\n", *q);
}

L'output finale di questo codice sarà 10 dalla linea 4 e 20 dalla linea 6. Ecco qui il diagramma che illustra passo passo il codice:

Il secondo codice a cui abbiamo accennato è invece il seguente:

void main()
{
        int *p, *q;

        p = (int *)malloc(sizeof(int));
        q = (int *)malloc(sizeof(int));
        *p = 10;
        *q = 20;
        *p = *q;
        printf("%d\n", *p);
}

Il risultato finale dovrebbe essere la stampa del valore 20 a partire dalla linea 6. Ecco il diagramma.

E' da notare che il compilatore permetterà di scrivere *p = *q poiché sia *p che *q sono interi. Questa istruzione dice “spostare il valore intero puntato da q nel valore intero puntato da p”.

Il compilatore permetterà di scrivere anche p = q, poiché sia p che q sono puntatori che puntano allo stesso “tipo” di dati. Se s fosse un puntatore a un carattere p = s non sarebbe permesso perchè si tratta di puntatori a due tipi differenti.

L'istruzione p = q dice: “punta p allo stesso blocco a cui punta q”. In altre parole, l'indirizzo di memoria contenuto in q viene immesso anche in p.

Da tutti questi esempi, si può vedere che ci sono quattro diversi modi di inizializzare un puntatore. Quando un puntatore viene dichiarato, come ad esempio in int *p, esso si trova in uno stato di non inizializzazione. Potrebbe puntare “ovunque”, e fare riferimento ad esso prima di un'inizializzazione opportuna potrebbe generare errori anche gravi.

L'inizializzazione di un puntatore comporta che esso sia forzato a puntare ad un'area di memoria nota:
1. Un modo, come già visto, è usare lo statement malloc. Questa istruzione alloca un blocco di memoria dalla memoria heap e quindi fa puntare il puntatore al blocco, in modo da inizializzarlo: il puntatore fa ora riferimento ad uno spazio di memoria noto.
2. Il secondo modo è quello di usare un'istruzione p = q così che p punti allo stesso luogo in cui q punta. Se q punta ad un blocco di memoria valido, allora p viene inizializzato. Il puntatore p viene inizializzato con il valore valido dell'indirizzo che q contiene. In caso contrario, se q non viene inizializzato o non contiene un indirizzo valido, anche p conterrà lo stesso indirizzo non utile
3. La terza strada è puntare il puntatore ad un indirizzo noto, come ad esempio l'indirizzo di una variabile globale. Per esempio, se i è un intero e p è un puntatore ad un intero, allora l'istruzione p= &i inizializza p puntando ad i.
4. La quarta strada è inizializzare il puntatore al valore 0 che è un valore “speciale”. Si può pertanto scrivere
p = 0; oppure
p = NULL;
Fisicamente quindi abbiamo posto uno zero in p, ossia p contiene l'indirizzo 0 come in figura:

Qualsiasi puntatore può essere impostato per puntare a 0. Quando p punta a zero, comunque, non punta ad alcun blocco. Il valore 0 non è un indirizzo di memoria valido. Si può usare in un'istruzione come:

if (p==0)
{
}
oppure 
while (p!=0)
{
}

Il sistema riconosce il valore 0 e genera messaggi di errore se si prova a fare riferimento a tale indirizzo.

Il comando malloc viene usato per allocare un blocco di memoria. E' anche possibile deallocare un blocco quando non è più necessario. Quando il blocco viene deallocato, può essere riutilizzato da un successivo comando malloc, che permette al sistema di riutilizzare la memoria. Il comando per deallocare la memoria si chiama free ed accetta un puntatore come parametro. Il comando free fa sostanzialmente due cose:
1. Il blocco di memoria puntato dal puntatore viene “sbloccato” e viene liberato il relativo spazio nella memoria heap. Ciò permette un successivo utilizzo di tale spazio da parte di altre funzioni malloc/altri applicazioni/programmi
2. Il puntatore passato come parametro viene lasciato in uno stato di non inizializzazione e deve essere pertanto re-inizializzato per poter essere di nuovo utilizzato.

L'istruzione free ritorna un puntatore al suo stato originario non inizializzato e rende disponibile per l'uso il blocco di memoria heap. L'esempio seguente mostra come usare la memoria heap. Viene allocato un blocco per memorizzare un intero, viene riempito, viene sovrascritto e poi viene rilasciato.

#include <stdio.h>

int main()
{
    int *p;
    p = (int *)malloc (sizeof(int));
    *p=10;
    printf("%d\n",*p);
    free(p);
    return 0;
}

Questo codice è veramente molto utile per illustrare il processo di allocazione, deallocazione e per come si usa un blocco in C. La linea che riguarda la chiamata alla funzione mallo alloca un blocco di memoria della dimensione specificata – nel nostro caso 4 bytes che è la dimensione di un intero. Il comando sizeof in C restituisce la dimensione, in bytes, di qualsiasi tipo di dato. Il codice ha in pratica fatto una chiamata alla malloc come malloc(4), poiché 4 è la dimensione in bytes di un intero sulla maggior parte dei comupter. Utilizzare sizeof invece del numero rende il codice portabile anche su altre macchine.

La funzione malloc restituisce un puntatore al blocco di memoria allocato. Il puntatore è generico. Usando il puntatore senza il casting di tipo produce in genere un warning dal compilatore . Il casting di tipo (int *) converte il generico puntatore restituito dalla malloc ad un puntatore ad un intero, che è proprio quello che p si aspetta. L'istruzione free restituisce un blocco libero da usare per successivi scopi.

Questo secondo esempio, invece, riguarda l'allocazione di una struttura invece di un intero:

#include <stdio.h>

struct rec
{
    int i;
    float f;
    char c;
};

int main()
{
    struct rec *p;
    p=(struct rec *) malloc (sizeof(struct rec));
    (*p).i=10;
    (*p).f=3.14;
    (*p).c='a';
    printf("%d %f %c\n",(*p).i,(*p).f,(*p).c);
    free(p);
    return 0;
}

Da notare è la seguente notazione:

(*p).i=10;

Molti di voi si chiederanno perchè *p.i=10; invece non funziona.

La risposta ha a che fare con la precedenza degli operatori in C. Il risultato del calcolo 5à3
4 è 17, non 32, perchè l'operatore * ha una maggior precedenza rispetto a + in molti linguaggi per computer. In C l'operatore punto (.) ha una precedenza maggiore rispetto al *, quindi è opportuno utilizzare le parentesi.

Può accadere che ci si stanchi di digitare (*p).i ogni volta, così il linguaggio C prevede una sintassi abbreviata. Le due istruzioni che seguono sono perfettamente equivalenti, ma la seconda è più semplice da scrivere:

(*p).i=10;
p->i=10;
La seconda forma è di fatto quella più utilizzata.

Scarica subito una copia gratis

2 Commenti

  1. Avatar photo LorenzoB 14 Novembre 2015
    • Avatar photo Marco Giancola 17 Novembre 2015

Scrivi un commento

Seguici anche sul tuo Social Network preferito!

Send this to a friend