C/Gestione della memoria

< C
Indice del libro

Il linguaggio C, come il lettore ha avuto modo di constatare leggendo il testo, è caratterizzato dal dare al programmatore il totale controllo sullo sviluppo della sua applicazione, non mascherando niente, anche a costo di una iniziale difficoltà tutt'altro che scontata.

Pertanto, mentre in linguaggi, come Python, Ruby, Java, la memoria è gestita dal Garbage collector, nel C è compito del programmatore provvedere ad allocare la memoria e soprattutto a deallocarla.

Ovviamente, per comprendere i meccanismi di gestione della memoria, è necessario conoscere l'organizzazione della stessa.

La memoria: lo stack e lo heap modifica

Possiamo immaginare la memoria divisa in due settori (detti segmenti): lo stack e lo heap. Lo stack è la memoria "fissa", che non cambia nel corso dell'esecuzione del programma, al contrario del segmento heap, che è la memoria cosiddetta "dinamica", le cui dimensioni possono variare durante l'esecuzione dell'applicazione.

Per meglio comprendere il concetto, ricorriamo ad un esempio pratico. Vogliamo creare un software per la somma di due numeri: si dichiarano due variabili con le quali gestire il processo logico suddetto. In questo caso noi conosciamo il numero di valori con cui dobbiamo operare (le due variabili). Ma si possono verificare situazioni nelle quali non si conosce a priori il numero dei valori con i quali il programma dovrà operare.

In questo caso si usano le liste, particolari strutture dati le cui dimensioni possono variare (aumentando o diminuendo) durante l'esecuzione del programma. Immaginiamo quindi che il nostro programma debba memorizzare un numero n di utenti, che non conosciamo a priori e che può variare nel corso dell'esecuzione. Come ovviare a questa situazione?

Funzioni di allocazione dinamica modifica

L'allocazione dinamica della memoria avviene generalmente attraverso la funzione malloc(), oppure calloc(), definite nella libreria standard stdlib.h. Se queste riescono a eseguire l'operazione, restituiscono il puntatore alla memoria allocata, altrimenti restituiscono il valore NULL.

void *malloc (size_t dimensione);

void *calloc (size_t numero, size_t dimensione);

La differenza tra le due funzioni sta nel fatto che la prima, malloc(), viene utilizzata per allocare un'area di memoria di una certa dimensione (espressa generalmente in byte), mentre la seconda, calloc(), permette di indicare il numero di elementi e si presta per l'allocazione di array (che, proprio per questa caratteristica di essere a dimensione variabile, si dicono array dinamici)

Dovendo utilizzare queste funzioni per allocare della memoria, è necessario conoscere la dimensione dei tipi primitivi di dati, ma per evitare incompatibilità conviene farsi aiutare dall'operatore sizeof().

Il valore restituito da queste funzioni è di tipo void * cioè una specie di puntatore neutro, indipendente dal tipo di dati da utilizzare. Per questo, in linea di principio, prima di assegnare a un puntatore il risultato dell'esecuzione di queste funzioni di allocazione, è opportuno eseguire un cast (conversione) al tipo di dato desiderato.

int *pi = NULL;
/* ... */
pi = (int *) malloc (sizeof (int));    // pi è un puntatore a interi

if (pi != NULL)
  {
                                       // Il puntatore è valido e allora procede.
   /* ... */
  }
else
  {
                                       // La memoria non è stata allocata e si fa qualcosa
                                       // di alternativo.
    /* ... */
  }

Come si può osservare dall'esempio, il cast viene eseguito con la notazione (int *) che esegue la conversione esplicita in un puntatore a int. Lo standard C non richiede l'utilizzo di questo cast, quindi l'esempio si può ridurre al modo seguente:

...
pi = malloc (sizeof (int));
...

La memoria allocata dinamicamente deve essere liberata in modo esplicito quando non serve più. Per questo si utilizza la funzione free() che richiede semplicemente il puntatore alla memoria precedentemente allocata e non restituisce alcunché.

void free (void *p);       // p punta alla memoria allocata in precedenza  

È necessario evitare di deallocare più di una volta la stessa area di memoria, perché ciò potrebbe provocare effetti imprevedibili.

int *pi = NULL;
/* ... */
pi = (int *) malloc (sizeof (int));

if (pi != NULL)
  {
    // Il puntatore è valido e allora procede.
    /* ... */
    free (pi); // Libera la memoria
    pi = NULL; // e per sicurezza azzera il puntatore.
    /* ... */
  }
else
  {
    // La memoria non è stata allocata e si fa qualcosa
    // di alternativo.
    /* ... */
  }

realloc() modifica

Lo standard prevede una funzione ulteriore, per la riallocazione di memoria: realloc(). Questa funzione si usa per ridefinire l'area di memoria con una dimensione differente (proprio in virtù del fatto che stiamo usando la memoria "dinamica):

void *realloc (void *puntatore, size_t dimensione);

In pratica, la riallocazione deve rendere disponibili gli stessi contenuti già utilizzati, salvo la possibilità che questi siano stati ridotti nella parte terminale. Se invece la dimensione richiesta nella riallocazione è maggiore di quella precedente, lo spazio aggiunto può contenere dati casuali. Il funzionamento di realloc() non è garantito, pertanto occorre verificare nuovamente, dopo il suo utilizzo, che il puntatore ottenuto sia ancora valido.