Tipi Composti e Dichiarazioni in C++
- Un tipo composto è un tipo che è definito in termini di un altro tipo.
- Una dichiarazione è un tipo base seguito da una lista di dichiaratori.
- Ogni dichiaratore nomina una variabile e dà alla variabile un tipo che è correlato al tipo base.
- I dichiaratori più complicati specificano variabili con tipi composti che sono costruiti a partire dal tipo base della dichiarazione.
- Il simbolo
&
ha due significati distinti: in una definizione di variabile,&
fa parte del dichiaratore e indica che la variabile è un riferimento; quando&
appare in un'espressione, è l'operatore "address of" che restituisce l'indirizzo di un oggetto. - Non c'è un unico modo corretto per definire puntatori o riferimenti. La cosa importante è scegliere uno stile e usarlo in modo coerente.
- Possiamo avere puntatori a puntatori e riferimenti a puntatori, ma non possiamo avere puntatori a riferimenti.
Tipi Composti
Un tipo composto è un tipo che è definito in termini di un altro tipo. C++ ha diversi tipi composti. Nelle precedenti lezioni abbiamo già visto due tipi composti: i riferimenti e i puntatori.
Il C++ permette anche di definire tipi composti più complessi, come classi, strutture, unioni e array. Questi tipi composti saranno trattati in lezioni successive.
Definire variabili di tipo composto è più complicato delle dichiarazioni che abbiamo visto finora. Abbiamo visto che le dichiarazioni semplici consistono in un tipo seguito da una lista di nomi di variabili. Più in generale, una dichiarazione è un tipo base seguito da una lista di dichiaratori. Ogni dichiaratore nomina una variabile e dà alla variabile un tipo che è correlato al tipo base.
Le dichiarazioni che abbiamo visto finora hanno dichiaratori che non sono altro che nomi di variabili. Il tipo di tali variabili è il tipo base della dichiarazione. Dichiaratori più complicati specificano variabili con tipi composti che sono costruiti a partire dal tipo base della dichiarazione.
Comprendere le Dichiarazioni di Tipi Composti
Come abbiamo visto, una definizione di variabile consiste in un tipo base e una lista di dichiaratori. Ogni dichiaratore può relazionare la sua variabile al tipo base in modo diverso dagli altri dichiaratori nella stessa definizione. Quindi, una singola definizione potrebbe definire variabili di tipi diversi:
// i è un int; p è un puntatore a int; r è un riferimento a int
int i = 1024, *p = &i, &r = i;
Attenzione alla confusione tra &
di un riferimento e &
dell'operatore "address of"
Molti programmatori sono confusi dall'interazione tra il tipo base e la modifica del tipo che può essere parte di un dichiaratore.
In particolare, il simbolo &
ha due significati distinti. In una definizione di variabile, &
fa parte del dichiaratore e indica che la variabile è un riferimento. Tuttavia, quando &
appare in un'espressione, è l'operatore "address of" che restituisce l'indirizzo di un oggetto.
Definire Più Variabili
È un malinteso comune pensare che il modificatore di tipo *
o &
si applichi a tutte le variabili definite in una singola istruzione. Parte del problema nasce perché possiamo mettere spazi bianchi tra il modificatore di tipo e il nome dichiarato:
// legale ma potrebbe essere fuorviante
int* p;
Diciamo che questa definizione potrebbe essere fuorviante perché suggerisce che int *
è il tipo di ogni variabile dichiarata in quell'istruzione. Nonostante le apparenze, il tipo base di questa dichiarazione è int
, non int *
. Il *
modifica il tipo di p
. Non dice nulla su altri oggetti che potrebbero essere dichiarati nella stessa istruzione:
// p1 è un puntatore a int; p2 è un int
int* p1, p2;
Ci sono due stili comuni usati per definire variabili multiple con tipo puntatore o riferimento. Il primo colloca il modificatore di tipo adiacente all'identificatore:
// sia p1 che p2 sono puntatori a int
int *p1, *p2;
Questo stile enfatizza che la variabile ha il tipo composto indicato.
Il secondo colloca il modificatore di tipo con il tipo ma definisce solo una variabile per istruzione:
// p1 è un puntatore a int
int* p1;
// p2 è un puntatore a int
int* p2;
Questo stile enfatizza che la dichiarazione definisce un tipo composto.
Non c'è un unico modo corretto per definire puntatori o riferimenti. La cosa importante è scegliere uno stile e usarlo in modo coerente.
In questo corso usiamo il primo stile e collochiamo il *
(o il &
) con il nome della variabile.
Puntatori a Puntatori
In generale, non ci sono limiti a quanti modificatori di tipo possono essere applicati a un dichiaratore. Quando c'è più di un modificatore, si combinano in modi che sono logici ma non sempre ovvi. Come esempio, consideriamo un puntatore. Un puntatore è un oggetto in memoria, quindi come qualsiasi oggetto ha un indirizzo. Pertanto, possiamo memorizzare l'indirizzo di un puntatore in un altro puntatore.
Indichiamo ogni livello di puntatore con il proprio *
. Cioè, scriviamo **
per un puntatore a un puntatore, ***
per un puntatore a un puntatore a un puntatore, e così via:
int valore_int = 1024;
// punt_int punta a un int
int *punt_int = &valore_int;
// punt_punt_int punta a un puntatore a un int
int **punt_punt_int = &punt_int;
Qui punt_int
è un puntatore a un int
e punt_punt_int
è un puntatore a un puntatore a un int
.
Potremmo rappresentare questi oggetti come
+-----------------+ +-----------------+ +-----------------+
| valore_int | | punt_int | | punt_punt_int |
| 1024 | | (indirizzo di | | (indirizzo di |
| |<-----| valore_int) |<-----| punt_int) |
+-----------------+ +-----------------+ +-----------------+
Proprio come dereferenziare un puntatore a un int
produce un int
, dereferenziare un puntatore a un puntatore produce un puntatore. Per accedere all'oggetto sottostante, dobbiamo dereferenziare il puntatore originale due volte:
cout << "Il valore di valore_int\n"
<< "valore diretto: " << valore_int << "\n"
<< "valore indiretto: " << *punt_int << "\n"
<< "valore doppiamente indiretto: " << **punt_punt_int
<< endl;
Questo programma stampa il valore di valore_int
in tre modi diversi: primo, direttamente; poi, attraverso il puntatore a int
in punt_int
; e infine, dereferenziando punt_punt_int
due volte per arrivare al valore sottostante in valore_int
.
Riferimenti a Puntatori
Un riferimento non è un oggetto. Quindi, non possiamo avere un puntatore a un riferimento. Tuttavia, poiché un puntatore è un oggetto, possiamo definire un riferimento a un puntatore:
int i = 42;
// p è un puntatore a int
int *p;
// r è un riferimento al puntatore p
int *&r = p;
// r si riferisce a un puntatore; assegnare &i a r fa puntare p a i
r = &i;
// dereferenziare r produce i, l'oggetto a cui p punta; cambia i a 0
*r = 0;
Il modo più facile per comprendere il tipo di r
è leggere la definizione da destra a sinistra. Il simbolo più vicino al nome della variabile (in questo caso il &
in &r
) è quello che ha l'effetto più immediato sul tipo della variabile. Quindi, sappiamo che r
è un riferimento. Il resto del dichiaratore determina il tipo a cui r
si riferisce. Il simbolo successivo, *
in questo caso, dice che il tipo a cui r
si riferisce è un tipo puntatore. Infine, il tipo base della dichiarazione dice che r
è un riferimento a un puntatore a un int
.
Può essere più facile comprendere dichiarazioni complicate di puntatori o riferimenti se le leggiamo da destra a sinistra.
Esercizi
-
Determina i tipi e i valori di ciascuna delle seguenti variabili:
int* punt_int, i, &r = i; int i, *punt_int = 0; int* punt_int, punt_int2;
Soluzione:
punt_int
è un puntatore aint
,i
è unint
non inizializzato,r
è un riferimento ai
.i
è unint
non inizializzato,punt_int
è un puntatore aint
inizializzato anullptr
.punt_int
è un puntatore aint
,punt_int2
è unint
non inizializzato (non un puntatore).