Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

martedì 9 ottobre 2012

C'era una volta la malloc()
come usare la malloc() in C

Oggi parleremo di allocazione dinamica della memoria. Se facciamo una rapida ricerca con Google sulla famigerata malloc() e sul suo uso (provare con "why use malloc", per esempio), noteremo una notevole quantità di interrogativi sull'argomento che vanno dai semplici dubbi ("ma come si usa ?") ai dubbi esistenziali ("perchè si usa ?"). Beh, ci troviamo su un argomento pseudo-filosofico dovuto al fatto che, effettivamente, è possibile programmare a lungo in C senza mai usare la malloc()... ma, in questo caso, stiamo usando bene il linguaggio? Mi dispiace ma la risposta è NO!

Facciamo prima un po' di chiarezza, però. Chiunque (incluso il sottoscritto) si trovi con la necessità di scrivere (molto) rapidamente un piccolo programma, per testare rapidamente qualcosa - chissà per una urgenza improvvisa durante qualche manutenzione software critica e urgentissima (da terminare per ieri) - come lo scrive ? Ovviamente usando a man bassa variabili automatiche, array sovradimensionati ("memoria ce n'è tanta"), magari qualche variabile statica e (orrore, orrore) variabili globali. Ecco, insomma, uno di quei programmi che già mentre lo scrivi lo battezzi "temp_qualcosa.c" perché sai che dovrai rifarlo da capo appena possibile o dovrai cancellarlo per la vergogna (mica che qualcuno lo legge per sbaglio).

Ma qui, di solito, parliamo di stile e di cose fatte bene: il C ha una potente e flessibile gestione della memoria (è un linguaggio con i puntatori !), e scrivere in C facendo finta di non saperlo è un errore. Attenzione però: questo non significa che bisogna usare sempre puntatori e malloc() ("se no non sei un buon programmatore"), anzi è vero il contrario. All'interno di una funzione le variabili automatiche saranno sempre (e giustamente) la maggioranza, anche perché (quando è possibile e corretto) usare lo stack invece del heap migliora le prestazioni del programma e, tra l'altro, lo rende più semplice da scrivere, leggere e manutenere. La allocazione dinamica della memoria è una operazione dispendiosa per il sistema operativo, aggiunge (evidentemente) complicazione al codice e aumenta la possibilità di errori (alzi la mano chi non si è mai dimenticato di usare la corrispondente free() di una malloc()).

Ma allora quando e perché usare la malloc()? Scusate la risposta un po' lapalissiana, ma io direi:

    1) quando è indispensabile
    2) quando è meglio

Vediamo il primo punto di La Palice:

Quando è indispensabile? La allocazione dinamica è indispensabile in almeno due casi: il primo è quello delle funzioni che ritornano un indirizzo. Vediamo un semplice esempio di funzione che duplica una stringa (ce n'è uno quasi identico sul K&R):
char *myStrdup(
    char *src)
{
    // esegue un duplicato di src (con spazio per il '\0')
    char *dest = malloc(strlen(src) + 1);
    if (dest != NULL)
        strcpy(dest, src);

    return dest;
}
Semplice, no? Inutile dire (ma lo dico lo stesso) che, in una funzione come questa, si deve usare malloc() e non si può optare per usare l'indirizzo di un array automatico allocato sullo stack, che, proprio perché è sullo stack, si perde dopo il return.

Il secondo caso con scelta obbligata è quello delle linked list. Vediamo un esempio molto semplificato (ma perfettamente compilabile e testabile):
// nodo di una linked list singola con campo dati
typedef struct snode {
    int data;
    struct snode *next;
} node_t;

// alloca un node con i dati el un puntatore al prossimo elemento
node_t *addNode(
    node_t *next,
    int    data)
{
    node_t *node = malloc(sizeof *node);
    node->data = data;
    node->next = next;
    return node;
}

// inserisce un nodo in testa a una lista
void insertNode(
    node_t **head,
    int    data)
{
    node_t *node = addNode(*head, data);
    *head = node;
}

// main() per test
void main() 
{
    // lista vuota
    node_t *head = NULL;

    // inserisce 10 nodi con data=indice loop
    int i;
    for (i = 1; i <= 10; i++)
        insertNode(&head, i);

    // scorre la lista e stampa i valori
    while (head) {
        printf("%d\n", head->data);
        head = head->next ;
    }
}
Per chi non le avesse mai usate, le linked-list non sono una frivolezza: sono una delle costruzioni più potenti della programmazione in generale (e del C in particolare). Chi lavora intensamente in C su progetti grandi e professionali finirà, prima o poi, con usarle: un motivo in più, quindi, per familiarizzare con la allocazione dinamica della memoria.

Passiamo al secondo punto di La Palice:

Quando è meglio? Sicuramente tutte le volte che si devono maneggiare dei dati (tipicamente array di tipi semplici o array di strutture), di cui non si conosce a compile-time la dimensione: se non usassimo la malloc() bisognerebbe allocare array sovradimensionati (per evitare che manchi spazio a run-time). E quest'ultimo dettaglio ci indica un'altro punto: se dobbiamo maneggiare grosse quantità di dati non possiamo confidare troppo nello stack, il cui spazio non è infinito, anche se lavoriamo con moderni OS su macchine piene di memoria (e se programmiamo applicazioni embedded con forti limitazioni Hardware... ancora peggio).

Con quanto detto fin'ora spero, almeno, di aver contribuito a fare un po' di chiarezza su quest'argomento controverso. Ah, nel post ho citato sempre, per semplicità, la malloc(), ma, come ben tutti sanno, per l'allocazione dinamica della memoria c'è una vera famiglia di funzioni (malloc(), calloc(), realloc() e free()) che permettono una notevole flessibilità e varietà di soluzioni.

Beh, con questo è tutto, e ricordate: per ogni malloc() c'è una free(): se i conti non vi tornano cominciate a preoccuparvi...

Ciao e al prossimo post.

8 commenti:

  1. Gran bel post. Complimenti.

    RispondiElimina
  2. Salve, premetto che ho appena iniziato a studiare il c, ho appena finito le basi del linguaggio e mi sono imbattuto nel delicato argomento dell'allocazione della memoria. Ho letto con molto interesse il suo post e avrei una domanda.
    Nel primo esempio che ha fatto perchè non posso restituire un array? Io ho capito (forse sbagliando) che il nome di un array è un indirizzo, per cui non potrei restituire quello e farlo ricevere da un puntatore a char per esempio?

    RispondiElimina
    Risposte
    1. La premessa è che la myStrdup() deve ritornare un indirizzo valido al chiamante. Se nella funzione dichiari un array non puoi usarlo né per immagazzinare il ritorno della malloc() (dà errore) né come valore di ritorno della funzione (è l'indirizzo di una variabile locale allocata nello stack della funzione).
      Spero di aver capito bene la tua domanda e di esserti stato utile.

      Elimina
    2. Quindi se ho capito: il problema è che se restituissi un array dichiarato dentro una funzione in realtà restituirei un indirizzo di una variabile che verrebbe "distrutta" (visto che esco dall'ambito della funzione)?

      Infatti provando a modificare il suo esempio mi da un errore relativamente all'assegnamento di malloc (come ha scritto lei),invece per la restituizione in realtà me lo lascia fare ma mi da un warning (il compilatore più sveglio di me si è accorto probabilmente che sto per fare una cosa potenzialmente dannosa)

      Elimina
    3. Si, tutto esatto. Il GCC so che dà un warning, su altri compilatori non ho verificato.

      Elimina
  3. La mia confusione era nata dal fatto che non mi avevano spiegato la differenza fra stack e heap per cui non avevo compreso la differenza fra il nome di un array e un puntatore.

    Grazie mille per le sue esaurienti risposte e per l'interessantissimo post!

    RispondiElimina