Puntatori e Variabili in linguaggio C

I puntatori sono uno dei concetti più importanti e potenti del linguaggio C. Essi consentono di manipolare direttamente la memoria del computer. In questa lezione verranno presentate le basi dei puntatori in linguaggio C, dalla loro dichiarazione e inizializzazione all'utilizzo degli operatori indirizzo & e di indirezione *.

Concetti Chiave
  • I puntatori, detti anche variabili puntatore, memorizzano al proprio interno indirizzi piuttosto che valori;
  • Un puntatore appena dichiarato non è inizializzato ma punta ad una locazione di memoria casuale;
  • Con l'operatore indirizzo & si può assegnare l'indirizzo di una variabile ad un puntatore;
  • Con l'operatore di indirezione * si può accedere al valore puntato dall'indirizzo.

Puntatori

Nella lezione precedente abbiamo imparato due concetti fondamentali:

  1. Ad ogni variabile è associato un indirizzo ad una locazione di memoria;
  2. L'indirizzo di una variabile può essere ottenuto come un numero intero senza segno.

A questo punto entrano in gioco i puntatori. Infatti, è possibile in linguaggio C prendere l'indirizzo di una variabile ed assegnarlo ad uno speciale tipo di variabile che prende il nome di puntatore.

Definizione

Puntatore

In linguaggio C, un puntatore, chiamato anche variabile puntatore, è uno speciale tipo di variabile che memorizza al proprio interno indirizzi di memoria.

In gergo tecnico, quando un puntatore p contiene l'indirizzo di una variabile x si dice che p punta ad x.

Ad esempio, un puntatore p che punta all'indirizzo di una variabile x può essere rappresentato graficamente in questo modo:

Rappresentazione grafica di un puntatore
Figura 1: Rappresentazione grafica di un puntatore

Dichiarazione di un puntatore

La dichiarazione di un puntatore o variabile puntatore non è molto diversa dalla dichiarazione di una variabile normale. La principale differenza sta nel fatto che il nome della variabile deve essere preceduto da un asterisco.

Ad esempio, volendo dichiarare una variabile puntatore ad un intero di nome p la sintassi è la seguente:

int *p;

Questa dichiarazione, in sostanza, dice al compilatore che la variabile p è in grado di puntare ad aree di memoria che contengono un intero. Queste aree di memoria possono essere variabili oppure possono essere qualcos'altro come vedremo in futuro.

Quando dichiariamo un puntatore, la sua dichiarazione può apparire insieme a quella di altre variabili. Ad esempio:

int x, *p, y;

In questo caso abbiamo definito due variabili di tipo int, x e y, ed una variabile puntatore ad int di nome p.

Nota

Stile di dichiarazione dei puntatori

In molti libri e siti web sul linguaggio C lo stile adoperato per dichiarare i puntatori è il seguente:

int* p;

In altre parole si pone l'asterisco subito dopo il tipo e si inserisce uno spazio tra l'asterisco e il nome della variabile.

Questo stile viene usato per evidenziare il fatto che la variabile p non è di tipo intero ma di tipo puntatore ad int.

Sebbene lo scopo di uno stile del genere sia nobile, si potrebbero provocare degli errori.

Chi usa questo stile, infatti, sarebbe tentato di scrivere del codice in questo modo:

int* p, q;

Ad uno sguardo poco attento sembrerebbe di aver dichiarato due puntatori ad intero. In realtà, così abbiamo dichiarato un puntatore ad intero, p, ed una variabile intera q!

Il nostro consiglio, per evitare errori, è quello di evitare di usare questo stile di programmazione.

Per cui, anziché scrivere:

/* Sconsigliato */
int* p;

è sempre meglio scrivere:

/* Consigliato */
int *p;

Lo standard del linguaggio C impone che una variabile puntatore punti esclusivamente a oggetti di un tipo specifico: il tipo referenziato.

Ad esempio la seguente variabile p:

int *p;

può puntare solo ad interi. Mentre la variabile q:

double *q;

può puntare solo a double.

Per quanto riguarda il tipo referenziato, invece, non vi sono restrizioni. Anzi, un puntatore potrebbe puntare anche ad un altro puntatore realizzando così puntatori di puntatori che vedremo in futuro.

Ricapitolando:

Definizione

Dichiarazione di un puntatore in linguaggio C

In linguaggio C la sintassi per dichiarare un puntatore è la seguente:

nome_tipo *nome_puntatore;

Un puntatore può puntare solo ad aree di memoria che contengono dati del tipo specificato nome_tipo.

Il tipo specificato prende il nome di tipo referenziato.

Il linguaggio C mette a disposizione due operatori per lavorare con i puntatori.

Il primo operatore è l'operatore indirizzo che abbiamo già visto nella lezione precedente. Attraverso questo operatore possiamo ottenere l'indirizzo di una variabile in memoria.

Il secondo operatore è l'operatore di indirezione che permette di accedere al contenuto puntato da un indirizzo.

Questi due operatori sono fondamentali e vedremo adesso come impiegarli con i puntatori.

Inizializzare un puntatore con l'operatore indirizzo

Quando si dichiara una variabile puntatore il compilatore riserva lo spazio necessario per contenere un indirizzo. Tuttavia sul puntatore appena dichiarato non bisogna fare assunzioni sul contenuto.

In altre parole quando si dichiara un puntatore esso non viene automaticamente inizializzato e potrebbe puntare ad una locazione di memoria non valida.

/* Dichiarazione di un puntatore */
int *p;
/* A questo punto p punta ad una locazione casuale */

Per questo motivo è di fondamentale importanza inizializzare un puntatore appena dichiarato prima di usarlo.

Il primo modo di inizializzare un puntatore è quello di usare l'operatore indirizzo per assegnargli l'indirizzo di una variabile. Ad esempio:

int x = 5;
int *p;

/* Inizializza p con l'indirizzo di x */
p = &x;

In questo esempio, dopo l'inizializzazione, p punterà ad x, ossia p conterrà l'indirizzo di x.

Puntatore dell'esempio
Figura 2: Puntatore dell'esempio

Nella figura possiamo osservare che la variabile x si trova nella locazione 2000 e contiene il valore 5. Dopo l'inizializzazione il puntatore p conterrà il valore 2000 che è l'indirizzo della variabile x.

Possiamo anche direttamente inizializzare un puntatore quando lo dichiariamo. Tornando all'esempio di prima possiamo scrivere:

int x = 5;
/* Dichiara e inizializza p */
int *p = &x;

Ricapitolando:

Definizione

Inizializzazione di un puntatore in linguaggio C

Un puntatore appena dichiarato in linguaggio C punta ad una locazione di memoria casuale.

Inizializzare un puntatore significa assegnargli un indirizzo attraverso l'operatore indirizzo:

nome_tipo x;
nome_tipo *p = &x;

Solo dopo che è stato inizializzato un puntatore può essere utilizzato.

Operatore di indirezione *

Abbiamo visto che possiamo assegnare un indirizzo ad una variabile puntatore. Ora entriamo nel vivo del loro utilizzo.

La potenza del linguaggio C sta nel fatto che quando si lavora con una variabile puntatore è possibile accedere al contenuto della locazione di memoria puntata. In altre parole, anche se la variabile puntatore contiene un indirizzo, è possibile leggere e scrivere il valore contenuto alla locazione di memoria corrispondente.

Per far questo è necessario usare l'operatore di indirezione: *.

Prendiamo l'esempio che segue:

1
2
3
4
int x = 5;
int *p = &x;

printf("%d\n", *p);

In questo pezzo di codice alla riga 2 per prima cosa assegnamo l'indirizzo di x al puntatore p con l'operatore indirizzo.

Successivamente, nella riga 4 andiamo a leggere il contenuto della locazione di memoria puntata da p e lo stampiamo a schermo con la funzione printf.

Da notare bene che verrà stampato il valore di x. Non verrà stampato l'indirizzo.

Tutto questo grazie all'operatore di indirezione.

Definizione

Operatore di Indirezione: *

In linguaggio C, l'operatore di indirezione viene utilizzato per accedere al valore contenuto nella locazione di memoria puntata da un indirizzo.

La sintassi per adoperarlo è la seguente:

/* Dato un puntatore */
tipo *p;

/* Dopo aver assegnato un indirizzo a p */
p = &x;

/* Si accede al contenuto puntato da p */
y = *p;

Si può vedere l'operatore di indirezione come l'inverso dell'operatore indirizzo. In altre parole possiamo scrivere una riga di codice di questo tipo:

y = *&x;

Questo riga è del tutto equivalente a:

y = x;

Ciò deriva dal fatto che l'operatore di indirezione annulla l'operatore indirizzo.

Fintantoché un puntatore punta ad una variabile, esso rappresenta in tutto e per tutto un alias della variabile stessa.

Ad esempio, preso un puntatore p che punta ad una variabile x:

int *p = x;

Finché p punta ad x, *p avrà sempre lo stesso valore di x. Inoltre, cambiare il valore di *p modifica anche il valore di i.

Per comprendere meglio studiamo un esempio. Dichiariamo una variabile intera x e gli assegniamo un valore, ad esempio 4:

int x = 4;

Successivamente, dichiariamo un puntatore ad intero p:

int *p;

Inizialmente p punta ad un indirizzo non valido o comunque casuale:

Il puntatore inizialmente non è inizializzato
Figura 3: Il puntatore inizialmente non è inizializzato

Assegniamo l'indirizzo di x al puntatore p:

p = &x;

Per cui adesso p punta ad x:

Adesso il puntatore è inizializzato
Figura 4: Adesso il puntatore è inizializzato

Proviamo a stampare il valore di x e il valore puntato da p:

printf("%d\n", x);
printf("%d\n", *p);

Il risultato che otteniamo è il seguente:

4
4

Proviamo adesso a modificare il valore puntato da p attraverso l'operatore di indirezione:

*p = 5;

La situazione che ora abbiamo è quella riportata in figura:

Modifica del valore tramite puntatore
Figura 5: Modifica del valore tramite puntatore

Per cui, se andiamo a stampare di nuovo il valore di x e il valore puntato da p otteniamo:

printf("%d\n", x);
printf("%d\n", *p);
5
5

Ricapitolando:

Definizione

Puntatori come Alias di variabili - Dereferenziazione

Usando l'operatore di indirezione, un puntatore diventa a tutti gli effetti un alias della variabile puntata.

Per cui se abbiamo definito:

tipo x;
tipo *p;

p = &x;

Allora le seguenti espressioni sono equivalenti:

*p
x

Il valore situato nella locazione di memoria puntata da un puntatore prende il nome di valore referenziato.

In gergo tecnico, applicare l'operatore di indirezione prende il nome di dereferenziazione.

Nota

Mai applicare l'operatore di indirezione ad un puntatore non inizializzato.

Abbiamo detto che un puntatore non inizializzato punta ad una locazione di memoria casuale.

Accedere al valore puntato da un puntatore non inizializzato può provocare un comportamento non predicibile del nostro programma.

Ad esempio:

int *p;
/* Non inizializziamo p */

/* Proviamo a stampare il contenuto puntato da p */
/* Il comportamento non è predicibile */
printf("%d\n", *p);

Il comportamento del programma in questo caso non sarà noto a priori. In alcuni casi l'output della printf potrebbe essere un valore del tutto casuale. Nel peggiore dei casi il nostro programma potrebbe andare in crash.

Altro caso grave è quello di scrivere il valore puntato da un puntatore non inizializzato.

Ad esempio:

int *p;
*p = 7;

In questo caso, se l'indirizzo casuale contenuto da p era valido abbiamo scritto il valore 7 a quell'indirizzo. A questo punto il comportamento del programma è del tutto casuale in quanto quell'indirizzo potrebbe appartenere ad una variabile di controllo o simili.

Se l'indirizzo non era valido il nostro programma andrà sicuramente in crash.

In molti casi il compilatore potrebbe generare un errore quando usiamo puntatori non inizializzati.

Ad esempio, se prendiamo il seguente programma indefinito.c:

#include <stdio.h>

int main() {
    int *p;
    printf("%d\n", *p);
    return 0;
}

E proviamo a compilare con GCC attivando l'opzione -Wall in questo modo:

gcc -Wall indefinito.c -o indefinito

Otteniamo un warning di questo tipo:

indefinito.c: In function ‘main’:
indefinito.c:5:2: warning: ‘p’ is used uninitialized in this function [-Wuninitialized]
    5 |  printf("%d\n", *p);
      |  ^~~~~~~~~~~~~~~~~~

Assegnamento di puntatori

In linguaggio C è possibile utilizzare l'operatore di assegnamento, =, per copiare puntatori. In altre parole è possibile copiare l'indirizzo contenuto in un puntatore ad un altro. Questo è possibile soltanto nel caso in cui i due puntatori puntino allo stesso tipo.

Ad esempio, supponiamo di avere una variabile intera x:

int x = 5;

Successivamente dichiariamo due puntatori:

int *p;
int *q;

I due puntatori punteranno a due locazioni casuali:

Inizialmente i due puntatori puntano a locazioni casuali
Figura 6: Inizialmente i due puntatori puntano a locazioni casuali

Inizializziamo il puntatore p:

p = &x;

Adesso p punta alla variabile x:

Primo puntatore inizializzato
Figura 7: Primo puntatore inizializzato

Possiamo inizializzare q copiando p, ossia assegnandogli il valore contenuto in p che è l'indirizzo di x:

q = p;

A questo punto anche q punterà a x:

Secondo puntatore inizializzato copiando l'indirizzo dal primo
Figura 8: Secondo puntatore inizializzato copiando l'indirizzo dal primo

Da questo momento in poi possiamo cambiare il valore di x modificando sia *p che *q.

Se modifichiamo *p:

*p = 9;

La situazione sarà:

Modifica del valore della variabile tramite il primo puntatore
Figura 9: Modifica del valore della variabile tramite il primo puntatore

Successivamente, se modifichiamo *q:

*q = 21;

La situazione diventerà:

Modifica del valore della variabile tramite il secondo puntatore
Figura 10: Modifica del valore della variabile tramite il secondo puntatore

Ricapitolando:

Definizione

Assegnamento di puntatori

In linguaggio C è possibile assegnare ad un puntatore un indirizzo attraverso l'operatore di assegnamento, =. In particolare è possibile assegnare ad un puntatore l'indirizzo contenuto in un altro:

tipo *p;
tipo *q;

q = p;

In tal caso dopo l'assegnamento il puntatore di destinazione conterrà lo stesso indirizzo.

Bisogna prestare attenzione a non confondere l'assegnamento del contenuto di un puntatore con l'assegnamento del valore puntato. Infatti, le due seguenti righe di codice sono differenti:

q = p;
*q = *p;

Nel primo caso stiamo assegnando a q l'indirizzo contenuto in p. Nel secondo caso stiamo assegnando alla locazione di memoria puntata da q il valore contenuto nella locazione di memoria puntata da p.

Per comprendere meglio prendiamo un esempio. Supponiamo di avere due variabili:

int x = 10;
int y = 20;

Supponiamo anche di avere due puntatori che puntano ad esse:

int *p = &x;
int *q = &y;

A questo punto la situazione è la seguente:

Due puntatori a variabili diverse
Figura 11: Due puntatori a variabili diverse

Se eseguiamo la riga seguente:

*q = *p;

Stiamo in effetti eseguendo la seguente operazione:

y = x;

Per cui il risultato diventa:

Copia dei valori dereferenziati
Figura 12: Copia dei valori dereferenziati

Se invece eseguiamo la riga seguente:

q = p;

A questo punto sia p che q punteranno ad x e la situazione diventa:

Copia degli indirizzi
Figura 13: Copia degli indirizzi

In Sintesi

In questa lezione abbiamo introdotto le basi dei puntatori in linguaggio C. Abbiamo visto come i puntatori sono variabili che contengono l'indirizzo di un'altra variabile invece del valore stesso e come possono essere dichiarati e inizializzati. Abbiamo anche visto come gli operatori indirizzo & e di indirezione * possono essere utilizzati per accedere e modificare il valore di una variabile tramite il suo indirizzo di memoria.

Tuttavia, in questa lezione ci siamo limitati ad usare i puntatori come alias di altre variabili. Un utilizzo che, a prima vista, può risultare abbastanza riduttivo nella realtà. A partire dalla prossima lezione inizieremo a studiare utilizzi molto più potenti e concreti a partire dal loro uso come argomenti di funzioni.