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 *
.
- 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:
- Ad ogni variabile è associato un indirizzo ad una locazione di memoria;
- 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.
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:
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
.
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:
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
.
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:
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 |
|
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.
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:
Assegniamo l'indirizzo di x
al puntatore p
:
p = &x;
Per cui adesso p
punta ad x
:
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:
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:
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.
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:
Inizializziamo il puntatore p
:
p = &x;
Adesso p
punta alla variabile x
:
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
:
Da questo momento in poi possiamo cambiare il valore di x
modificando sia *p
che *q
.
Se modifichiamo *p
:
*p = 9;
La situazione sarà:
Successivamente, se modifichiamo *q
:
*q = 21;
La situazione diventerà:
Ricapitolando:
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:
Se eseguiamo la riga seguente:
*q = *p;
Stiamo in effetti eseguendo la seguente operazione:
y = x;
Per cui il risultato diventa:
Se invece eseguiamo la riga seguente:
q = p;
A questo punto sia p
che q
punteranno ad x
e la situazione diventa:
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.