I Puntatori

I puntatori sono variabili che contengono come valore un indirizzo di memoria appartenente ad un oggetto.

La loro utilità risiede nel fatto che la dimensione di memoria occupata da un indirizzo é nettamente inferiore a quella occupata dal dato puntato comportando un risparmio di risorse computazionali e mnemoniche. Generalmente la quantità di memoria necessaria ad un puntatore é 4 byte (su un sistema a 32 bit, 8 su un sistema a 64 bit). La sintassi utilizzata per la dichiarazione di un puntatore é:

tipo *identificatore;

Una volta dichiarato al puntatore deve essere assegnato un indirizzo di memoria valido, e ciò é possibile attraverso l’operatore di indirizzamento espresso dal carattere & come nella seguente sintassi:

identificatore = &identificatore_oggetto_puntato;

Pertanto:

int valore = 10;
int *puntatore = &valore; // puntatore conterrà l’indirizzo di variabile

Allo stesso modo, l’utilizzo nel carattere * detto operatore di deriferimento, permette di accedere al contenuto di un oggetto puntato.

int valore = 10;
int *puntatore = &valore;
int variabile = *puntatore // Il valore di variabile é 10.

Così come:

int valore = 10;
int *puntatore = &valore;
*puntatore = 1000; // la variabile valore sarà 1000.

Puntatore come parametro di funzione

I puntatori possono essere utilizzati come argomenti di funzione, in questo modo:

//Dichiarazione o prototipo di funzione
tipo nome_funzione(tipo *puntatore);

//oppure
tipo nome_funzione(tipo *); //senza identificatore

Come già detto, dal momento che gli argomenti delle funzioni sono passati “by value” ossia, per creazione di variabili temporanee locali interne alla funzione, l’uso dei puntatori permette di operare sui valori esterni alla funzione stessa.

E’ altresì possibile stampare l’indirizzo di memoria di un puntatore, attraverso lo specificatore di tipo %#p (oppure 0x%p).

printf(“L’indirizzo di memoria é: %#p”, identificatore_puntatore);

Puntatori come return di funzioni

Le funzioni possono avere return dei puntatori, é sufficiente che ne venga dichiarato il return in sede di dichiarazione e prototipazione.

int *value(void) {
/…/
}

Tuttavia, nel caso si cerchi di ritornare il valore di una variabile privata della funzione stessa, si riscontrerà un errore causato dal fatto che la variabile ha cessato di esistere al termine della funzione.

Array e puntatori

E’ allo stesso modo possibile utilizzare i puntatori con gli array, attraverso la seguente sintassi:

int identificatore_array[] = { 1, 2, 3, 4, 5, 6, 7, 8, , 9, 10 };
int *puntatore_array = identificatore_array;

E’ importante sottolineare che in questo caso il puntatore punterà all’elemento 0 della predetta array, come messo in evidenza da questa sintassi equivalente ed alternativa:

int *puntatore_array = &identificatore_array[0];

L’accesso ai restanti elementi dell’array resta possibile sommando o sottraendo al puntatore tante unità di memoria (dove con unità di memoria intendiamo l’area di memoria occupata da ogni singolo elemento dell’array) quante quelle che risultano necessarie al raggiungimento della posizione nell’array del dato che desideriamo manipolare. Ad esempio:

int identificatore_array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int *puntatore_array = identificatore_array;
puntatore_array += 2 // *puntatore_array = 3;

Aritmetica dei puntatori

Alla luce di quanto sopra esposto, diviene altresì possibile compiere sui puntatori vere e proprie operazioni aritmetiche come somma e sottrazione tra puntatori o valori interi:

int a = 2;
int b = 4;
int identificatore_array[] = { 1, 2, 3, 4, 5, 6, 7, 8, , 9, 10 };
int *puntatore_array = &identificatore_array[a];
int *puntatore_array2 = &identificatore_array[b];

ptrdiff_t somma_puntatori = *puntatore_array - *puntatore_array2 // 6
ptrdiff_t differenza_puntatori = *puntatore_array - *puntatore_array2 // -2

E’ bene notare che il tipo utilizzato per contenere le variabili somma_puntatori e differenza_puntatori sia di tipo ptrdiff_t e contenuta nella libreria <stddef.h>. Può essere in questo modo usata per stampare il suo contento la funzione printf mediante lo specificatore di formato %td.

Oltre alle operazioni di somma e sottrazione é possibile compiere anche operazioni di confronto ( >, <, >=, <=, ==) ottenendo come risultato true (1) a false (0).

Puntatori ad array come parametri di funzione

La definizione di un argomento di tipo array comporta sempre il passaggio dello stesso sotto forma di puntatore, proprio perché ciò che viene passato é sempre l’indirizzo di memoria dell’elemento 0. Sintassi:

int main(int array[]) { /…/ }

Scorrimento di un array con l’aritmetica dei puntatori

Per scorrere gli elementi di un array vengono solitamente utilizzati i cicli iterativi. E’ tuttavia possibile utilizzare l’aritmetica dei puntatori per raggiungere lo stesso scopo. Nel seguente modo:

#define SIZE 6
int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

for (int *puntatore = array; puntatore < SIZE; puntatore++) {
/.../
}

Puntatore ad un array multidimensionale

Bisogna innanzitutto ricordare che un array multidimensionale é un array di array, dove una dimensione è un array in cui ogni elemento é esso stesso un array. Per quanto concerne l’utilizzo di puntatori é assai più semplice rappresentare il loro funzionamento nel seguente modo:

int array[] = {
{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 },
{ 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 }
};

Per accedere ad un elemento, farlo con la notazione classica degli array:

valore = array[0][0] // valore = 1

Per utilizzarli come argomento di funzione utilizzare la sintassi comune o quella dei puntatori:

void nome_funzione (int array[0][10]);
// Oppure, usando un puntatore
void nome_funzione (int (*array[10]));

NB: ricordarsi le parentesi intorno ad *array[3], altrimenti anziché indicare un puntatore ad un array di tre elementi, indicheremmo un array di 3 puntatori, per via della più alta precedenza degli operatori [ ] rispetto a *.

Una sintassi del tipo int array [ ][ ] non sarebbe in ogni caso accettata, perché, non dando indicazioni circa il numero di colonne, non si saprà in quale area di memoria inizia la successiva riga, essendo la scrittura dei dati, sequenziale. Per questo non é obbligatorio indicare il numero delle righe mentre invece va sempre indicato quello delle colonne.

Array di puntatori

Negli array di puntatori ciascun elemento é un puntatore. Come già accennato dal paragrafo precedente, il suo utilizzo é il seguente:

int *array[] = {
    (int[]) { 1, 2 }, //2 Colonne
    (int[]) { 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 } //10 Colonne
};

Si usano per creare i cosiddetti array triangolari ossia degli array aventi una struttura irregolare cioè un numero di colonne differente per ciascuna riga. E, dato che per definizione il nome di un array non é altri che un puntatore al suo primo elemento, un array di puntatori contiene gli indirizzi di puntamento relative agli array che contiene.

Puntatori a VLA

I puntatori possono essere usati anche con le VLA (Variable lenght array) salvo alcune restrizioni che restano valide per le VLA in generale: possono essere dichiarati solo nelle funzioni o nei prototipi e non possono e sono possono puntare a strutture o unioni.

Puntatori a puntatori

Il puntatore ad un puntatore é una variabile atta a memorizzare l’indirizzo di un puntatore il quale punta ad un valore determinato.

//Dichiarazione di un doppio puntatore

int variabile = 10;
int *puntatore = &variabile;
int **puntatore_a_puntatore = &puntatore;

//Ritornare indirizzo di memoria di un doppio puntatore
*nome_identificatore = *puntatore

//Ritornare il valore della variabile puntata da un doppio puntatore
int valore = **puntatore

La loro utilità si spiega nelle funzioni dove i parametri sono sempre passati by value, qualora si abbia la necessità di manipolare il riferimento in luogo del valore puntato attuando, di fatto, un passaggio by reference, non nativamente supportato in C.

Puntatori a funzione

I puntatori possono anche essere utilizzati come riferimenti a funzioni, osservando la seguente sintassi:

//Dichiarazione puntatore
tipo (*identificatore_puntatore)(tipo argomenti);
identificatore_puntatore = identificatore_funzione;

//Invocazione funzione puntata
(*identificatore_puntatore)(argomenti)

Typedef come alias di puntatori a funzione

L’apposizione delle parentesi tonde serve a distinguerlo dall’invocazione di funzione e agisce come alias. Resta tuttavia possibile invocare una funzione puntata anche senza l’apposizione delle parentesi resta uno standard stilistico utile a distinguere i puntatori a funzione dalle funzioni stesse. Trovano impiego quando le funzioni sono utilizzate quali argomenti di altre funzioni o nella definizione delle cd. callback, cioè quelle funzioni invocate da altre funzioni al verificarsi di un determinato evento.

Risultando a questo punto piuttosto contorta la programmazione mediante puntatori a funzione, la keyword typedef ci viene in aiuto permettendoci di semplificare il codice. Per esempio:

typedef int (*puntatore_a_funzione)(int,int) //Creiamo l’alias puntatore_a_funzione
puntatore_a_funzione nome_funzione;

Sarà quindi possibile creare un puntatore alla funzione in modo più semplice.

Puntatori a void

E’ possibile anche creare puntatori che non puntano a nessun tipo particolare, oppure puntatori a qualsiasi tipo (detti puntatori generici). Questi restano estranei all’utilizzo dell’operatore di deriferimento o all’aritmetica dei puntatori, non essendo definibile a monte la quantità di memoria occupata. Tuttavia un puntatore a un tipo determinato (esempio int *) può essere assegnato a void, e viceversa. L’utilità dei puntatori generici (puntatori a void) é presto spiegata quando é necessario costruire funzioni generiche i cui argomenti non sono di tipo noto.

N.B: è possibile dichiarare solo puntatori a void ma mai variabili di tipo void, proprio per definizione.

Puntatori NULL

Un puntatore a NULL é un puntatore che non punta ad alcun indirizzo di memoria.

Puntatori costanti

La keyword const anteposta ad un puntatore definirà un puntatore costante. Ne esistono di due tipi, quelli riferiti al dato puntato (1° caso), che sarà accessibile attraverso il puntatore in sola lettura, e quelli riferiti al puntatore stesso, il cui indirizzo di memoria non potrà essere modificato (2° caso) mantenendo tuttavia la possibilità di modificare il dato. Puntatori costanti relativi a costanti (3° caso) che non permettono la modifica né dell’indirizzo di memoria né del dato riferito. La loro utilità consiste nell’ottimizzazione delle risorse computazionali, che non prevedendo modifiche, saranno risparmiate.

//Puntatore a costante
const tipo *identificatore_puntatore

//Puntatore costante a variabile
tipo *const identificatore_puntatore

//Puntatore costante a costante
const tipo *const identificatore_puntatore

La keyword restrict

Dallo standard C99 esiste anche la Keyword restrict che applicata ad un puntatore, lo rende un puntatore ristretto,i cui effetti restano limitati al puntatore e non all’oggetto puntato. Questa keyword comunica al compilatore che l’utilizzo dei puntatori é univoco, cioè non sono presenti più puntatori dello stesso per la stessa area di memoria, dando luogo ad un compilazione ottimizzata in termini di risorse computazionali.

Conversione di puntatori

Come le variabili, così anche i puntatori seguono delle regole di conversione e promozione in sede di inizializzazione, assegnamento e comparazione. E’ necessario prestare attenzione in modo particolare nell’utilizzo combinato di puntatori a void con altri tipi di puntatori in quanto causa di comportamenti non definiti.

Luca Scandroglio

Sono un consulente tecnico informatico, un web designer e uno sviluppatore italiano. Aiuto le aziende a dotarsi degli strumenti tecnologici e digitali per superare le sfide del mercato di oggi.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *