
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.

Ho cercato sul sito un eventuale blog dove si potessero fare domande inerenti all’argomento in questione, ma dati gli scarsi risultati (magari non l’ho trovato io), provo a fare una domanda inerente all’argomente direttamente sotto l’articolo dello stesso (anche se il post è abbastanza datato). Programmo da poco in C e a lezione ci hanno introdotto la gestione dinamica della memoria tramite malloc & Co, non ho però compreso bene quando utilizzare un array e quando utilizzare la suddetta funzione. Da quel che ho carpito, la malloc è utile principalmente in quanto mi permette di creare array della dimensione desiderata senza doverne conoscere la dimensione da prima, evitando così di creare array sovradimensionati o altro. I punti salienti del mio dubbio sono:
1. E’ possibile sostituire l’utilizzo degli array con memoria allocata con la malloc? Quando usare i primi e quando i secondi? Esistono altri vantaggi oltre a quello di non dover obbligatoriamente conoscere gli elementi prima dell’esecuzione del programma?
2. Esiste un metodo comodo per caricare la memoria allocata dalla malloc? Per gli array potevo scrivere array[ ] = { “bimboPaffuto” } ma ho verificato su visual studio che ciò non è possibile per la malloc (anche perchè inizializzerei il puntatore che ritorna la funzione…credo…). Ho ottenuto risultati migliori facendo:
char *ptr = malloc (5*sizeof(char));
ptr = “cacao”;
ma quando vado a fare una realloc mi genera un breakpoint (possibile che manchi lo /0 alla fine della stringa?).
Spero di aver scritto nella sezione giusta, buona giornata 🙂
La funzione malloc serve per creare gli oggetti dinamici. Quando dichiaro una variabile (o un array o altro) all’inizio del programma, appena eseguo il programma viene allocata la memoria per quella variabile e solo quando termina l’esecuzione del programma tale memoria viene deallocata; questi sono gli oggetti statici: quelli che vengono creati specificandoli in una definizione. Gli oggetti dinamici invece vengono creati e distrutti durante l’esecuzione del programma, non hanno un nome esplicito e si fa rierimento ad essi tramite i puntatori. Con malloc posso creare ovviamente anche degli array ma è errato dire che mi permette di creare array senza doverne conoscere a priori la dimensione, infatti lei ha giustamente scritto malloc(5*sizeof(char)) per creare un’array di 5 caratteri (ovvero una stringa), ma lo ha dovuto stabilire prima che i caratteri erano 5. Invece malloc è utile quando voglio implementare tramite struct (record) e puntatori strutture dati tipo liste, pile, code, alberi e grafi. Grazie a tali strutture io posso inserire tutti i dati che voglio senza dover sapere a priori quanti saranno. Se per es. voglio inserire dei numeri memorizzandoli in un array (dichiarato o creato con malloc) devo necessariamente stabilire a priori la dimensione dell’array e quindi il numero massimo di valori che potrò inserirvi, ma se invece li inserisco in una lista formata da record aventi come campi un intero (dove vado a memorizzare il valore) e un puntatore (che punta al record successivo), ogni volta che desidero inserire un valore nella lista creo dinamicamente con malloc un record nel quale memorizzo il valore e lo collego alla lista tramite il suo campo puntatore. In tal modo posso inserire tutti i valori che voglio senza sapere a priori quanti saranno e senza spreco di memoria. Quindi l’allocazione dinamica della memoria è utile quando non so a priori quante locazioni di memoria dovrò utilizzare.
La sua 2° domanda (“Esiste un metodo comodo per caricare la memoria allocata dalla malloc?”) temo di non averla compresa; cosa intende per “metodo comodo” e per “caricare”?
L’inizializzazione array[ ] = { “bimboPaffuto” } è errata: la stringa non va racchiusa tra parentesi graffe, a meno che non stia inizializzando un array di stringhe.