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.
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++;
}
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:
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.
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.
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:
Regole di funzionamento della funzione realloc
- 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. - Se la funzione
realloc
fallisce, restituisceNULL
e lascia inalterato il blocco di memoria originale. - Se il puntatore passato come argomento di
realloc
èNULL
, la funzione si comporta comemalloc
e alloca un nuovo blocco di memoria. - Se la dimensione richiesta è zero, la funzione
realloc
si comporta comefree
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
.
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
ocalloc
. -
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);