Indirizzi e Puntatori
s
Operatore di indirizzo &
L'operatore unario di
indirizzo :
&
restituisce l'indirizzo della locazione di memoria
dell'operando.
L'operando deve essere un
ammissibile l-value. Il valore
restituito dall'operatore non può
essere usato come l-value (in quanto
l'indirizzo di memoria di una variabile non può essere
assegnato in un'istruzione, ma è
predeterminato dal programma).
Esempi (notare l'uso delle parentesi per alterare l'ordine delle
precedenze):
| |
&a |
|
ammesso, purchè
a sia un
l-value |
|
&(a+1)
|
|
non ammesso, in quanto
a+1 non è un
l-value |
|
&(a>b?a:b) |
|
ammesso, in quanto
l'operatore
condizionale può restituire un
l-value,
purchè a e
b siano
l-values |
|
&a
= b |
|
non ammesso, in quanto
l'operatore
& non può restituire un
l-value |
Gli indirizzi di memoria sono rappresentati da numeri
interi, in
byte, e, nelle operazioni di
output, sono scritti, di default, in forma
esadecimale.
[p26]
Cosa sono i puntatori ?
I puntatori sono particolari
tipi del linguaggio. Una variabile di
tipo puntatore è designata a contenere
l'indirizzo di memoria di un'altra variabile (detta variabile
puntata), la quale a sua volta può essere di qualunque
tipo, anche non nativo (persino
un altro puntatore!).
Dichiarazione di una variabile di tipo
puntatore
Benchè gli indirizzi siano numeri
interi e quindi una variabile
puntatore possa contenere solo valori
interi, tuttavia il
C++ (come il
C) pretende che nella dichiarazione
di un puntatore sia specificato anche
il tipo della variabile puntata
(in altre parole un dato puntatore può
puntare solo a un determinato tipo di
variabili, quello specificato nella dichiarazione).
Per ottenere ciò, bisogna usare
l'operatore di dichiarazione :
*
| Es. : |
int *
pointer |
dichiara (e definisce) la variabile
pointer,
puntatore a variabile di
tipo
int |
Nota: nelle
definizioni multiple
*
va ripetuto: in altre parole, l'operatore di
dichiarazione
*
va considerato, dal punto di vista sintattico, un prefisso
dell'identificatore e non un
suffisso del
tipo.
Si può dire pertanto che, a questo punto della nostra conoscenza,
il numero dei tipi del
C++ è "raddoppiato": esistono tanti
tipi di
puntatori quanti sono i
tipi delle variabili puntate.
Un puntatore accetta quasi sempre
il
casting, purchè
il risultato della conversione sia ancora un
puntatore. Tornando all'esempio precedente,
l'operazione di
casting:
(double*)pointer
restituisce un puntatore a una variabile
di tipo
double.
Nota2: nel
casting, invece,
l'operatore di dichiarazione
*
è un suffisso del
tipo. (!)
Si può anche dichiarare un
puntatore a
puntatore.
| Es. : |
double**
pointer_to_pointer |
| |
dichiara (e definisce) la variabile
pointer_to_pointer,
puntatore a
puntatore a variabile di
tipo double
|
Assegnazione di un valore a un puntatore
Sappiamo che gli indirizzi di memoria non possono essere
assegnati da istruzioni di programma,
ma sono determinati automaticamente in fase di esecuzione; quindi non si
possono assegnare valori a un
puntatore, salvo che in questi quattro
casi:
-
a un puntatore è
assegnato il valore
NULL (non punta a "niente");
-
a un puntatore è
assegnato l'indirizzo di una variabile
esistente, restituito dall'operatore
&
( Es. :
int
a;
int*
p;
p =
&a;
);
-
è eseguita un'operazione di allocazione dinamica della memoria
(di cui tratteremo più avanti);
-
a un puntatore è
assegnato il valore che deriva da
un'operazione di aritmetica dei puntatori (vedere prossima
sezione).
Quanto detto per le assegnazioni
vale anche per le inizializzazioni.
Va precisato, comunque, che ogni tentativo di
assegnare valori a un
puntatore in casi diversi da quelli
sopraelencati (per esempio
l'assegnazione
di una
costante) costituisce un errore che
non viene segnalato dal compilatore, ma che può produrre effetti
indesiderabili (o talvolta disastrosi) in fase di esecuzione.
Aritmetica dei puntatori
Abbiamo detto che il valore assunto da un
puntatore è un numero
intero che rappresenta, in
byte, un indirizzo di memoria. Il
C++ (come il
C) ammette le operazioni di
somma fra un
puntatore e un valore
intero (con risultato
puntatore), oppure di
sottrazione fra due
puntatori (con risultato
intero). Tali operazioni vengono però
eseguite in modo "intelligente", cioè tenendo conto del
tipo della variabile puntata.
Per esempio, se si incrementa un
puntatore a
float di
3 unità, in realtà il suo
valore viene incrementato di 12
byte.
Queste regole dell'aritmetica dei puntatori assicurano che il
risultato sia sempre corretto, qualsiasi sia la lunghezza in
byte della variabile puntata. Per
esempio, se p punta a un
elemento di un
array,
p++
punterà all'elemento successivo,
qualunque sia il tipo (anche non
nativo) dell'array.
[p27]
Operatore di dereferenziazione
*
L'operatore unario di
dereferenziazione
*
(che abbrevieremo in deref.) di un
puntatore restituisce il valore della
variabile puntata
dall'operando ed ha un duplice significato:
-
usato come r-value, esegue un'operazione
di estrazione.
Es. a
=
*p ;
(assegna ad
a il valore della variabile puntata
da p)
-
usato come l-value, esegue un'operazione
di inserimento.
Es.
*p =
a ;
(assegna il valore di
a alla variabile puntata da
p)
In pratica l'operazione di deref.
è inversa a quella di
indirizzo. Infatti, se
assegniamo a un
puntatore
p
l'indirizzo di una variabile
a,
p
=
&a
;
allora la relazione logica:
*p ==
a risulta vera,
cioè la deref. di
p coincide con
a.
Ovviamente non è detto il contrario, cioè, se
assegniamo alla
deref. di
p il valore di
a,
p
=
&b
;
*p =
a ;
ciò non comporta automaticamente che in
p si ritrovi l'indirizzo di
a (dove invece resta l'indirizzo di
b), ma semplicemente che il valore della
variabile puntata da p (cioè
b) coinciderà con
a.
Puntatori a
void
Contrariamente all'apparenza un
puntatore dichiarato a
void,
es.:
void*
vptr;
può puntare a qualsiasi
tipo di variabile. Ne
consegue che a un puntatore a
void si può
assegnare il valore di qualunque
puntatore, ma non viceversa (è
necessario operare il casting).
| Es.: |
definiti: |
int*
iptr;
e
void*
vptr; |
|
è ammessa
l'assegnazione: |
vptr
=
iptr; |
|
ma non: |
iptr
=
vptr; |
|
bensì: |
iptr
=
(int*)vptr; |
I puntatori a
void non possono essere
dereferenziati nè possono essere
inseriti in operazioni di aritmetica dei puntatori. In generale si
usano quando il tipo della variabile
puntata non è ancora stabilito al momento della definizione
del puntatore, ma è determinato
successivamente, in base al flusso di esecuzione del programma.
Errori di dangling references
In C++ (come in
C)
l'assegnazione
dell'indirizzo di una variabile
a a un
puntatore
p :
p =
&a
;
e il successivo accesso ad a tramite
deref. di
p, possono portare a errori di
dangling references (perdita degli agganci) se
puntatore e variabile puntata non
condividono lo stesso ambito d'azione.
Infatti, se l'ambito di
p è più esteso di quello
di a (per esempio se
p è una variabile
globale) e
a va out of scope mentre
p continua ad essere visibile, la
deref. di
p accede ad un'area della memoria non
più allocata al programma, con risultati spesso imprevedibili.
[p28]
Funzioni con argomenti puntatori
Quando, nella chiamata di una
funzione, si passa come
argomento un indirizzo (sia che
si tratti di una variabile puntatore oppure
del risultato di un'operazione di
indirizzo), per esempio (essendo, al solito,
p un
puntatore e
a una qualsiasi variabile):
| funz(....
p ....)
|
oppure |
funz(....
&a
....) |
nella definizione (e ovviamente anche nella dichiarazione)
della funzione il corrispondente
argomento va dichiarato come
puntatore; continuando l'esempio (se
a è di tipo
int):
void
funz(....
int*
p
....)
L'argomento è, come sempre,
passato by value. In C++ è
anche possibile, passarlo by reference, nel qual caso bisogna
indicare entrambi gli operatori di dichiarazione
* e
& :
void
funz(....
int*& p
....)
Se il puntatore è passato
by value, nella funzione
viene creata una copia del
puntatore e, qualsiasi modifica venga
fatta al suo valore, il corrispondente valore nel programma chiamante
rimane inalterato. In questo caso, tuttavia, tramite
l'operazione di
deref., la variabile puntata (che
si trova nel programma chiamante), è accessibile e modificabile
dall'interno della funzione.
| Es.: |
programma chiamante: |
int
a =
10;
......
funz(&a); |
|
funzione: |
void
funz(
int*
p)
{
....*p =
*p+5; ....
} |
alla fine, nella variabile a si trova
il valore 15 (in questo caso non esistono
problemi di scope, in quanto la variabile
a, pur non essendo direttamente visibile
dalla funzione, è ancora in
vita e quindi è accessibile tramite
un'operazione di
deref.).
Per i motivi suddetti, quando
l'argomento della chiamata è
un indirizzo, si dice impropriamente che la variabile
puntata è trasmessa by address e che, per questa
ragione, è modificabile. In realtà
l'argomento non è la
variabile puntata, ma il
puntatore, e questo è trasmesso,
come ogni altra variabile, by value.
[p29]