trad. italiana e adattamento a cura di Giacomo Grande
I Riferimenti sono come i puntatori costanti, che sono dereferenziati
automaticamente dal compilatore.
Benchè
i riferimenti esistano anche in Pascal, la versione del C++ è stata presa
dal linguaggio Algol. È essenziale in C++ supportare la sintassi dell'overloading
degli operatori (vedi Capitolo 12), ma c'è anche una generale convenienza nel controllare come gli argomenti vengono
passati dentro e fuori le funzioni.
In
questo capitolo daremo prima brevemente uno sguardo alle differenze tra i puntatoti in C e C++, per poi introdurre
i riferimenti. Ma la parte più consistente del capitolo è rivolta a indagare
su un aspetto abbastanza confuso per i nuovi programmatori in C++:
il costruttore di copia, uno speciale costruttore (che richiede i riferimenti)
che costruisce un nuovo oggetto a partire da un oggetto già esistente dello
stesso tipo. Il costruttore di copia viene usato dal compilatore per passare
alle funzioni e ritornare dalle funzioni oggetti per valore.
Infine viene fatta luce su una caratteristica oscura del C++, che è il puntatore-a-membro.
La differenza principale che c'è tra i puntatori in
C e quelli in C++ è che il C++ è un linguaggio fortemente tipizzato. Questo
viene fuori, ad esempio, laddove abbiamo a che fare con void*. Il
C non permette di assegnare a caso un puntatore di un tipo ad un altro tipo,
ma esso permette di fare ciò attraverso void*. Ad esempio:
bird* b; rock* r; void* v; v = r; b = v;
Siccome questa "caratteristica" del C permette di trattare,
di nascosto, qualunque tipo come qualunque altro, esso apre una voragine nel
sistema dei tipi. Il C++ non permette ciò; il compilatore da un messaggio
di errore, e se si vuole davvero trattare un tipo come un altro, bisogna farlo
esplicitamente usando il cast, sia per il compilatore che per il lettore.
(Il Capitolo 3 ha introdotto la sintassi migliorativa del casting "esplicito"
del C++.)
Un riferimento (&) è come un puntatore
costante, che viene automaticamente dereferenziato. Viene usato generalmente
per le liste di argomenti e per i valori di ritorno delle funzioni. Ma si
possono costruire anche riferimenti isolati. Per esempio,
//: C11:FreeStandingReferences.cpp #include <iostream> using namespace std; // Riferimento ordinario isolato: int y; int& r = y; // Quando viene creato un riferimento, deve essere // inizializzato per riferirsi ad un oggetto vero. // Tuttavia si può anche scrivere: const int& q = 12; // (1) // I riferimenti sono legati all'indirizzo di memoria di qualcos'altro: int x = 0; // (2) int& a = x; // (3) int main() { cout << "x = " << x << ", a = " << a << endl; a++; cout << "x = " << x << ", a = " << a << endl; } ///:~
Nella linea (1), il compilatore alloca uno spazio
di memoria, lo inizializza con il valore 12 e fissa un riferimento a questo
spazio di memoria. Il punto è che un riferimento deve essere legato a
qualche altro pezzo di memoria. Quando si accede ad un riferimento,
di fatto si accede a questo pezzo di memoria. Così, se scriviamo linee
come la (2) e la (3), allora incrementare a corrisponde di fatto
a incrementare x, come è mostrato nel main( ). Diciamo
che il modo più semplice di pensare ad un riferimento è come un puntatore
elaborato. Un vantaggio di questo "puntatore" è che non bisogna preoccuparsi
di inizializzarlo (il compilatore ne forza l'inizializzazione) e come
dereferenziarlo (lo fa il compilatore).
Ci sono determinate regole quando si usano i riferimenti:
Il posto più comune dove si possono trovare
i riferimenti è negli argomenti di funzioni e nei valori di ritorno
delle stesse. Quando un riferimento viene usato come argomento di
una funzione, ogni modifica al riferimento dentro la funzione
causa cambiamenti all'argomento fuori dalla funzione. Naturalmente
si può ottenere lo stesso effetto con un puntatore, ma un riferimento
ha una sintassi molto più pulita. (Si può pensare, se volete, ai
riferimenti come una pura convenienza sintattica.)
Se si ritorna un riferimento da una funzione,
bisogna usare la stessa accortezza che si userebbe se si ritornasse
un puntatore. Qualunque sia l'oggetto a cui un riferimento è connesso,
questo non deve essere buttato via al ritorno dalla funzione, altrimenti
si avrebbe un riferimento a una zona di memoria sconosciuta.
Qui c'è un esempio:
//: C11:Reference.cpp // Semplici riferimenti del C++ int* f(int* x) { (*x)++; return x; // Sicuro, x è fuori da questo scope } int& g(int& x) { x++; // Lo stesso effetto che in f() return x; // Sicuro, fuori da questo scope } int& h() { int q; //! return q; // Errore static int x; return x; // Sicuro, x vive fuori da questo scope } int main() { int a = 0; f(&a); // Brutto (ma esplicito) g(a); // Pulito (ma nascosto) } ///:~
La chiamata alla funzione f( )
non ha la stessa convenienza e chiarezza dell'uso del riferimento,
ma è chiaro che viene passato un indirizzo. Nella chiamata
alla funzione g( ), viene passato un indirizzo (attraverso
un riferimento), ma non lo si vede.
L'argomento passato per riferimento
in Reference.cpp funziona solo quando l'argomento è
un oggetto non-const. Se è un oggetto const,
la funzione g( ) non accetta l'argomento, il che
in effetti è una buona cosa, perchè la funzione effettua
modifiche all'argomento esterno. Se sappiamo che la funzione
rispetterà la constanza
di un oggetto, allora porre l'argomento come riferimento const
permette di usare la funzione in qualunque situazione. Questo
significa che, per i tipi predefiniti, la funzione non deve
modificare gli argomenti, mentre per i tipi definiti dall'utente
la funzione deve chiamare solo funzioni membro const
e non deve modificare nessun dato membro public.
L'uso di riferimenti const
negli argomenti di funzioni è importante soprattutto perchè la funzione può
ricevere oggetti temporanei. Questo può essere stato creato
come valore di ritorno da un'altra funzione o esplicitamente
dall'utente della nostra funzione. Gli oggetti temporanei
sono sempre const, per cui se non si usa un riferimento
const, l'argomento non viene accettato dal compilatore.
Come esempio molto semplice,
//: C11:ConstReferenceArguments.cpp // Passaggio dei riferimenti come const void f(int&) {} void g(const int&) {} int main() { //! f(1); // Errore g(1); } ///:~
La chiamata a f(1) causa un errore di compilazione,
perchè il compilatore deve prima creare un riferimento.
E questo lo fa allocando memoria per un int, inizializzandola
a 1 e producendo l'indirizzo da legare al riferimento.
Il dato memorizzato deve essere const perchè
cambiarlo non ha senso – non si possono mai mettere
le mani su di esso. Bisogna fare la stessa assunzione
per tutti gli oggetti temporanei: cioè che sono inaccessibili.
Il compilatore è in grado di valutare quando si sta cambiando
un dato del genere, perchè il risultato sarebbe una perdita
di informazione.
In C, se si vuole modificare il
contenuto di un puntatore piuttosto che l'oggetto
a cui punta, bisogna dichiarare una funzione come questa:
void f(int**);
e bisogna prendere l'indirizzo
del puntatore quando lo si passa alla funzione:
int i = 47; int* ip = &i; f(&ip);
Con i riferimenti in C++,
la sintassi è piu chiara. L'argomento della funzione
diventa un riferimento a un puntatore, e non bisogna
prendere l'indirizzo del puntatore.
//: C11:ReferenceToPointer.cpp #include <iostream> using namespace std; void increment(int*& i) { i++; } int main() { int* i = 0; cout << "i = " << i << endl; increment(i); cout << "i = " << i << endl; } ///:~
Facendo girare questo programma
si può provare che è il puntatore ad essere incrementato
e non ciò a cui punta.
Dovrebbe essere normale
abitudine passare gli argomenti ad una funzione
come riferimento const. Anche se questo
potrebbe sulle prime sembrare solo un problema
di efficienza (e noi non vogliamo preoccuparci
di affinare l'efficienza mentre progettiamo e
implementiamo un programma), c'è molto di più
in gioco: come vedremo nel resto del capitolo,
per passare un oggetto per valore è necessario
un costruttore di copia e questo non sempre è
disponibile.
Il miglioramento dell'efficienza
può essere sostanziale se si usa una tale accortezza:
passare un argomento per valore richiede la
chiamata a un costruttore e a un distruttore,
ma se non si deve modificare l'argomento, il
passaggio come riferimento const richiede
soltanto un push di un indirizzo sullo stack.
Infatti, l'unica occasione
in cui non è preferibile passare un indirizzo è quando si devono fare tali modifiche all'oggeto
che il passaggio per valore è il solo approccio
sicuro (piuttosto che modificare l'oggetto
esterno, cosa che il chiamante in genere non
si aspetta). Questo è l'argomento della prossima
sezione.
Adesso che abbiamo
capito i concetti base dei riferimenti in
C++, siamo pronti per affrontare uno dei
concetti più confusi del linguaggio: il
costruttore di copia, spesso chiamato X(X&)
("rif X di X "). Questo costruttore è essenziale
per controllare il passaggio e il ritorno
di tipi definiti dall'utente durante le
chiamate a funzioni. È così importante,
di fatto, che il compilatore sintetizza
automaticamente un costruttore di copia
se non se ne fornisce uno, come vedremo.
Per comprendere
la necessità del costruttore di copia,
consideriamo il modo in cui il C gestisce
il passaggio e il ritorno di variabili
per valore durante le chiamate a funzioni.
Se dichiariamo una funzione e la chiamiamo,
int f(int x, char c); int g = f(a, b);
come fa il compilatore
a sapere come passare e restituire queste
variabili? È gia noto! Il range dei
tipi con cui ha a che fare è così
piccolo – char, int,
float, double, e le loro
varianti – che questa informazione è già presente nel compilatore.
Se facciamo
generare il codice assembly al nostro
compilatore e andiamo a vedere le
istruzioni generate per la chiamata
alla funzione f( ), vedremmo
qualcosa equivalente a:
push b push a call f() add sp,4 mov g, register a
Questo codice è stato significativamente ripulito
per renderlo generico; le espressioni
per b ed a potrebbero
essere diverse, a seconda che le
variabili sono globali (in qual
caso esse saranno _b e _a)
o locali (il compilatore le indicizzerà
a partire dallo stack pointer).
Questo è vero anche per l'espressione
di g. La forma della chiamata
ad f( ) dipenderà dallo
schema di decorazione dei nomi,
e "register a" dipende da come sono
chiamati i registri della CPU all'interno
dell'assembler della macchina. La
logica che c'è dietro, comunque,
rimane la stessa.
In C e C++,
dapprima vengono messi sullo stack
gli argomenti, da destra a sinistra,
e poi viene effettuata la chiamata
alla funzione. Il codice di chiamata è responsabile della cancellazione
degli argomenti dallo stack (cosa
che giustifica l'istruzione add
sp,4). Ma bisogna notare che
per passare gli argomenti per
valore, il compilatore ne mette
semplicemente una copia sullo
stack – esso ne conosce
le dimensioni e quindi il push
di questi argomenti ne produce
una copia accurata.
Il valore
di ritorno di f( ) è messo in un registro. Di
nuovo, il compilatore conosce
tutto ciò che c'è da conoscere
riguardo al tipo di valore da
restituire, in quanto questo
tipo è incorporato nel linguaggio
e il compilatore lo può restituire
mettendolo in un registro. Con
i tipi primitivi del C, il semplice
atto di copiare i bit del valore è equivalente a copiare l'oggetto.
Ma adesso
consideriamo i tipi definiti
dall'utente. Se creiamo una
classe e vogliamo passare
un oggetto di questa classe
per valore, come possiamo
supporre che il compilatore
sappia cosa fare? Questo non è un tipo incorporato nel
compilatore, ma un tipo che
abbiamo creato noi.
Per
investigare sul problema,
possiamo iniziare con una
semplice struttura che è
chiaramente troppo grande
per essere restituita in
un registro:
//: C11:PassingBigStructures.cpp struct Big { char buf[100]; int i; long d; } B, B2; Big bigfun(Big b) { b.i = 100; // Opera sull'argomento return b; } int main() { B2 = bigfun(B); } ///:~
Decodificare
l'output in assembly in
questo caso è un pò
più complicato perchè
molti compilatori usano
funzioni di "appoggio"
invece di mettere tutte
le funzionalità inline.
Nel main( ),
la chiamata a bigfun
( ) parte come
possiamo immaginare –
l'intero contenuto di
B è messo sullo
stack. (Qui alcuni compilatori
caricano dei registri
con l'indirizzo di Big
e la sua dimensione e
poi chiamano una funzione
di appoggio per mettere
Big sullo stack.)
Nel
frammento di codice
precedente, mettere
gli argomenti sullo
stack era tutto ciò
che era richiesto prima
della chiamata alla
funzione. In PassingBigStructures.cpp,
tuttavia, possiamo vedere
un'azione in più: prima
di effettuare la chiamata
viene fatto il push
dell'indirizzo di B2
anche se è ovvio che
non è un argomento.
Per capire cosa succede
qui, è necessario capire
quali sono i vincoli
del compilatore quando
effettua una chiamata
ad una funzione.
Quando
il compilatore genera
codice per una chiamata
a funzione, prima
mette gli argomenti
sullo stack e poi
effettua la chiamata.
All'interno della
funzione viene generato
del codice per far
avanzare lo stack
pointer anche più
di quanto necessario
per fornire spazio
alle variabili locali
della funzione. ("Avanzare"
può significare spostare
in giù o in su a
seconda della macchina.)
Ma durante una CALL
in linguaggio assembly
la
CPU fa il push
dell'indirizzo del
codice di programma
da cui la funzione è stata chiamata,
così l'istruzione
assembly RETURN può
usare questo indirizzo
per ritornare nel
punto della chiamata.
Questo indirizzo naturalmente è sacro, perchè
senza di esso il programma
viene completamente
perso. Qui viene mostrato
come potrebbe apparire
la struttura di stack
dopo una CALL e l'allocazione
di variabili locali
in una funzione:
Argomenti della funzione |
Indirizzo di ritorno |
Variabili locali |
Il
codice generato
per il resto della
funzione si aspetta
che la memoria sia
srutturata esattamente
in questo modo,
in modo tale che
esso può tranquillamente
pescare tra gli
argomenti della
funzione e le variabili
locali senza toccare
l'indirizzo di ritorno.
Chiameremo questo
blocco di memoria,
che contiene tutto
ciò che viene usato
dalla funzione durante
il processo di chiamata, struttura di funzione (function frame).
Possiamo
pensare che è
ragionevole tentare
di restituire
i valori attraverso
lo stack. Il compilatore
dovrebbe semplicemente
metterli sullo
stack, e la funzione
dovrebbe restituire
un offset
per indicare
la posizione nello
stack da cui inizia
il valore di ritorno.
Il
problema sussiste
perchè le funzioni
in C e C++ supportano
gli interrupt;
cioè i linguaggi
sono ri-entranti.
Essi supportano
anche le chiamate
ricorsive alle
funzioni. Questo
significa che
in qualsiasi
punto dell'esecuzione
del programma
può arrivare
un interrupt,
senza interrompere
il programma.
Naturalmente
la persona che
scrive la routine
di gestione
dell'interrupt
(interrupt service
routine, ISR) è responsabile
del salvataggio
e del ripristino
di tutti i registri
usati nella
ISR, ma se l'ISR
ha bisogno di
ulteriore memoria
sullo stack,
questo deve
essere fatto
in maniera sicura.
(Si può pensare
ad un ISR come
ad una normale
funzione senza
argomenti e
con un valore
di ritorno void
che salva e
ripristina lo
stato della
CPU. La chiamata
ad una ISR è
scatenata da
qualche evento
hardware piuttosto
che da una chiamata
esplicita all'interno
del programma.)
Adesso
proviamo ad
immaginare
cosa potrebbe
succedere
se una funzione
ordinaria
provasse a
restituire
un valore
attraverso
lo stack.
Non si può
toccare nessuna
parte dello
stack che
si trova al
di sopra dell'indirizzo
di ritorno,
così la funzione
dovrebbe mettere
i valori al
di sotto dell'indirizzo
di ritorno.
Ma quando
viene eseguita
l'istruzione
assembly RETURN lo stack pointer deve puntare all'indirizzo di ritorno (o al
di sotto di
esso, dipende
dalla macchina),
così appena
prima del
RETURN la
funzione deve
spostare in
su lo stack
pointer, tagliando
fuori in questo
modo tutte
le variabili
locali. Se
proviamo a
ritornare
dei valori
sullo stack
al di sotto
dell'indirizzo
di ritorno, è proprio
in questo
momento che
diventiamo
vulnerabili,
perchè potrebbe
arrivare un
interrupt.
L' ISR sposterà
in avanti
lo stack pointer
per memorizzare
il suo indirizzo
di ritorno
e le sue variabili
locali e così
sovrascrive
il nostro
valore di
ritorno.
Per
risolvere
questo problema
il chiamante
dovrebbe
essere responsabile
dell'allocazione
di uno spazio
extra sullo
stack per
il valore
di ritorno,
prima di
chiamare
la funzione.
Tuttavia,
il C non è stato
progettato
per fare
questo,
e il C++ deve mantenere la compatibilità. Come
vedremo
brevemente,
il
compilatore
C++ usa
uno schema
molto più
efficiente.
Un'altra
idea potrebbe
essere
quella
di restituire
il valore
in qualche
area dati
globale,
ma anche
questa
non può
funzionare.
Rientranza
vuol dire
che qualunque
funzione
può essere
una routine
di interrupt
per qualunque
altra
funzione,
inclusa
la funzione
corrente.
Così,
se mettiamo
il valore
di ritorno
in un'area
globale,
possiamo
ritornare
nella
stessa
funzione,
che a
questo
punto
sovrascrive
il valore
di ritorno.
La stessa
logica
si applica
alla ricorsione.
L'unico
posto
sicuro
dove
restituire
i valori
sono
i registri,
così
siamo
di nuovo
al problema
di cosa
fare
quando
i registri
non
sono
grandi
abbastanza
per
memorizzare
il valore
di ritorno.
La risposta è nel
mettere
sullo
stack
come
un argomento
della
funzione
l'indirizzo
dell'area
di destinazione
del
valore
di ritorno,
e far
si che
la funzione
copi
le informazioni
di ritorno
direttamente
nell'area
di destinazione.
Questo
non
solo
risolve
tutti
i problemi,
ma è
molto
efficiente.
Questo è anche
il motivo
per
cui,
in PassingBigStructures.cpp,
il compilatore
fa il
push
dell'indirizzo
di B2
prima
di chiamare
bigfun( )
nel
main( ).
Se guardiamo
all'output
in assembly
di bigfun( ),
possiamo
vedere
che
essa
si aspetta
questo
argomento
nascosto
e ne
effettua
la copia
nell'area
di destinazione
all'interno
della
funzione.
Fin
qui
tutto
bene.
Abbiamo
una
strada
percorribile
per
il
passaggio
e
il
ritorno
di
grosse,
ma
semplici,
strutture.
Ma
va
notato
che
tutto
quello
che
abbiamo è
un
modo
per
copiare
bit
da
una
parte
all'altra
della
memoria,
cosa
che
funziona
bene
certamente
per
il
modo
primitivo
in
cui
il
C
vede
le
variabili.
Ma
in
C++
gli
oggetti
possono
essere
molto
più
sofisticati
di
un
mucchio
di
bit;
essi
hanno
un
significato.
Questo
significato
potrebbe
non
rispondere
bene
ad
una
copia
di
bit.
Consideriamo
un
semplice
esempio:
una
classe
che
sa
quanti
oggetti
del
suo
tipo
ci
sono
in
ogni
istante.
Dal
Capitolo
10
sappiamo
che
per
fare
questo
si
include
un
membro
dati
static:
//: C11:HowMany.cpp // Una classe che conta i suoi oggetti #include <fstream> #include <string> using namespace std; ofstream out("HowMany.out"); class HowMany { static int objectCount; public: HowMany() { objectCount++; } static void print(const string& msg = "") { if(msg.size() != 0) out << msg << ": "; out << "objectCount = " << objectCount << endl; } ~HowMany() { objectCount--; print("~HowMany()"); } }; int HowMany::objectCount = 0; // Passaggio e ritorno PER VALORE: HowMany f(HowMany x) { x.print("x argument inside f()"); return x; } int main() { HowMany h; HowMany::print("after construction of h"); HowMany h2 = f(h); HowMany::print("after call to f()"); } ///:~
La
classe HowMany contiene uno static
int
objectCount
e
una
funzione
membro
static
print( ),
per
riportare
il
valore
di
objectCount,
con
un
argomento
opzionale
per
stampare
un
messaggio.
Il
costruttore
incrementa
il
contatore
ogni
volta
che
viene
creato
un
oggetto,
e
il
distruttore
lo
decrementa.
Il
risultato,
tuttavia,
non è
quello
che
ci
si
aspetta:
after construction of h: objectCount = 1 x argument inside f(): objectCount = 1 ~HowMany(): objectCount = 0 after call to f(): objectCount = 0 ~HowMany(): objectCount = -1 ~HowMany(): objectCount = -2
Dopo
che è
stato
creato
h,
il
contatore
di
oggetti
vale
1,
il
che è
giusto.
Ma
dopo
la
chiamata
ad
f( )
ci
si
aspetta
di
avere
il
contatore
di
oggetti
a
due,
perchè
viene
creato
anche
l'oggetto
h2.
Invece
il
contatore
vale
zero,
il
che
indica
che
qualcosa è
andata
terribilmente
storta.
Questo
viene
confermato
dal
fatto
che
i
due
distruttori
alla
fine
portano
il
contatore
a
valori
negativi,
cosa
che
non
dovrebbe
mai
succedere.
Guardiamo
dentro
f( )
cosa
succede
dopo
che è
stato
passato
l'argomento
per
valore.
Il
passaggio
per
valore
significa
che
l'oggetto
h
esiste
al
di
fuori
della
struttura
della
funzione,
e
che
c'è
un
oggetto
addizionale
all'interno
della
struttura
della
funzione,
che è
la
copia
che è
stata
passata
per
valore.
Tuttavia
l'argomento è
stato
passato
usando
il
concetto
primitivo
del
C
della
copia
per
bit,
mentre
la
classe
HowMany
del
C++
richiede
una
vera
inizializzazione
per
mantenere
l'integrità,
perciò
la
semplice
copia
per
bit
fallisce
nella
produzione
dell'effetto
desiderato.
Quando
la
copia
locale
dell'oggeto
esce
fuori
scope
alla
fine
della
chiamata
ad
f( ),
viene
chiamato
il
distruttore,
che
decrementa
objectCount,
per
cui
al
di
fuori
della
funzione
objectCount
vale
zero.
Anche
la
creazione
di
h2
viene
fatta
usando
la
copia
per
bit,
per
cui
anche
qui
il
costruttore
non
viene
chiamato
e
quando
h
ed
h2
escono
fuori
scope
i
loro
distruttori
producono
valori
negativi
per
objectCount.
Il problema sussiste perchè il compilatore fa un'assunzione su come viene creato un nuovo oggetto a partire da uno già esistente. Quando si passa un oggetto per valore si crea un nuovo oggetto, l'oggetto passato all'interno della funzione, da un oggetto esistente, cioè l'oggetto originale al di fuori della struttura della funzione. Questo spesso è vero anche quando si ritorna un oggetto all'uscita di una funzione. Nell'espressione
HowMany h2 = f(h);
h2, oggetto non preventivamente
costruito,
viene
creato
dal
valore
di
ritorno
della
funzione
f( ),
quindi
di
nuovo
un
oggetto
viene
creato
a
partire
da
uno
esistente.
L'assunzione
del
compilatore è
che
si
vuole
fare
una
creazione
usando
la
copia
per
bit,
e
questo
in
molti
casi
funziona
perfettamente,
ma
in
HowMany
non
funziona,
in
quanto
il
significato
dell'inizializzazione
va
al
di
là
della
semplice
copia
per
bit.
Un
altro
esempio
comune è
quello
delle
classi
che
contengono
puntatori
–
a
cosa
devono
puntare?
Devono
essere
copiati?
Devono
essere
associati
a
qualche
altro
pezzo
di
memoria?
Fortunatamente
si
può
intervenire
in
questo
processo
ed
evitare
che
il
compilatore
faccia
una
copia
per
bit.
Questo
si
fa
definendo
la
propria
funzione
da
usare
ogni
volta
che è
necessario
creare
un
nuovo
oggetto
a
partire
da
uno
esistente.
Logicamente,
siccome
si
costruisce
un
nuovo
oggetto,
questa
funzione è
un
costruttore,
e
altrettanto
logicamente,
l'unico
argomento
di
questo
costruttore
deve
essere
l'oggetto
da
cui
si
sta
effettuando
la
costruzione.
Ma
questo
oggetto
non
può
essere
passato
al
costruttore
per
valore,
in
quanto
stiamo
cercando
di
definire
la
funzione
che
gestisce
il
passaggio
per
valore,
e
sintatticamente
non
ha
senso
passare
un
puntatore,
perchè,
dopotutto,
stiamo
creando
un
nuovo
oggetto
da
quello
esistente.
Qui
i
riferimenti
ci
vengono
in
soccorso,
quindi
prendiamo
il
riferimento
all'oggetto
sorgente.
Questa
funzione è
chiamata
costruttore
di
copia
ed è
spesso
chiamata
X(X&),
se
la
classe è
X.
Se
si
crea
un
costruttore
di
copia,
il
compilatore
non
effettua
una
copia
per
bit
quando
si
crea
un
oggetto
a
partire
da
uno
esistente.
Esso
chiamerà
sempre
il
costruttore
di
copia.
Se
invece
non
si
fornisce
il
costruttore
di
copia,
il
compilatore
effettua
comunque
una
copia,
ma
con
il
costruttore
di
copia
abbiamo
la
possibilità
di
avere
un
controllo
completo
sul
processo.
Adesso è
possibile
risolvere
il
problema
riscontrato
in
HowMany.cpp:
Ci
sono
un
pò
di
modifiche
in
questo
codice
,
in
modo
che
si
possa
avere
un'idea
più
chiara
di
cosa
succede.
Prima
di
tutto
,
la
stringa
name
agisce
da
identificatore
quando
vengono
stampate
informazioni
relative
all'oggetto.
Nel
costruttore,
si
può
mettere
una
stringa
di
identificazione
(generalmente
il
nome
dell'oggetto)
copiato
in
name
usando
il
costruttore
di
string.
Il
default
=
""
crea
una
stringa
vuota.
Il
costruttore
incrementa
ObjectCount
come
prima,
e
il
distruttore
lo
decrementa. Poi
c'è
il
costruttore
di
copia,
HowMany2(const
HowMany2&).
Il
costruttore
di
copia
può
creare
un
oggetto
solo
da
uno
già
esistente,
così
il
nome
dell'oggetto
esistente
viene
copiato
in
name,
seguito
dalla
parola
"copia"
così
si
può
vedere
da
dove
viene.
Se
si
guarda
meglio,
si
può
vedere
che
la
chiamata
name(h.name)
nella
lista
di
inizializzazione
del
costruttore
chiama
il
costruttore
di
copia
di
string. Nel
costruttore
di
copia,
il
contatore
di
oggetti
viene
incrementato
come
nel
normale
costruttore.
In
questo
modo
si
può
ottenere
un
conteggio
accurato
degli
oggetti
quando
c'è
un
passaggio
e
un
ritorno
per
valore. La
funzione
print( ) è
stata
modificata
per
stampare
un
messaggio,
l'identificatore
dell'oggetto
e
il
contatore
degli
oggetti.
Esso
deve
accedere
ora
al
dato
name
di
un
particolare
oggetto,
perciò
non
può
essere
una
funzione
membro
static. All'interno
del
main( ),
si
può
vedere
che è
stata
aggiunta
una
seconda
chiamata
a
f( ).
Tuttavia,
questa
seconda
chiamata
usa
il
comune
approccio
del
C
di
ignorare
il
valore
di
ritorno.
Ma
adesso
che
sappiamo
come
viene
restituito
il
valore
(cioè,
il
codice
all'interno
della
funzione
gestisce
il
processo
di
ritorno,
mettendo
il
risultato
in
un'area
di
destinazione
il
cui
indirizzo
viene
passato
come
argomento
nascosto),
ci
si
potrebbe
chiedere
cosa
succede
quando
il
valore
di
ritorno
viene
ignorato.
Il
risultato
del
programma
potrebbe
fornire
qualche
delucidazione
su
questo. Prima
di
mostrare
il
risultato,
qui
c'è
un
piccolo
programma
che
usa
iostreams
per
aggiungere
il
numero
di
linea
ad
un
file: L'intero
file
viene
letto
dentro
vector<string>,
usando
lo
stesso
codice
già
visto
precedentemente
in
questo
libro.
Quando
si
stampano
i
numeri
di
linea,
si
vorrebbero
avere
tutte
le
linee
allineate
l'una
all'altra,
e
questo
richiede
di
aggiustare
i
numeri
di
linea
nel
file
in
modo
che
la
larghezza
permessa
per
i
numeri
di
linea
sia
coerente.
Possiamo
facilmente
calcolare
il numero di una linea usando vector::size( ),
ma
quello
di
cui
abbiamo
veramente
bisogno è
sapere
se
ci
sono
più
di
10
linee,
100
linee,
1,000
linee,
ecc.
Se
prendiamo
il
logaritmo,
base
10,
del
numero
delle
linee
nel
file,
lo
tronchiamo
ad
un
int
ed
aggiungiamo
uno
al
valore,
calcoliamo
la
larghezza
massima
che
deve
assumere
il
contatore
di
linee. Possiamo
notare
una
coppia
di
strane
chiamate
all'interno
del
ciclo
for:
setf( )
e
width( ).
Queste
sono
chiamate
ostream
che
permettono
di
controllare,
in
questo
caso,
la
giustificazione
e
la
larghezza
dell'output.
Ma
queste
devono
essere
chiamate
ogni
volta
che
viene
stampata
una
linea
ed è
per
questo
che
sono
messe
dentro
un
ciclo
for.
Il
volume
2
di
questo
libro
contiene
un
capitolo
intero
che
spiega
gli
iostreams
e
dice
molte
più
cose
riguardo
a
queste
chiamate,
come
pure
su
altri
modi
di
controllare
iostreams. Quando
Linenum.cpp
viene
applicato
a
HowMany2.out,
il
risultato è Come
ci
si
potrebbe
aspettare,
la
prima
cosa
che
succede è
che
viene
chiamato
il
costruttore
normale
per
h,
che
incrementa
il
contatore
di
oggetti
a
uno.
Ma
dopo,
quando
si
entra
in
f( ),
il
costruttore
di
copia
viene
chiamato
in
modo
trasparente
dal
compilatore
per
effettuare
il
passaggio
per
valore.
Viene
creato
un
nuovo
oggetto,
che è
la
copia
di
h
(da
cui
il
nome
"h
copy")
all'interno
della
struttura
di
f( ),
così
il
contatore
di
oggetti
diventa
due,
grazie
al
costruttore
di
copia. La
linea
otto
indica
l'inizio
del
ritorno
da
f( ).
Ma
prima
che
la
variabile
locale
"h
copy"
possa
essere
distrutta
(essa
esce
fuori
scope
alla
fine
della
funzione),
deve
essere
copiata
nel
valore
di
ritorno,
che è
h2.
L'oggetto
(h2),
non
ancora
costruito,
viene
creato
a
partire
da
un
oggetto
già
esistente
(la
variabile
locale
dentro
f( )),
perciò
il
costruttore
di
copia
viene
di
nuovo
usato
alla
linea
nove.
Adesso
il
nome
dell'identificatore
di h2 diventa "h copy copy" , in quanto
esso
viene
copiato
dalla
copia
locale
ad
f( ).
Dopo
che
l'oggetto è
stato
restituito,
ma
prima
di
uscire
dalla
funzione,
il
contatore
di
oggetti
diventa
temporaneamente
tre,
ma
dopo
l'oggetto
locale
"h
copy"
viene
distrutto.
Dopo
che
la
chiamata
ad
f( ) è
stata
completata,
alla
linea
13,
ci
sono
solo
due
oggetti,
h
e
h2,
e
si
può
vedere
che
h2
finisce
per
essere
la
"copia
della
copia
di
h." La
linea
15
inizia
la
chiamata
ad
f(h),
ignorando
questa
volta
il
valore
di
ritorno.
Si
può
vedere
alla
linea
16
che
il
costruttore
di
copia
viene
chiamato
esattamente
come
prima
per
passare
l'argomento.
E,
come
prima,
la
linea
21
mostra
come
il
costruttore
di
copia
viene
chiamato
per
il
valore
di
ritorno.
Ma
il
costruttore
di
copia
deve
avere
un
indirizzo
su
cui
lavorare
come
sua
destinazione
(un
puntatore
this).
Da
dove
viene
questo
indirizzo? Succede
che
il
compilatore
può
creare
un
oggetto
temporaneo
ogni
qualvolta
ne
ha
bisogno
per
valutare
opportunamente
un'espressione.
In
questo
caso
ne
crea
uno
che
noi
non
vediamo
che
funge
da
destinazione
per
il
valore
di
ritorno,
ignorato,
di
f( ).
La
durata
di
questo
oggetto
temporaneo è
la
più
breve
possibile
in
modo
tale
che
non
ci
siano
in
giro
troppi
oggetti
temporanei
in
attesa
di
essere
distrutti
e
che
impegnano
risorse
preziose.
In
alcuni
casi
l'oggetto
temporaneo
può
essere
passato
immediatamente
ad
un'altra
funzione,
ma
in
questo
caso
esso
non è
necessario
dopo
la
chiamata
alla
funzione
e
quindi
non
appena
la
funzione
termina,
con
la
chiamata
al
distruttore
dell'oggetto
locale
(linee
23
e
24),
l'oggetto
temporaneo
viene
distrutto
(linee
25
e
26). Infine,
alle
linee
28-31,
l'oggetto
h2
viene
distrutto,
seguito
da
h,
e
il
contatore
di
oggetti
torna
correttamente
a
zero. Siccome
il
costruttore
di
copia
implementa
il
passaggio
e
il
ritorno
per
valore, è
importante
che
il
compilatore
ne
crei
uno
di
default
nel
caso
di
semplici
strutture
–
che
poi è
la
stessa
cosa
che
fa
in
C.
Tuttavia,
tutto
quello
che
abbiamo
visto
finora è
il
comportamento
di
default
primitivo:
una
copia
per
bit. Quando
sono
coinvolti
tipi
più
complessi,
il
compilatore
C++
deve
comunque
creare
automaticamente
un
costruttore
di
copia
di
default.
Ma,
di
nuovo,
un
copia
per
bit
non
ha
senso,
perchè
potrebbe
non
implementare
il
significato
corretto. Qui
c'è
un
esempio
che
mostra
un
approccio
più
intelligente
che
può
avere
il
compilatore.
Supponiamo
di
creare
una
nuova
classe
composta
di
oggetti
di
diverse
classi
già
esistenti.
Questo
viene
chiamato,
in
modo
abbastanza
appropriato,
composizione,
ed è
uno
dei
modi
per
costruire
nuove
classi
a
partire
da
classi
già
esistenti.
Adesso
mettiamoci
nei
panni
di
un
utente
inesperto
che
vuole
provare
a
risolvere
velocemente
un
problema
creando
una
nuova
classe
in
questo
modo.
Non
sappiamo
nulla
riguardo
al
costruttore
di
copia,
pertanto
non
lo
creiamo.
L'esempio
mostra
cosa
fa
il
compilatore
mentre
crea
un
costruttore
di
copia
di
default
per
questa
nostra
nuova
classe: La
classe
WithCC
contiene
un
costruttore
di
copia,
che
annuncia
semplicemente
che è
stato
chiamato,
e
questo
solleva
una
questione
interessante.
Nella
classe
Composite,
viene
creato
un
oggetto
di
tipo
WithCC
usando
un
costruttore
di
default.
Se
non
ci
fosse
stato
per
niente
il
costruttore
nella
classe WithCC, il compilatore ne avrebbe
creato
automaticamente
uno
di
default,
che
in
questo
caso
non
avrebbe
fatto
nulla.
Ma
se
aggiungiamo
un
costruttore
di
copia,
diciamo
al
compilatore
che
ci
accingiamo
a
gestire
la
creazione
del
costruttore
ed
esso
non
ne
creerà
uno
per
noi
e
darà
errore,
a
meno
che
non
gli
diciamo
esplicitamente
di
crearne
uno
di
default,
come è
stato
fatto
per
WithCC. La
classe
WoCC
non
ha
un
costruttore
di
copia,
ma
memorizza
un
messaggio
in
una
stringa
interna
che
può
essere
stampata
con
print( ).
Questo
costruttore
viene
esplicitamente
chiamato
nella
lista
di
inizializzatori
del
costruttore
di
Composite
(brevemente
introdotta
nel
Capitolo
8
e
completamente
coperta
nel
Capitolo
14).
La
ragione
di
ciò
sarà
chiara
più
avanti. La
classe
Composite
ha
oggetti
membro
sia
di
tipo
WithCC
che
WoCC
(notare
che
l'oggetto
incorporato
WoCC
viene
inizializzato
nella
lista
di
inizializzatori
del
costruttore,
come
deve
essere),
e
non
ha
un
costruttore
di
copia
esplicitamente
definito.
Tuttavia,
in
main( )
viene
creato
un
oggetto
usando
il
costruttore
di
copia
nella
definizione: Il
costruttore
di
copia
di
Composite
viene
creato
automaticamente
dal
compilatore,
e
l'output
del
programma
rivela
il
modo
in
cui
ciò
avviene: Per
creare
un
costruttore
di
copia
una
classe
che
usa
la
composizione
(e
l'ereditarietà,
che è
introdotta
nel
Capitolo
14),
il
compilatore
chiama
ricorsivamente
i
costruttori
di
copia
per
tutti
gli
oggetti
membri
e
per
le
classi
base.
Cioè,
se
l'oggetto
membro
contiene
un
altro
oggetto,
anche
il
suo
costruttore
di
copia
viene
chiamato.
Così,
in
questo
caso
il
compilatore
chiama
il
costruttore
di
copia
per
WithCC.
L'output
mostra
che
questo
costruttore
viene
chiamato.
Siccome
WoCC
non
ha
un
costruttore
di
copia,
il
compilatore
ne
crea
uno
che
effettua
semplicemente
la
copia
per
bit,
e
lo
chiama
all'interno
del
costruttore
di
copia
di
Composite.
La
chiamata
a
Composite::print( )
in
main
mostra
che
questo
succede
in
quanto
i
contenuti
di c2.WoCC sono identici ai contenuti
di
c.WoCC.
Il
processo
che
il
compilatore
mette
in
atto
per
sintetizzare
un
costruttore
di
copia è
detto
inizializzazione
per
membro
(memberwise
initialization)
. È
sempre
meglio
creare
il
proprio
costruttore
di
copia,
invece
di
farlo
fare
al
compilatore.
Questo
garantisce
che
starà
sotto
il
nostro
controllo. A
questo
punto
forse
vi
gira
un
pò
la
testa
e
vi
state
meravigliando
di
come
sia
stato
possibile
scrivere
finora
codice
funzionante
senza
sapere
nulla
riguardo
al
costruttore
di
copia.
Ma
ricordate:
abbiamo
bisogno
di
un
costruttore
di
copia
solo
se
dobbiamo
passare
un
oggetto
per
valore.
Se
questo
non
succede,
non
abbiamo
bisogno
di
un
costruttore
di
copia. "Ma,"
potreste
dire,
"se
non
forniamo
un
costruttore
di
copia,
il
compilatore
ne
creerà
uno
per
noi.
E
come
facciamo
a
sapere
che
un
oggetto
non
sarà
mai
passato
per
valore?" C'è
una
tecnica
molto
semplice
per
prevenire
il
passaggio
per
valore:
dichiarare
un
costruttore
di
copia
private.
Non
c'è
bisogno
di
fornire
una
definizione,
a
meno
che
una
delle
funzioni
membro
o
una
funzione
friend
non
abbiano
bisogno
di
effettuare
un
passaggio
per
valore.
Se
l'utente
prova
a
passare
o
a
ritornare
un
oggetto
per
valore,
il
compilatore
da
un
messaggio
di
errore,
in
quanto
il
costruttore
di
copia è
private.
Inoltre
esso
non
creerà
un
costruttore
di
copia
di
default,
in
quanto
gli
abbiamo
detto
esplicitamente
che
abbiamo
noi
il
controlo
su
questo. Qui
c'è
un
esempio: Notare
l'uso
della
forma
molto
più
generale
che
fa
uso
di
const. La
sintassi
dei
riferimenti è
più
accurata
di
quella
dei
puntatori,
ma
nasconde
il
significato
al
lettore.
Per
esempio,
nella
libreria
iostreams
una
versione
overloaded
della
funzione
get( )
prende
come
argomento
un
char&,
e
lo
scopo
della
funzione è
proprio
quello
di
modificare
l'argomento
per
inserire
il
risultato
della
get( ).
Ma
quando
si
legge
del
codice
che
usa
questa
funzione,
non è
immediatamente
ovvio
che
l'oggetto
esterno
viene
modificato: In
effetti
la
chiamata
alla
funzione
fa
pensare
ad
un
passaggio
per
valore
e
quindi
suggerisce
che
l'oggetto
esterno
non
viene
modificato. Per
questo
motivo,
probabilmente è
più
sicuro
da
un
punto
di
vista
della
manutenzione
del
codice
usare
i
puntatori
quando
si
deve
passare
l'indirizzo
di
un
argomento
che
deve
essere
modificato.
Se
un
indirizzo
lo
si
passa
sempre
come
riferimento
const
eccetto
quando
si
intende
modificare
l'oggetto
esterno
attraverso
l'indirizzo,
caso
in
cui
si
usa
un
puntatore
non-const,
allora
il
codice
diventa
molto
più
facile
da
seguire
per
un
lettore. Un
puntatore è
una
variabile
che
memorizza
l'indirizzo
di
qualche
locazione
di
memoria.
Si
può
cambiare
a
runtime
quello
che
il
puntatore
seleziona
e
la
destinazione
di
un
puntatore
può
essere
sia
un
dato
sia
una
funzione.
Il puntatore-a-membro del C++ segue
lo
stesso
principio,
solo
che
seleziona
una
locazione
all'interno
di
una
classe.
Il
dilemma
qui è
che
il
puntatore
ha
bisogno
di
un
indirizzo,
ma
non
ci
sono
"indirizzi"
dentro
una
classe;
selezionare
un
membro
di
una
classe
significa
prenderne
l'offset
all'interno
della
classe.
Non
si
può
produrre
un
indirizzo
prima
di
comporre
tale
offset
con
l'indirizzo
di
inizio
di
un
particolare
oggetto.
La
sintassi
dei
puntatori
a
membri
richiede
di
selezionare
un
oggetto
nel
momento
stesso
in
cui
viene
dereferenziato
un
puntatore
a
membro. Per
capire
questa
sintassi,
consideriamo
una
semplice
struttura,
con
un
puntatore
sp
e
un
oggetto
so
per
questa
struttura.
Si
possono
selezionare
membri
con
la
sintassi
mostrata: Adesso
supponiamo
di
avere
un
puntatore
ordinario
a
un
intero,
ip.
Per
accedere
al
valore
puntato
da
ip
si
dereferenzia
il
puntatore
con
‘*': Infine
consideriamo
cosa
succede
se
abbiamo
un
puntatore
che
punta
a
qualcosa
all'interno
di
un
oggetto
di
una
classe,
anche
se
di
fatto
esso
rappresenta
un
offset
all'interno
dell'oggetto.
Per
accedere
a
ciò
a
cui
punta
bisogna
dereferenziarlo
con
*.
Ma
si
tratta
di
un
offset
all'interno
dell'oggetto,
e
quindi
bisogna
riferirsi
anche
a
quel
particolare
oggetto.
Così
l'
*
viene
combinato
con
la
dereferenziazione
dell'oggetto.
Così
la
nuova
sintassi
diventa
–>*
per
un
puntatore
a
un
oggetto
e
.*
per
un
oggetto
o
un
riferimento,
come
questo: Ora
qual'è
la
sintassi
per
definire
pointerToMember?
Come
qualsiasi
puntatore,
bisogna
dire
a
quale
tipo
punta
e
usare
un
*
nella
definizione.
L'unica
differenza è
che
bisogna
dire
con
quale
classe
di
oggetti
questo
puntatore-a-membro
deve
essere
usato.
Naturalmente,
questo
si
ottiene
con
il
nome
della
classe
e
l'operatore
di
risoluzione
di
scope.
definisce
una
variabile
puntatore-a-membro
chiamata
pointerToMember
che
punta
a
un
int
all'interno
di
ObjectClass.
Si
può
anche
inizializzare
un
puntatore-a-membro
quando
lo
si
definisce
(o
in
qualunque
altro
momento): Non
c'è
un
"indirizzo"
di
ObjectClass::a
perchè
ci
stiamo
riferendo
ad
una
classe
e
non
ad
un
oggetto
della
classe.
Così,
&ObjectClass::a
può
essere
usato
solo
come
sintassi
di
un
puntatore-a-membro. Qui
c'è
un
esempio
che
mostra
come
creare
e
usare
i
puntatori
a
dati
membri: Ovviamente
non è
il
caso
di
usare
i
puntatori-a-membri
dappertutto,
se
non
in
casi
particolari
(che è
il
motivo
esatto
per
cui
sono
stati
introdotti). Inoltre
essi
sono
molto
limitati:
possono
essere
assegnati
solo
a
locazioni
specifiche
all'interno
di
una
classe.
Non
si
possono,
ad
esempio,
incrementare
o
confrontare
come
i
normali
puntatori. In
modo
simile
si
ottiene
la
sintassi
di
un
puntatore-a-membro
per
le
funzioni
membro.
Un
puntatore
a
funzione
(introdotto
nel
Capitolo
3) è
definito
come
questo: Le
parentesi
intorno
a
(*fp)
sono
necessarie
affinchè
il
compilatore
valuti
in
modo
appropriato
la
definizione.
Senza
di
esse
potrebbe
sembrare
la
definizione
di
una
funzione
che
restituisce
un
int*.
Le
parentesi
giocano
un
ruolo
importante
anche
nella
definizione
e
nell'uso
dei
punatori
a
funzioni
membro.
Se
abbiamo
una
funzione
in
una
classe,
possiamo
definire
un
puntatore
ad
essa
usando
il
nome
della
classe
e
l'operatore
di
risoluzione
di
scope
all'interno
di
una
definizione
ordinaria
di
puntatore
a
funzione: Nella
definizione
di
fp2
si
può
notare
che
un
puntatore
ad
una
funzione
membro
può
anche
essere
inizializzato
quando
viene
creato,
o
in
qualunque
altro
momento.
A
differenza
delle
funzioni
non-membro,
l'operatore
& non è opzionale quando si prende l'indirizzo
di
una
funzione
membro.
Tuttavia,
si
può
fornire
l'identificatore
della
funzione
senza
la
lista
degli
argomenti,
perchè
la
risoluzione
dell'overload
può
essere
effettuata
sulla
base
del
tipo
del
puntatore
a
membro.
L'importanza
di
un
puntatore
sta
nel
fatto
che
si
può
cambiare
runtime
l'oggetto
a
cui
punta,
il
che
conferisce
molta
flessibiltà
alla
programmazione,
perchè
attraverso
un
puntatore
si
può
selezionare
o
cambiare
comportamento
a
runtime.
Un
puntatore-a-membro
non
fa
eccezione;
esso
permette
di
scegliere
un
membro
a
runtime.
Tipicamente,
le
classi
hanno
solo
funzioni
membro
pubblicamente
visibili (I dati membro sono generalmente considerati
parte
dell'implementazione
sottostante),
così
l'esempio
seguente
seleziona
funzioni
membro
a
runtime. Naturalmente
non è
particolarmente
ragionevole
aspettarsi
che
un
utente
qualsiasi
possa
creare
espressioni
così
complicate.
Se
un
utente
deve
manipolare
direttamente
un
puntatore-a-membro,
allora
un
typedef è
quello
che
ci
vuole.
Per
rendere
le
cose
veramente
pulite
si
possono
usare
i
puntatori-a-membri
come
parte
del
meccanismo
di
implementazione
interna.
Qui
c'è
l'esempio
precedente
che
usa
un
puntatore-a-membro
all'interno
della
classe.
Tutto
quello
che
l'utente
deve
fare è
passare
un
numero
per
selezionare
una
funzione.[48]
Nell'interfaccia
della
classe
e
nel
main( ),
si
può
vedere
che
l'intera
implementazione,
comprese
le
funzioni, è
stata
nascosta.
Il
codice
deve
sempre
invocare
count( )
per
selezionare
una
funzione.
In
questo
modo
l'implementatore
della
classe
può
cambiare
il
numero
di
funzioni
nell'implementazione
sottostante,
senza
influire
sul
codice
in
cui
la
classe
viene
usata. L'inizializzazione
dei
puntatori-a-membri
nel
costruttore
può
sembrare
sovraspecificato.
Non
si
potrebbe
semplicemente
scrivere visto
che
il
nome
g
appare
nella
funzione
membro,
che è
automaticamente
nello
scope
della
classe?
Il
problema è
che
questo
non è
conforme
alla
sintassi
del
puntatore-a-membro,
che
invece è
richiesta,
così
tutti,
specialmente
il
compilatore,
possono
sapere
cosa
si
sta
facendo.
Allo
stesso
modo,
quando
un
puntatore-a-membro
viene
dereferenziato,
assomiglia
a che
pure è
sovraspecificato;
this
sembra
ridondante.
Di
nuovo,
la
sintassi
richiede
che
un
puntatore-a-membro
sia
sempre
legato
a
un
oggetto
quando
viene
dereferenziato. I
puntatori
in
C++
sono
quasi
identici
ai
puntatori
in
C,
il
che è
buono.
Altrimenti
un
sacco
di
codice
scritto
in
C
non
si
compilerebbe
in
C++.
Gli
unici
errori
a
compile-time
che
vengono
fuori
sono
legati
ad
assegnamenti
pericolosi.
Se
proprio
si
vuole
cambiare
il
tipo,
bisogna
usare
esplicitamente
il
cast. Il
C++
aggiunge
anche
i
riferimenti,
presi
dall'Algol
e
dal
Pascal,
che
sono
come
dei
puntatori
costanti
automaticamente
dereferenziati
dal
compilatore.
Un
riferimento
contiene
un
indirizzo,
ma
lo
si
può
trattare
come
un
oggetto.
I
riferimenti
sono
essenziali
per
una
sintassi
chiara
con
l'overload
degli
operatori
(l'argomento
del
prossimo
capitolo),
ma
essi
aggiungono
anche
vantaggi
sintattici
nel
passare
e
ritornare
oggetti
per
funzioni
ordinarie. Il
costruttore
di
copia
prende
un
riferimento
ad
un
oggetto
esistente
dello
stesso
tipo
come
argomento
e
viene
usato
per
creare
un
nuovo
oggetto
da
uno
esistente.
Il
compilatore
automaticamente
chiama
il
costruttore
di
copia
quando
viene
passato
o
ritornato
un
oggetto
per
valore.
Anche
se
il
compilatore è
in
grado
di
creare
un
costruttore
di
copia
di
default,
se
si
pensa
che
ne
serve
uno
per
la
nostra
classe è
meglio
definirlo
sempre
in
proprio
per
assicurare
il
corretto
funzionamento.
Se
non
si
vuole
che
gli
oggetti
siano
passati
o
ritornati
per
valore,
si
può
creare
un
costruttore
di
copia
private. I
puntatori-a-membri
hanno
le
stesse
funzionalità
dei
puntatori
ordinari:
si
può
scegliere
una
particolare
zona
di
memoria
(dati
o
funzioni)
a
runtime.
I
puntatori-a-membri
semplicemente
funzionano
con
i
membri
di
classi
invece
che
con
dati
globali
o
funzioni.
Si
ottiene
la
flessibilità
di
programmazione
che
permette
di
cambiare
il
comportamento
a
runtime. Le soluzioni
agli
esercizi
selezionati
possono
essere
trovate
nel
documento
elettronico
The
Thinking
in
C++
Annotated
Solution
Guide,
disponibile
dietro
un
piccolo
compenso
su
www.BruceEckel.com.
[48] Grazie a Mortensen per questo esempio
//: C11:HowMany2.cpp
// Il costruttore di copia
#include <fstream>
#include <string>
using namespace std;
ofstream out("HowMany2.out");
class HowMany2 {
string name; // Identificatore dell'oggetto
static int objectCount;
public:
HowMany2(const string& id = "") : name(id) {
++objectCount;
print("HowMany2()");
}
~HowMany2() {
--objectCount;
print("~HowMany2()");
}
// Il costruttore di copia:
HowMany2(const HowMany2& h) : name(h.name) {
name += " copy";
++objectCount;
print("HowMany2(const HowMany2&)");
}
void print(const string& msg = "") const {
if(msg.size() != 0)
out << msg << endl;
out << '\t' << name << ": "
<< "objectCount = "
<< objectCount << endl;
}
};
int HowMany2::objectCount = 0;
// Passaggio e ritorno PER VALORE:
HowMany2 f(HowMany2 x) {
x.print("x argument inside f()");
out << "Returning from f()" << endl;
return x;
}
int main() {
HowMany2 h("h");
out << "Entering f()" << endl;
HowMany2 h2 = f(h);
h2.print("h2 after call to f()");
out << "Call f(), no return value" << endl;
f(h);
out << "After call to f()" << endl;
} ///:~
//: C11:Linenum.cpp
//{T} Linenum.cpp
// Aggiunge i numeri di linea
#include "../require.h"
#include <vector>
#include <string>
#include <fstream>
#include <iostream>
#include <cmath>
using namespace std;
int main(int argc, char* argv[]) {
requireArgs(argc, 1, "Usage: linenum file\n"
"Adds line numbers to file");
ifstream in(argv[1]);
assure(in, argv[1]);
string line;
vector<string> lines;
while(getline(in, line)) // Legge un intero file nella stringa line
lines.push_back(line);
if(lines.size() == 0) return 0;
int num = 0;
// Il numero di linee nel file determina la larghezza:
const int width =
int(log10((double)lines.size())) + 1;
for(int i = 0; i < lines.size(); i++) {
cout.setf(ios::right, ios::adjustfield);
cout.width(width);
cout << ++num << ") " << lines[i] << endl;
}
} ///:~
1) HowMany2()
2) h: objectCount = 1
3) Entering f()
4) HowMany2(const HowMany2&)
5) h copy: objectCount = 2
6) x argument inside f()
7) h copy: objectCount = 2
8) Returning from f()
9) HowMany2(const HowMany2&)
10) h copy copy: objectCount = 3
11) ~HowMany2()
12) h copy: objectCount = 2
13) h2 after call to f()
14) h copy copy: objectCount = 2
15) Call f(), no return value
16) HowMany2(const HowMany2&)
17) h copy: objectCount = 3
18) x argument inside f()
19) h copy: objectCount = 3
20) Returning from f()
21) HowMany2(const HowMany2&)
22) h copy copy: objectCount = 4
23) ~HowMany2()
24) h copy: objectCount = 3
25) ~HowMany2()
26) h copy copy: objectCount = 2
27) After call to f()
28) ~HowMany2()
29) h copy copy: objectCount = 1
30) ~HowMany2()
31) h: objectCount = 0
Oggetti temporanei
Costruttore di copia di default
//: C11:DefaultCopyConstructor.cpp
// Creazione automatica del costruttore di copia
#include <iostream>
#include <string>
using namespace std;
class WithCC { // Con costruttore di copia
public:
// Richiesto il costruttore di default esplicito:
WithCC() {}
WithCC(const WithCC&) {
cout << "WithCC(WithCC&)" << endl;
}
};
class WoCC { // Senza costruttore di copia
string id;
public:
WoCC(const string& ident = "") : id(ident) {}
void print(const string& msg = "") const {
if(msg.size() != 0) cout << msg << ": ";
cout << id << endl;
}
};
class Composite {
WithCC withcc; // Oggetti incorporati
WoCC wocc;
public:
Composite() : wocc("Composite()") {}
void print(const string& msg = "") const {
wocc.print(msg);
}
};
int main() {
Composite c;
c.print("Contents of c");
cout << "Calling Composite copy-constructor"
<< endl;
Composite c2 = c; // Chiama il costruttore di copia
c2.print("Contents of c2");
} ///:~
Composite c2 = c;
Contents of c: Composite()
Calling Composite copy-constructor
WithCC(WithCC&)
Contents of c2: Composite()
Alternative alla costruzione della copia
Prevenire
il
passaggio-per-valore
//: C11:NoCopyConstruction.cpp
// Prevenire la costruzione della copia
class NoCC {
int i;
NoCC(const NoCC&); // Nessuna definizione
public:
NoCC(int ii = 0) : i(ii) {}
};
void f(NoCC);
int main() {
NoCC n;
//! f(n); // Errore: chiamato il costruttore di copia
//! NoCC n2 = n; // Errore: chiamato c-c
//! NoCC n3(n); // Errore: chiamato c-c
} ///:~
NoCC(const NoCC&);
Funzioni che modificano oggetti esterni
char c;
cin.get(c);
Puntatori a membri
//: C11:SimpleStructure.cpp
struct Simple { int a; };
int main() {
Simple so, *sp = &so;
sp->a;
so.a;
} ///:~
*ip = 4;
objectPointer->*pointerToMember = 47;
object.*pointerToMember = 47;
int ObjectClass::*pointerToMember;
int ObjectClass::*pointerToMember = &ObjectClass::a;
//: C11:PointerToMemberData.cpp
#include <iostream>
using namespace std;
class Data {
public:
int a, b, c;
void print() const {
cout << "a = " << a << ", b = " << b
<< ", c = " << c << endl;
}
};
int main() {
Data d, *dp = &d;
int Data::*pmInt = &Data::a;
dp->*pmInt = 47;
pmInt = &Data::b;
d.*pmInt = 48;
pmInt = &Data::c;
dp->*pmInt = 49;
dp->print();
} ///:~
Funzioni
int (*fp)(float);
//: C11:PmemFunDefinition.cpp
class Simple2 {
public:
int f(float) const { return 1; }
};
int (Simple2::*fp)(float) const;
int (Simple2::*fp2)(float) const = &Simple2::f;
int main() {
fp = &Simple2::f;
} ///:~
Un esempio
//: C11:PointerToMemberFunction.cpp
#include <iostream>
using namespace std;
class Widget {
public:
void f(int) const { cout << "Widget::f()\n"; }
void g(int) const { cout << "Widget::g()\n"; }
void h(int) const { cout << "Widget::h()\n"; }
void i(int) const { cout << "Widget::i()\n"; }
};
int main() {
Widget w;
Widget* wp = &w;
void (Widget::*pmem)(int) const = &Widget::h;
(w.*pmem)(1);
(wp->*pmem)(2);
} ///:~
//: C11:PointerToMemberFunction2.cpp
#include <iostream>
using namespace std;
class Widget {
void f(int) const { cout << "Widget::f()\n"; }
void g(int) const { cout << "Widget::g()\n"; }
void h(int) const { cout << "Widget::h()\n"; }
void i(int) const { cout << "Widget::i()\n"; }
enum { cnt = 4 };
void (Widget::*fptr[cnt])(int) const;
public:
Widget() {
fptr[0] = &Widget::f; // Richiesta specificazione completa
fptr[1] = &Widget::g;
fptr[2] = &Widget::h;
fptr[3] = &Widget::i;
}
void select(int i, int j) {
if(i < 0 || i >= cnt) return;
(this->*fptr[i])(j);
}
int count() { return cnt; }
};
int main() {
Widget w;
for(int i = 0; i < w.count(); i++)
w.select(i, 47);
} ///:~
fptr[1] = &g;
(this->*fptr[i])(j);
Sommario
Esercizi
Ultima
modifica:24/12/2002