[inizio] [indice generale] [precedente] [successivo] [indice analitico] [contributi]


156. C: oggetti dinamici e aritmetica dei puntatori

Fino a questo punto è stato visto l'uso dei puntatori come mezzo per fare riferimento a zone di memoria già allocata. È possibile gestire della memoria allocata dinamicamente durante l'esecuzione del programma, facendovi riferimento attraverso i puntatori, utilizzando apposite funzioni per l'allocazione e deallocazione di questa memoria.

156.1 Oggetti dinamici

Nel file di intestazione `stdio.h' è definita la macro `NULL', che, convenzionalmente, serve a rappresentare il valore di un puntatore indefinito. In pratica un puntatore di qualunque tipo che contenga tale valore, rappresenta il riferimento al nulla.

156.1.1 Dichiarazione e verifica di un puntatore

A seconda del compilatore, la dichiarazione di un puntatore potrebbe coincidere con la sua inizializzazione implicita al valore `NULL'. Per essere sicuri che un puntatore sia inizializzato, lo si può fare in modo esplicito, come nell'esempio seguente:

#include <stdio.h>
...
main () {
    int *pi = NULL;
    ...
}

Dovendo gestire degli oggetti dinamici, prima di utilizzare l'area di memoria a cui dovrebbe fare riferimento un puntatore, è meglio verificare che questo punti effettivamente a qualcosa.

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

156.1.2 Allocazione di memoria

L'allocazione di memoria avviene generalmente attraverso la funzione `malloc()', oppure `calloc()'. 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 <quantità>, size_t <dimensione>)

La differenza tra le due funzioni sta nel fatto che la prima, `malloc()', viene utilizzata per allocare un'area di una certa dimensione, espressa generalmente in byte, mentre la seconda, `calloc()', permette di indicare una quantità di elementi e si presta per l'allocazione di array.

Dovendo utilizzare queste funzioni per allocare della memoria, è necessario conoscere la dimensione dei tipi primitivi di dati, ma per evitare incompatibilità conviene farsi aiutare da `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.

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

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

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

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

156.1.3 Deallocazione di memoria

La memoria allocata dinamicamente deve essere liberata in modo esplicito quando non serve più. Infatti, il linguaggio C non offre alcun meccanismo di raccolta della spazzatura o garbage collector. Per questo si utilizza la funzione `free()' che richiede semplicemente il puntatore e non restituisce alcunché.

void free(void *<puntatore>)

È necessario evitare di deallocare più di una volta la stessa area di memoria. Ciò può provocare risultati 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; /* per sicurezza azzera il puntatore */
    ...
} else {
    /* la memoria non è stata allocata e si fa qualcosa si alternativo */
}

156.2 Aritmetica dei puntatori

Con le variabili puntatore è possibile eseguire delle operazioni elementari: possono essere incrementate e decrementate. Il risultato che si ottiene è il riferimento a una zona di memoria adiacente, in funzione della dimensione del tipo di dati per il quale è stato creato il puntatore.

156.2.1 Array

Gli array sono una serie di elementi dello stesso tipo e dimensione. La dichiarazione di un array è in pratica la dichiarazione di un puntatore al tipo di dati degli elementi di cui questo è composto. Si osservi l'esempio seguente:

int ai[3] = { 1, 3, 5 };
int *pi;
...
pi = ai;

In questo modo il puntatore `pi' punta all'inizio dell'array.

...
*pi = 10; /* equivale a:  ai[0] = 10 */
pi++;
*pi = 30; /* equivale a:  ai[1] = 30 */
pi++;
*pi = 50; /* equivale a:  ai[2] = 50 */

Ecco che, incrementando il puntatore si accede all'elemento successivo adiacente, in funzione della dimensione del tipo di dati. Decrementando il puntatore si ottiene l'effetto opposto, di accedere all'elemento precedente.

Deve essere chiaro che è compito del programmatore sapere quando l'incremento o il decremento di un puntatore ha significato. Diversamente si rischia di accedere a zone di memoria estranee all'array, con risultati imprevedibili.

---------------------------

Appunti Linux 2000.04.12 --- Copyright © 1997-2000 Daniele Giacomini --  daniele @ pluto.linux.it


[inizio] [indice generale] [precedente] [successivo] [indice analitico] [contributi]