[successivo] [precedente] [inizio] [fine] [indice generale] [violazione GPL] [translators] [docinfo] [indice analitico] [volume] [parte]


Capitolo 286.   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 funzioni apposite per l'allocazione e la deallocazione di questa memoria.

286.1   Oggetti dinamici

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

286.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>
...
int 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 di alternativo */
  }

286.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 di 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));
...

286.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 di alternativo */
  }

286.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.

286.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 di informatica libera 2003.01.01 --- Copyright © 2000-2003 Daniele Giacomini -- daniele @ swlibero.org

Dovrebbe essere possibile fare riferimento a questa pagina anche con il nome c_oggetti_dinamici_e_aritmetica_dei_puntatori.html

[successivo] [precedente] [inizio] [fine] [indice generale] [violazione GPL] [translators] [docinfo] [indice analitico]