Allocazione Dinamica degli Array in Linguaggio C

Espandiamo il nostro studio sull'allocazione dinamica della memoria in C, questa volta concentrandoci sugli array.

Finora, abbiamo lavorato con array statici, ossia array la cui dimensione è nota a tempo di compilazione o comunque rimane fissa durante l'esecuzione del programma.

Grazie all'allocazione dinamica, possiamo creare array la cui dimensione può essere calcolata durante l'esecuzione del programma. Inoltre, possiamo modificare la dimensione di un array dinamico in qualsiasi momento.

Array allocati in modo dinamico

Gli array allocati in modo dinamico hanno gli stessi vantaggi delle stringhe allocate dinamicamente, ma con la differenza che possono contenere qualsiasi tipo di dato, non solo caratteri.

Quando realizziamo un programma, risulta spesso difficile stimare a priori la dimensione corretta di un array. Conviene meglio lasciare che sia il programma stesso, in fase di esecuzione, a calcolare la dimensione corretta e modificarla di conseguenza.

In linguaggio C, questo problema può essere risolto allocando dinamicamente gli array ed accedendo ad essi attraverso un puntatore al primo elemento.

La stretta relazione che sussiste tra puntatori ed array in C, rende l'uso degli array dinamici semplice e naturale, proprio come se stessimo lavorando con array statici.

In generale, per allocare un array dinamicamente possiamo usare la funzione malloc. Tuttavia, spesso conviene usare la funzione calloc per una serie di vantaggi, oltre al fatto che inizializza tutti gli elementi dell'array a zero.

Invece, per modificare dinamicamente la dimensione di un array, ossia per far in modo che cresca o si rimpicciolisca durante l'esecuzione del programma, possiamo usare la funzione realloc.

Ora vediamo all'opera queste tre funzioni per lavorare con array dinamici.

Uso di malloc per allocare un array dinamico

L'utilizzo di malloc per allocare dinamicamente un array funziona esattamente come per le stringhe. La differenza principale consiste nel fatto che gli elementi di un array arbitrario non necessariamente occupano un byte di memoria come, invece, accade per le stringhe.

Per questo motivo, dobbiamo usare l'operatore sizeof per calcolare la quantità di spazio di memoria necessario per ogni elemento.

Chiariamo attraverso un esempio. Supponiamo di voler allocare un array composto da n interi. Il valore n sarà calcolato durante l'esecuzione del programma.

Per prima cosa, bisogna dichiarare un puntatore ad intero:

int *array;

Una volta che il valore di n diventa noto, possiamo allocare dinamicamente l'array. Tuttavia la dimensione non sarà n, bensì n * sizeof(int). Questo perché ogni elemento dell'array è un intero, che occupa sizeof(int) byte di memoria.

array = (int *) malloc(n * sizeof(int));

In generale, dato un array composto da elementi di un qualunque tipo tipo, la formula per allocare dinamicamente un array di n elementi è la seguente:

tipo *array = (tipo *) malloc(n * sizeof(tipo));

Questa formula è importantissima. Infatti, passare una dimensione sbagliata alla funzione malloc potrebbe avere conseguenze disastrose e causare segmentation fault.

Nota

Attenzione alla dimensione di memoria richiesta alla funzione malloc

Bisogna sempre adoperare l'operatore sizeof per calcolare la dimensione corretta di un array dinamico.

Sbagliare la dimensione di un array dinamico può causare errori gravi e imprevedibili.

Ad esempio, se vogliamo allocare un array di n interi ma scriviamo il codice che segue:

int *array = (int *) malloc(n);

Se la dimensione di un int è maggiore di un byte, come accade praticamente su ogni computer moderno, la funzione malloc allocherà una dimensione insufficiente di memoria. Questo potrebbe causare un segmentation fault o comportamenti imprevedibili.

Una volta che il puntatore punta al blocco di memoria allocato, possiamo a tutti gli effetti ignorare il fatto che array sia un puntatore ed utilizzarlo come il nome di un array. Questo proprio in virtù del fatto che esiste una relazione stretta tra puntatori e il nome di un array.

Ad esempio, volendo inizializzare tutti gli elementi dell'array a zero, possiamo scrivere:

for (int i = 0; i < n; i++) {
    array[i] = 0;
}

Inoltre, è possibile adoperare l'aritmetica dei puntatori per accedere agli elementi dell'array. Ad esempio, per stampare tutti gli elementi dell'array, possiamo scrivere:

int *p = array;
for (int i = 0; i < n; i++) {
    printf("%d ", *p);
    p++;
}
Definizione

Uso di malloc per allocare un array

Per allocare dinamicamente un array di n elementi di tipo tipo, possiamo usare la funzione malloc.

La sintassi generale è la seguente:

tipo *array = (tipo *) malloc(n * sizeof(tipo));

dove:

  • tipo è il tipo di dato degli elementi dell'array;
  • array è il puntatore all'array allocato;
  • n è il numero di elementi dell'array.

La funzione calloc e gli array dinamici

La funzione malloc ha il vantaggio di essere abbastanza generica da poter essere impiegata nella stragrande maggioranza dei casi per allocare blocchi di memoria che possono contenere qualsiasi tipo di dato.

Nel caso degli array, tuttavia, spesso conviene usare la funzione calloc.

La funzione calloc è definita nell'header <stdlib.h> della libreria standard del C ed ha il seguente prototipo:

void *calloc(size_t num, size_t size);

La funzione calloc alloca lo spazio necessario per allocare un array composto da num elementi, ciascuno dei quali occupa size byte di memoria.

In caso di successo, la funzione restituisce un puntatore al blocco di memoria allocato. In caso di errore, restituisce NULL.

Inoltre, dopo aver allocato la memoria richiesta, la funzione calloc inizializza tutti gli elementi dell'array a zero.

Per cui, volendo ad esempio allocare un array di n interi e inizializzarli a zero, possiamo scrivere:

int *array = (int *) calloc(n, sizeof(int));

Ricapitolando:

Definizione

Uso di calloc per allocare e inizializzare un array

Per allocare dinamicamente e inizializzare gli elementi di un array a zero, possiamo usare la funzione calloc.

La sintassi generale è la seguente:

tipo *array = (tipo *) calloc(n, sizeof(tipo));

dove:

  • tipo è il tipo di dato degli elementi dell'array;
  • array è il puntatore all'array allocato;
  • n è il numero di elementi dell'array.

Modifica dinamica della dimensione di un array con realloc

Quando si alloca la memoria per un array, potrebbe succedere che, in un secondo momento, sorga l'esigenza di modificare la dimensione dell'array. Magari, perché si è reso necessario aggiungere o rimuovere elementi.

A questo scopo, la libreria standard del C mette a disposizione una funzione di libreria chiamata realloc. La funzione realloc può ridimensionare un array, e in generale un qualsiasi blocco di memoria allocato dinamicamente, in maniera tale da soddisfare le nostre esigenze.

La funzione realloc è definita sempre nell'header <stdlib.h> e ha il seguente prototipo:

void *realloc(void *ptr, size_t size);

La funzione realloc prende in input un puntatore ptr ad un blocco di memoria allocato dinamicamente in precedenza e la nuova dimensione size che vogliamo assegnare al blocco di memoria.

Il punto fondamentale è che il puntatore ptr deve essere stato restituito da una chiamata a malloc, calloc o realloc fatto in precedenza. In caso contrario, il comportamento della funzione realloc è indefinito.

Il parametro size, invece, può essere più grande o più piccolo della dimensione originale del blocco di memoria.

In generale, realloc non richiede che il blocco di memoria da ridimensionare sia un array. Infatti, possiamo usare realloc per ridimensionare qualsiasi blocco di memoria allocato dinamicamente. Nella maggior parte dei casi, però, realloc viene usato per ridimensionare array.

Definizione

Uso di realloc per ridimensionare un array

Per ridimensionare dinamicamente un array, possiamo usare la funzione realloc.

La sintassi generale è la seguente:

array = (tipo *) realloc(array, nuova_dimensione * sizeof(tipo));

dove:

  • p è il puntatore all'array da ridimensionare;
  • tipo è il tipo di dato degli elementi dell'array;
  • nuova_dimensione è la nuova dimensione dell'array.
Nota

Il puntatore passato a realloc deve essere stato restituito da malloc, calloc o realloc

Bisogna prestare attenzione a passare a realloc un puntatore che sia stato restituito da una chiamata a malloc, calloc o realloc fatta in precedenza.

Se il puntatore passato a realloc non è stato restituito da una di queste funzioni, il comportamento della funzione realloc è indefinito.

Regole del comportamento di realloc

Lo standard del linguaggio C impone che la funzione realloc si comporti rispettando le seguenti regole:

Definizione

Regole di funzionamento della funzione realloc

  1. Quando realloc espande un blocco di memoria, la funzione non inizializza i byte che compongono il blocco di memoria aggiuntivo. Questo significa che i byte aggiuntivi potrebbero contenere valori casuali.
  2. Se la funzione realloc fallisce, restituisce NULL e lascia inalterato il blocco di memoria originale.
  3. Se il puntatore passato come argomento di realloc è NULL, la funzione si comporta come malloc e alloca un nuovo blocco di memoria.
  4. Se la dimensione richiesta è zero, la funzione realloc si comporta come free e libera il blocco di memoria.

Lo standard del linguaggio C si ferma qui e non specifica ulteriormente il comportamento della funzione realloc.

Tuttavia, ci si può aspettare comunque un comportamento ragionevolmente efficiente della funzione.

In generale, tutte le implementazioni della funzione realloc cercano di ridimensionare la dimensione del blocco in loco.

In altre parole:

  • Se il blocco deve essere rimpicciolito, la funzione realloc cercherà di ridurre la dimensione del blocco di memoria senza doverlo spostare;
  • Se il blocco deve essere espanso, la funzione realloc cercherà di espandere il blocco di memoria senza doverlo spostare.
  • Se, nell'espandere il blocco di memoria non c'è spazio sufficiente, la funzione realloc allocherà un nuovo blocco di memoria, copierà i dati dal vecchio blocco al nuovo blocco e libererà il vecchio blocco.

Uso di realloc

Un punto chiave che riguarda l'utilizzo della funzione realloc è che il puntatore di memoria allocato dinamicamente passato come argomento deve essere aggiornato con il valore restituito dalla funzione.

Ad esempio:

/* Allochiamo, inizialmente, un array di n elementi */
int *array = (int *) malloc(n * sizeof(int));

/* ... */

/* Ridimensioniamo l'array a m elementi */
array = (int *) realloc(array, m * sizeof(int));

In questo esempio, l'array viene ridimensionato da n a m elementi.

Poiché il nuovo blocco di memoria potrebbe trovarsi ad un indirizzo completamente diverso rispetto al blocco di memoria originale, è necessario aggiornare il puntatore array con il valore restituito da realloc.

Per questo, nell'ultima riga dell'esempio, assegniamo il valore restituito da realloc a array.

Definizione

Il puntatore passato a realloc deve essere aggiornato con il valore restituito

Dopo aver chiamato la funzione realloc, il puntatore passato come argomento deve essere aggiornato con il valore restituito dalla funzione:

p = (tipo *) realloc(p, nuova_dimensione * sizeof(tipo));

Deallocare la memoria di un array dinamico

Così come per le stringhe, anche per generici array dinamici è necessario deallocare la memoria una volta che non è più necessaria.

Per deallocare la memoria di un array dinamico, possiamo usare la funzione free come già vista in precedenza:

free(array);

Dopo aver chiamato free, il puntatore array non punterà più ad alcun blocco di memoria. Per cui, è buona norma assegnare NULL al puntatore array per evitare che venga usato erroneamente:

array = NULL;

Alla funzione free è possibile passare un qualunque puntatore ottenuto con una chiamata a malloc, calloc o realloc.

In Sintesi

In questa lezione abbiamo imparato i seguenti punti chiave:

  • Gli array allocati dinamicamente in C sono array che possono crescere o rimpicciolire durante l'esecuzione del programma.
  • Per allocare dinamicamente un array, possiamo usare la funzione malloc o calloc.
  • Se usiamo malloc, dobbiamo calcolare la dimensione corretta dell'array moltiplicando il numero di elementi per la dimensione di ciascun elemento:

    tipo *array = (tipo *) malloc(n * sizeof(tipo));
    
  • Se usiamo calloc, la funzione inizializza tutti gli elementi dell'array a zero:

    tipo *array = (tipo *) calloc(n, sizeof(tipo));
    
  • Per ridimensionare dinamicamente un array, possiamo usare la funzione realloc:

    array = (tipo *) realloc(array, nuova_dimensione * sizeof(tipo));
    
  • Dopo aver chiamato realloc, il puntatore passato come argomento deve essere aggiornato con il valore restituito dalla funzione.

  • Per deallocare la memoria di un array dinamico, possiamo usare la funzione free:

    free(array);