*
c'è il simbolo &
, che abbiamo già incontrato a proposito degli operatori logici (AND = &&
), di confronto bit a bit (AND bit a bit = &
) e come operatore ``indirizzo di'' (&
). Così come abbiamo fatto per i puntatori, anche in tal caso si invita a non fare confusione tra i diversi significati che il simbolo &
possiede in C++ ( purtroppo i simboli disponibili su di una tastiera sono limitati, non ci si può far nulla ...). Esempi di riferimento sono pertanto:
int& a |
int &a |
riferimento di una variabile intera | |
double& a |
double &a |
riferimento di una variabile reale |
&
.
Un riferimento, come dice la parola stessa, è una variabile che ``si riferisce'' ad un'altra, ne costituisce pertanto un alias, un sinonimo; ecco la seconda importante somiglianza con i puntatori, i quali anch'essi possono essere alias di oggetti, in particolare di variabili, se preceduti dall'operatore *
. Nel caso dei riferimenti il legame tra essi e le variabili riferite è più stretto di quello esistente tra puntatori e variabili puntate: un puntatore contiene l'indirizzo di una variabile e, a meno che esso non sia stato dichiarato costante, è possibile modificare tale indirizzo, inserendoci quello di variabili sempre diverse. Al contrario, ogni riferimento va inizializzato, ossia gli va assegnato un valore iniziale, e il suo valore non può cambiare: la variabile riferita rimarrà sempre la medesima, per tutta la vita del riferimento (la quale si computa con gli stessi criteri delle altre variabili e dei puntatori). Vediamo un esempio:
//ex5_5_1.cpp #include <iostream.h> void main() { int a = 6, b = 3; // int& r; // NO: errore // `r' declared as reference but not initialized // `r' dichiarato come riferimento ma non inizializzato int& r = a; cout << "a = " << a << "\tr = " << r << "\tb = " << b << "\n"; r = b; // in realta' e' come scrivere a = b cout << "a = " << a << "\tr = " << r << "\tb = " << b << "\n"; }
output:
a = 6 | r = 6 | b = 3 | |
a = 3 | r = 3 | b = 3 |
Prima osservazione: il compilatore ci torna un messaggio di errore se cerchiamo di dichiarare un riferimento, mentre è possibile dichiarare un puntatore, come vediamo nel seguente esempio, che si invita a confrontare con il precedente:
//ex5_5_2.cpp #include <iostream.h> void main() { int a = 6, b = 3; int* r; // OK r = &a; cout << "a = " << a << "\tr = " << *r << "\tb = " << b << "\n"; r = &b; cout << "a = " << a << "\tr = " << *r << "\tb = " << b << "\n"; }
output:
a = 6 | r = 6 | b = 3 | |
a = 6 | r = 3 | b = 3 |
Come si vede adesso il valore della variabile a
non viene modificato, come invece avveniva nell'esempio ex5_5_1.cpp
. I riferimenti hanno le caratteristiche dei puntatori costanti: devono essere inizializzati e non possono cambiare la variabile di cui sono alias; essi però a questo scopo posseggono una maggiore chiarezza espressiva, in quanto non necessitano dell'operatore ``indirizzo di'' (&
) o dell'operatore *
. Si veda il seguente:
// ex5_5_3.cpp void main() { int a = 3, b = 7; int* const p_a = &a; int& r_a = a; // non occorre l'operatore & // p_a = &b; // NO: errore // r_a = b; // ok ma r_a resta alias di a // equivale a 'a = b' b += *p_a; // b = 10 b += r_a; // b = 13 // non occorre l'operatore * }
Anche nel caso si utilizzino i riferimenti come argomenti di funzioni, il vantaggio dal punto di vista estetico è notevole; diminuisce di pari passo dunque anche il rischio di dimenticare l'operatore &
o *
. Come esempio riassuntivo sulle modalità di passaggio degli argomenti alle funzioni, vediamo il seguente:
// ex5_5_4.cpp #include <iostream.h> // passaggio per valore // sbagliata: funziona solo se a == b void scambia1 (int a, int b) { int t = a; a = b; b = t; } // passaggio per indirizzo void scambia2 (int* a, int* b) { int t = *a; *a = *b; *b = t; } // passaggio per riferimento void scambia3 (int& a, int& b) { int t = a; a = b; b = t; } void main() { int a = 3, b = 7; cout << "a = " << a << "\tb = " << b << "\n"; scambia1 (a, b); // non scambia cout << "a = " << a << "\tb = " << b << "\n"; scambia2 (&a, &b); cout << "a = " << a << "\tb = " << b << "\n"; scambia3 (a, b); cout << "a = " << a << "\tb = " << b << "\n"; }
output:
a = 3 | b = 7 | |
a = 3 | b = 7 | |
a = 7 | b = 3 | |
a = 3 | b = 7 |
Vediamo una per una le diverse modalità di passaggio degli argomenti. La
prima funzione (scambia1
) utilizza il passaggio per valore; è
importante capire perché non vengono in realtà scambiate le due
variabili a
e b
nella funzione main
: quando viene
effettuata la chiamata alla funzione scambia1
, i valori di a
e
b
della main
servono ad inizializzare le variabili
a
e b
della scambia1
, le quali sono ben distinte dalle
loro omonime. Successivamente si scambiano tra di loro queste due variabili;
in realtà a
e b
della main
non sono state mai
toccate, se non in fase di lettura al momento della chiamata. Al contrario,
nella seconda funzione (scambia2
), vengono passati gli indirizzi di
a
e b
della main
, per cui lo scambio avviente
effettivamente fra di esse. L'unico neo consiste nel fatto che bisogna stare
attenti, se si utilizza il passaggio per indirizzo, a due cose: passare
alla funzione gli indirizzi delle variabili nelle funzione chiamante, e
utilizzare l'operatore *
per i puntatori nella funzione chiamata. La
soluzione più pulita consiste infine nel passaggio per riferimento (funzione
scambia3
), che è praticamente identica alla scambia1
però funziona, perché le variabili a
e b
sono riferimenti, e dunque alias di a
e b
della funzione main
.
I puntatori in C++ sono un bagaglio acquisito dalla compatibilità con il C,
nel quale essi erano davvero indispensabili: il programmatore era costantemente posto di fronte ad operazioni a basso livello sulla memoria, con enormi ed evidenti difficoltà nello scrivere dei programmi stabili e affidabili. Essendo invece il C++ un linguaggio di programmazione ad oggetti molto astratto, i puntatori si sarebbero tranquillamente potuti trascurare, utilizzando esclusivamente i riferimenti che, come si vede già da primi semplici esempi, sono molto più comodi da gestire; un esempio di linguaggio ad alto livello che non possiede i puntatori è l'ormai celebre Java. Comunque, in C++ i puntatori ci sono, e dunque vanno usati; si cerchi comunque di non confonderli con i riferimenti, o viceversa, ricordando che questi ultimi costituiscono una maggiore astrazione rispetto ai puntatori. Si noti infatti che il riferimento ad un oggetto, è in realtà l'oggetto stesso; è quindi naturale che i riferimenti siano da preferire ai puntatori come argomenti di funzioni: perché portare dentro una funzione un misterioso indirizzo (che va ``catturato'' tramite l'operatore &
, che spesso si dimentica ...) quando si può avere l'oggetto bello e pronto. Nel caso non si intenda modificare tale oggetto nella funzione chiamata, lo si dichiara poi costante ed il gioco è fatto. Vediamo il seguente:
// ex5_5_5.cpp // calcola il perimetro di un quadrato #include <iostream.h> double perimetro (const double& lato) { return lato * 4; } void rendiPositivo (double& lato) { if (lato < 0) lato = -lato; } void main() { double lato; cout << "lato quadrato? "; cin >> lato; rendiPositivo (lato); cout << "perimetro = " << perimetro (lato); }
esempio di output:
lato quadrato? -5.5
perimetro = 22
La funzione rendiPositivo
modifica la variabile lato
, nel caso essa sia negativa, per cui il riferimento passatole non può essere costante; invece la funzione perimetro
sicuramente non modifica tale variabile, la quale può quindi tranquillamente essere passata per riferimento costante; in questa maniera la funzione è più potente rispetto alla stessa funzione con il riferimento non costante. Perché? Si veda il seguente esempio, che mette a confronto due funzioni identiche, che calcolano l'area di un quadrato:
// ex5_5_6.cpp double area_const (const double& lato) { return lato * lato; } double area (double& lato) { return lato * lato; } void main() { double lato = 10; double A = area_const (lato); // A = 100 double B = area_const (10); // B = 100 OK double C = area (lato); // C = 100 // double D = area (10); // NO }
Il compilatore può considerare la costante letterale 10
come variabile costante riferita da lato
nella area_const
, ma non può fare lo stesso in area
perché una costante (solo lettura) non può essere alias di un riferimento non costante (lettura e scrittura), per cui il compilatore torna un errore. Come abbiamo già fatto notare per i puntatori, questo problema non sussiste nel passaggio per valore degli argomenti delle funzioni, che è dunque da preferire ove possibile (ad esempio nella funzione rendiPositivo
dell'esempio ex5_5_5.cpp
non è possibile). Ultima osservazione: anche i riferimenti, come i puntatori, possono essere utilizzati come tipo di ritorno di una funzione; quando ci addentreremo maggiormente nella programmazione a oggetti, vedremo esempi significativi; per ora ci si accontenti del seguente:
// ex5_5_7.cpp #include <iostream.h> #include <stdlib.h> #include <time.h> // tale funzione sceglie "a caso" una variabile, // tra le due date, da ritornare int& scegliVariabile (int& a, int& b) { if (rand() % 2 == 0) return a; else // l'else non e' necessario: perche'? return b; } void main() { srand ( time(0) ); int a, b; for (int i = 0; i < 5; i++) { a = 0; b = 0; scegliVariabile(a,b) = 1; cout << "a = " << a << "\tb = " << b << "\n"; } }
esempio di output:
a = 1 | b = 0 | |
a = 0 | b = 1 | |
a = 0 | b = 1 | |
a = 1 | b = 0 | |
a = 0 | b = 1 |