Puntatori in C++
- Un puntatore in C++ è una variabile che memorizza l'indirizzo di un'altra variabile.
- I puntatori sono definiti usando l'operatore asterisco (
*
) e possono essere inizializzati con l'operatore address-of (&
). - I puntatori possono essere dereferenziati usando l'operatore asterisco (
*
) per accedere o modificare il valore della variabile a cui puntano. - I puntatori possono essere nulli, il che significa che non puntano a nessuna variabile valida.
- I puntatori possono essere confrontati tra loro e con il valore null per verificare se puntano alla stessa variabile o se sono nulli.
- I puntatori
void
possono essere usati per rappresentare un puntatore generico che può essere convertito in qualsiasi altro tipo di puntatore.
Puntatori
Un puntatore è un tipo composto che punta a un altro tipo.
Come i riferimenti, i puntatori sono usati per l'accesso indiretto ad altri oggetti. A differenza di un riferimento, un puntatore è un oggetto a sé stante. I puntatori possono essere assegnati e copiati; un singolo puntatore può puntare a diversi oggetti diversi durante la sua vita. A differenza di un riferimento, un puntatore non ha bisogno di essere inizializzato al momento in cui viene definito. Come altri tipi built-in, i puntatori definiti nell'ambito di blocco hanno valore indefinito se non sono inizializzati.
I puntatori sono spesso difficili da capire. I problemi di debug dovuti a errori di puntatore tormentano anche i programmatori esperti.
Trattazione approfondita sui puntatori
I puntatori in C++ sono una funzionalità mutuata direttamente dal linguaggio C. In questa lezione forniamo una trattazione completa dei puntatori, ma non copriamo ogni dettaglio.
Se siete interessati ad una trattazione più approfondita dei puntatori, potete consultare le lezioni sui puntatori in linguaggio C disponibili su questo sito.
Definiamo un tipo puntatore scrivendo un dichiaratore della forma:
tipo *p;
dove p
è il nome che viene definito.
L'asterisco *
deve essere ripetuto per ogni variabile puntatore:
// sia punt_int1 che punt_int2 sono puntatori a int
int *punt_int1, *punt_int2;
// punt_double2 è un puntatore a double;
// valore_double è un double
double valore_double, *punt_double2;
Ottenere l'Indirizzo di un Oggetto
Un puntatore contiene l'indirizzo di un altro oggetto.
Si può ottenere l'indirizzo di un oggetto usando l'operatore address-of (l'operatore &
):
int valore_int = 42;
// p contiene l'indirizzo di valore_int;
// p è un puntatore a valore_int
int *p = &valore_int;
La seconda istruzione definisce p
come un puntatore a int
e inizializza p
per puntare all'oggetto int chiamato valore_int
. Poiché i riferimenti non sono oggetti, non hanno indirizzi. Quindi, non possiamo definire un puntatore a un riferimento.
Con due eccezioni, che vedremo in seguito, i tipi del puntatore e dell'oggetto a cui punta devono corrispondere:
double valore_double;
// OK: l'inizializzatore è l'indirizzo di un double
double *punt_double = &valore_double;
// OK: l'inizializzatore è un puntatore a double
double *punt_double2 = punt_double;
// ERRORE: i tipi di punt_int e punt_double differiscono
int *punt_int = punt_double;
// ERRORE: assegnare l'indirizzo di un double a un puntatore a int
punt_int = &valore_double;
I tipi devono corrispondere perché il tipo del puntatore viene usato per inferire il tipo dell'oggetto a cui il puntatore punta. Se un puntatore indirizzasse un oggetto di un altro tipo, le operazioni eseguite sull'oggetto sottostante fallirebbero.
Valore del Puntatore
Il valore (cioè, l'indirizzo) memorizzato in un puntatore può essere in uno di quattro stati:
- Può puntare a un oggetto.
- Può puntare alla posizione immediatamente dopo la fine di un oggetto.
- Può essere un puntatore
null
, indicando che non è legato ad alcun oggetto. - Può essere non valido; valori diversi dai tre precedenti sono non validi.
È un errore copiare o altrimenti provare ad accedere al valore di un puntatore non valido. Come quando usiamo una variabile non inizializzata, questo è un errore che il compilatore è improbabile che rilevi. Il risultato dell'accesso a un puntatore non valido è indefinito. Pertanto, dobbiamo sempre sapere se un dato puntatore è valido.
Sebbene i puntatori nei casi 2 e 3 siano validi, ci sono limiti a ciò che possiamo fare con tali puntatori. Poiché questi puntatori non puntano ad alcun oggetto, non possiamo usarli per accedere all'oggetto (supposto) a cui il puntatore punta. Se proviamo ad accedere a un oggetto attraverso tali puntatori, il comportamento è indefinito.
Usare un Puntatore per Accedere a un Oggetto
Quando un puntatore punta a un oggetto, possiamo usare l'operatore di dereferenziazione (l'operatore *
) per accedere a quell'oggetto:
int valore_int = 42;
// p contiene l'indirizzo di valore_int;
// p è un puntatore a valore_int
int *p = &valore_int;
// * restituisce l'oggetto a cui p punta;
// stampa 42
std::cout << *p;
Dereferenziare un puntatore restituisce l'oggetto a cui il puntatore punta. Possiamo assegnare a quell'oggetto assegnando al risultato della dereferenziazione:
// * restituisce l'oggetto;
// assegniamo un nuovo valore a valore_int attraverso p
*p = 0;
// stampa 0
std::cout << *p;
Quando assegniamo a *p
, stiamo assegnando all'oggetto a cui p
punta.
Attenzione: dereferenziare puntatori non validi
Possiamo dereferenziare solo un puntatore valido che punta a un oggetto.
In caso contrario, il comportamento è indefinito.
Alcuni simboli hanno significati multipli
Alcuni simboli, come &
e *
, sono usati sia come operatore in un'espressione che come parte di una dichiarazione. Il contesto in cui un simbolo è usato determina cosa significa il simbolo:
int i = 42;
// & segue un tipo ed è parte di una dichiarazione;
// r è un riferimento
int &r = i;
// * segue un tipo ed è parte di una dichiarazione;
// p è un puntatore
int *p;
// & è usato in un'espressione come operatore address-of
p = &i;
// * è usato in un'espressione come operatore di dereferenziazione
*p = i;
// & è parte della dichiarazione;
// * è l'operatore di dereferenziazione
int &r2 = *p;
Nelle dichiarazioni, &
e *
sono usati per formare tipi composti. Nelle espressioni, questi stessi simboli sono usati per denotare un operatore. Poiché lo stesso simbolo è usato con significati molto diversi, può essere utile ignorare le apparenze e pensare a loro come se fossero simboli diversi.
Puntatori Null
Un puntatore null non punta ad alcun oggetto. Il codice può verificare se un puntatore è null prima di tentare di usarlo. Ci sono diversi modi per ottenere un puntatore null:
// equivalente a int *p1 = 0;
int *p1 = nullptr;
// inizializza direttamente p2 dalla costante letterale 0
int *p2 = 0;
// equivalente a int *p3 = 0;
int *p3 = NULL;
L'approccio più diretto è inizializzare il puntatore usando il letterale nullptr
, che è stato introdotto dallo standard C++11. nullptr
è un letterale che ha un tipo speciale che può essere convertito a qualsiasi altro tipo puntatore. In alternativa, possiamo inizializzare un puntatore al letterale 0, come facciamo nella definizione di p2
.
I programmi più vecchi a volte usano una macro del preprocessore chiamata NULL
, che l'header cstdlib
definisce come 0.
Descriveremo il preprocessore in un po' più di dettaglio nelle prossime lezioni. Ciò che è utile sapere ora è che il preprocessore è un programma che viene eseguito prima del compilatore. Le variabili del preprocessore sono gestite dal preprocessore e non fanno parte del namespace std
. Di conseguenza, ci riferiamo a loro direttamente senza il prefisso std::
.
Quando usiamo una variabile del preprocessore, il preprocessore sostituisce automaticamente la variabile con il suo valore. Quindi, inizializzare un puntatore a NULL
è equivalente a inizializzarlo a 0
. I programmi C++ moderni dovrebbero generalmente evitare di usare NULL
e usare nullptr
invece.
È illegale assegnare una variabile int
a un puntatore, anche se il valore della variabile è 0
.
int zero = 0;
// ERRORE: non si può assegnare un int a un puntatore
punt_int = zero;
Inizializzare tutti i puntatori
I puntatori non inizializzati sono una fonte comune di errori a runtime.
Come con qualsiasi altra variabile non inizializzata, ciò che accade quando usiamo un puntatore non inizializzato è indefinito. Usare un puntatore non inizializzato quasi sempre risulta in un crash a runtime. Tuttavia, il debug dei crash risultanti può essere sorprendentemente difficile.
Sotto la maggior parte dei compilatori, quando usiamo un puntatore non inizializzato, i bit nella memoria in cui risiede il puntatore sono usati come indirizzo. Usare un puntatore non inizializzato è una richiesta di accedere a un supposto oggetto in quella supposta posizione. Non c'è modo di distinguere un indirizzo valido da uno non valido formato dai bit che si trovano nella memoria in cui è stato allocato il puntatore.
La nostra raccomandazione di inizializzare tutte le variabili è particolarmente importante per i puntatori. Se possibile, definire un puntatore solo dopo che l'oggetto a cui dovrebbe puntare è stato definito. Se non c'è un oggetto a cui legare un puntatore, allora inizializzare il puntatore a nullptr
o zero. In questo modo, il programma può rilevare che il puntatore non punta a un oggetto.
Assegnazione e Puntatori
Sia i puntatori che i riferimenti danno accesso indiretto ad altri oggetti. Tuttavia, ci sono differenze importanti nel modo in cui lo fanno.
La più importante è che un riferimento non è un oggetto. Una volta definito un riferimento, non c'è modo di far riferire quel riferimento a un oggetto diverso. Quando usiamo un riferimento, otteniamo sempre l'oggetto a cui il riferimento era inizialmente legato.
Non c'è tale identità tra un puntatore e l'indirizzo che contiene. Come con qualsiasi altra variabile (non riferimento), quando assegniamo a un puntatore, diamo al puntatore stesso un nuovo valore. L'assegnazione fa puntare il puntatore a un oggetto diverso:
int i = 42;
// punt_int è inizializzato ma non indirizza alcun oggetto
int *punt_int = 0;
// punt_int2 inizializzato per contenere l'indirizzo di i
int *punt_int2 = &i;
// se punt_int3 è definito dentro un blocco,
// punt_int3 è non inizializzato
int *punt_int3;
// punt_int3 e punt_int2 indirizzano lo stesso oggetto, ad es., i
punt_int3 = punt_int2;
// punt_int2 ora non indirizza alcun oggetto
punt_int2 = 0;
Può essere difficile capire se un'assegnazione cambia il puntatore o l'oggetto a cui il puntatore punta. La cosa importante da tenere a mente è che l'assegnazione cambia il suo operando sinistro. Quando scriviamo
// il valore in punt_int è cambiato; punt_int ora punta a valore_int
punt_int = &valore_int;
assegniamo un nuovo valore a punt_int
, che cambia l'indirizzo che punt_int
contiene.
D'altra parte, quando scriviamo
// il valore in valore_int è cambiato; punt_int è invariato
*punt_int = 0;
allora *punt_int
(cioè, il valore a cui punt_int
punta) è cambiato.
Altre Operazioni sui Puntatori
Fintanto che il puntatore ha un valore valido, possiamo usare un puntatore in una condizione. Proprio come quando usiamo un valore aritmetico in una condizione, se il puntatore è 0, allora la condizione è falsa:
int valore_int = 1024;
// punt_int è un puntatore valido, null
int *punt_int = 0;
// punt_int2 è un puntatore valido che contiene l'indirizzo di valore_int
int *punt_int2 = &valore_int;
// punt_int ha valore 0, quindi la condizione valuta come falso
if (punt_int)
// ...
// punt_int2 punta a valore_int, quindi non è 0; la condizione valuta come vero
if (punt_int2)
// ...
Qualsiasi puntatore non zero viene valutato come vero.
Dati due puntatori validi dello stesso tipo, possiamo confrontarli usando gli operatori di uguaglianza (==
) o disuguaglianza (!=
). Il risultato di questi operatori ha tipo bool
. Due puntatori sono uguali se contengono lo stesso indirizzo e disuguali altrimenti. Due puntatori contengono lo stesso indirizzo (cioè, sono uguali) se sono entrambi null, se indirizzano lo stesso oggetto, o se sono entrambi puntatori uno dopo lo stesso oggetto. Si noti che è possibile per un puntatore a un oggetto e un puntatore uno dopo la fine di un oggetto diverso contenere lo stesso indirizzo. Tali puntatori si confronteranno uguali.
Poiché queste operazioni usano il valore del puntatore, un puntatore usato in una condizione o in un confronto deve essere un puntatore valido. Usare un puntatore non valido come condizione o in un confronto è indefinito.
Puntatori void
Il tipo void *
è un tipo di puntatore speciale che può contenere l'indirizzo di qualsiasi oggetto. Come qualsiasi altro puntatore, un puntatore void *
contiene un indirizzo, ma il tipo dell'oggetto a quell'indirizzo è sconosciuto:
double oggetto = 3.14, *punt_double = &oggetto;
// OK:
// void* può contenere il valore indirizzo
// di qualsiasi tipo puntatore a dati
// oggetto può essere un oggetto di qualsiasi tipo
void *punt_void = &oggetto;
// OK: assegnare un puntatore a double a un puntatore void
// punt_void può contenere un puntatore a qualsiasi tipo
punt_void = punt_double;
Ci sono solo un numero limitato di cose che possiamo fare con un puntatore void *
: Possiamo confrontarlo con un altro puntatore, possiamo passarlo a o restituirlo da una funzione, e possiamo assegnarlo a un altro puntatore void *
. Non possiamo usare un void *
per operare sull'oggetto che indirizza, infatti non conosciamo il tipo di quell'oggetto, e il tipo determina quali operazioni possiamo eseguire sull'oggetto.
Generalmente, usiamo un puntatore void *
per trattare la memoria come memoria, piuttosto che usare il puntatore per accedere all'oggetto memorizzato in quella memoria. Tratteremo l'uso dei puntatori void *
in questo modo nelle prossime lezioni e vedremo come possiamo recuperare l'indirizzo memorizzato in un puntatore void *
.
Esercizi
-
Scrivi codice per cambiare il valore di un puntatore. Scrivi codice per cambiare il valore a cui il puntatore punta.
Soluzione:
int a = 10; int b = 20; int *p = &a; // p punta a a *p = 15; // cambia il valore di a tramite p p = &b; // ora p punta a b *p = 25; // cambia il valore di b tramite p
-
Spiega le differenze chiave tra puntatori e riferimenti.
Soluzione:
- Un riferimento è un alias per un oggetto esistente, mentre un puntatore è una variabile che contiene l'indirizzo di un oggetto.
- Un riferimento deve essere inizializzato al momento della dichiarazione e non può essere cambiato per riferire a un altro oggetto, mentre un puntatore può essere inizializzato in seguito e può essere cambiato per puntare a diversi oggetti.
- I riferimenti non possono essere nulli, mentre i puntatori possono essere nulli (puntatori null).
- I riferimenti sono più sicuri da usare perché non possono essere nulli o non inizializzati, mentre i puntatori richiedono una gestione più attenta per evitare errori di accesso alla memoria.
- Un puntatore è un oggetto a sé stante, mentre un riferimento non lo è.
-
Cosa fa il seguente programma?
int i = 42; int *p1 = &i; *p1 = *p1 * *p1;
Soluzione:
Il programma definisce una variabile intera
i
e la inizializza a 42. Poi definisce un puntatorep1
che punta ai
. Infine, il programma dereferenziap1
per ottenere il valore dii
, lo moltiplica per se stesso (42 * 42 = 1764) e assegna il risultato di nuovo ai
tramite il puntatorep1
. Alla fine del programma, il valore dii
sarà 1764. -
Spiega ciascuna delle seguenti definizioni. Indica se qualcuna è illegale e, se sì, perché.
int i = 0; // Definizione 1 double* punt_double = &i; // Definizione 2 int *punt_int = i; // Definizione 3 int *p = &i;
Soluzione:
- Definizione 1:
double* punt_double = &i;
è illegale perchépunt_double
è un puntatore adouble
, ma&i
è l'indirizzo di una variabile di tipoint
. I tipi non corrispondono. - Definizione 2:
int *punt_int = i;
è illegale perchéi
è una variabile di tipoint
, non un indirizzo. Un puntatore deve essere inizializzato con un indirizzo, non con un valore intero. - Definizione 3:
int *p = &i;
è legale.p
è un puntatore aint
e viene inizializzato con l'indirizzo dii
, che è di tipoint
.
- Definizione 1:
-
Assumendo che
p
sia un puntatore aint
, spiega il seguente codice:if (p) // ... if (*p) // ...
Soluzione:
if (p)
: Questa condizione verifica se il puntatorep
è non nullo. Sep
contiene un indirizzo valido (cioè, punta a un oggetto), la condizione sarà vera. Sep
è nullo (non punta a nessun oggetto), la condizione sarà falsa.if (*p)
: Questa condizione dereferenzia il puntatorep
e verifica il valore dell'oggetto a cuip
punta. Se l'oggetto a cuip
punta ha un valore diverso da zero, la condizione sarà vera. Altrimenti, se l'oggetto ha valore zero, la condizione sarà falsa. È importante notare che questa condizione presuppone chep
sia un puntatore valido; dereferenziare un puntatore nullo causerebbe un comportamento indefinito.
-
Dato un puntatore
p
, puoi determinare sep
punta a un oggetto valido? Se sì, come? Se no, perché no?Soluzione:
Non possiamo determinare con certezza se un puntatore
p
punta a un oggetto valido solo esaminando il valore dip
. Possiamo verificare sep
è nullo (cioè, se non punta a nessun oggetto) confrontandolo connullptr
o0
. Tuttavia, anche sep
non è nullo, non possiamo essere sicuri che punti a un oggetto valido. Potrebbe puntare a un'area di memoria che è stata deallocata o che non è più valida per altri motivi. Pertanto, l'unico modo sicuro per garantire che un puntatore punti a un oggetto valido è assicurarsi che sia stato inizializzato correttamente e che l'oggetto a cui punta sia ancora in vita e accessibile nel contesto del programma. -
Perché l'inizializzazione di
p
è legale ma quella dipunt_long
è illegale?int i = 42; void *p = &i; long *punt_long = &i;
Soluzione:
L'inizializzazione di
p
è legale perchép
è un puntatore di tipovoid*
, che può contenere l'indirizzo di qualsiasi tipo di oggetto. In questo caso,&i
è l'indirizzo di una variabile di tipoint
, e assegnarlo a un puntatorevoid*
è consentito. D'altra parte,punt_long
è un puntatore di tipolong*
, e non può essere inizializzato con l'indirizzo di una variabile di tipoint
senza un cast esplicito, poiché i tipi non corrispondono.