Il linguaggio C++ mette a disposizione dello sviluppatore un metodo di indirizzamento della memoria alternativo all'uso dei puntatori: le variabili reference.
La loro particolarità consiste nella modalità di accesso, che maschera l'operazione di dereferenziazione necessaria, come abbiamo visto, per leggere o scrivere un valore nella locazione di memoria contenuta in un normale puntatore.
A parte la loro definizione, infatti, per le variabili reference viggono le medesime regole sintattiche valide per l'accesso ad una variabile ordinaria.
La differenza consiste nel fatto che una variabile reference non ha una locazione di memoria propria, ma punta a quella di una variabile ordinaria già esistente. Per questo motivo, l'inizializzazione deve avvenire contestualmente alla dichiarazione come mostrato nell'esempio seguente:
int a = 1;
int& ref_a = a;
int b = 2;
ref_a = b; // Errore: non possiamo cambiare il riferimento dopo la dichiarazione!
int& r; // Errore: una variabile reference deve essere inizializzata!
Dal momento della sua definizione, ogni modifica del valore di ref_a si riflette automaticamente su a e non sarà più possibile cambiare il riferimento, puntando ad esempio ad un'altra variabile. In pratica, definire una variabile reference corrisponde al fornire un nome alternativo per una variabile già esistente. Per questo motivo esse sono dette anche variabili alias.
L'uso di variabili reference fornisce alcuni vantaggi rispetto all'uso dei puntatori. Prima di tutto, la necessità di inizializzarle previene la possibilità di commettere delle violazioni nell'accesso alla memoria, come invece accade quando si dereferenzia un puntatore nullo o non inizializzato.
La mancata inizializzazione di una variabile reference ci viene infatti notificata subito tramite un errore di compilazione. Un puntatore non inizializzato, invece, rappresenta un possibile problema a tempo di esecuzione, con la terminazione immediata del programma da parte del sistema operativo in caso di una violazione d'accesso.
In secondo luogo, l'uso di variabili reference è sintatticamente più agevole rispetto all'uso di puntatori.
Tuttavia, a differenza dei puntatori, le variabili reference sono solo raramente necessarie. Il loro impiego può infatti essere sostituito del tutto dall'uso dei puntatori, con le dovute differenze sintattiche.
Nello standard C++ non è prescritta una particolare modalità di implementazione delle variabili reference da parte dei compilatori e, di fatto, molti di essi le trattano implicitamente come puntatori veri e propri, in maniera trasparente per il programmatore.
La scelta e le modalità del loro impiego è più che altro una questione di stile e di leggibilità del codice.
Talvolta è comodo non dover ricorrere alla sintassi inevitabilmente più complessa dei puntatori, e altre volte è bene rendere esplicite le operazioni di dereferenziazione.
Vedremo meglio nelle lezioni seguenti, ad esempio, che nel caso della definizione di funzioni con parametri, esistono due modalità per il passaggio dei valori, dette "per copia" e "per riferimento". Quando si usano puntatori per la definizione dei parametri, le due modalità sono ben distinguibili sintatticamente anche nel contesto del chiamante, ma lo stesso non può dirsi delle variabili reference.
Tuttavia, le implicazioni derivanti dall'uso di una modalità o l'altra sono profondamente differenti, e pertanto, in questo caso, è sempre preferibile usare i puntatori, perchè ciò rende più chiaro il nostro codice.
In altri casi, invece, le variabili reference migliorano la leggibilità del codice senza effetti collaterali, come nell'esempio seguente:
// da qualche parte nel codice dell'applicazione a cui sto lavorando
int nomeMoltoMoltoLungoCheHaDecisoQualcunAltroDelMioTeam = 1;
...
// da qualche parte nel codice di mia competenza
int& x = nomeMoltoMoltoLungoCheHaDecisoQualcunAltroDelMioTeam;
// qualunque operazione che coinvolga 'x' si rifletterà automaticamente sulla variabile a cui punta.
x = ... ;
Il tipo di una variabile reference
Abbiamo visto in precedenza che esiste una differenza di tipo tra una variabile puntatore e una variabile ordinaria: tipo*
e tipo
sono di fatto trattati come tipi differenti dal compilatore.
Cosa possiamo dire delle variabili reference? La forma tipo&
introduce forse un nuovo tassello nella definizione dei tipi di C++?
La risposta è: no, il tipo di una variabile reference non è diverso da quello della variabile ordinaria a cui è associata. Nel primo esempio di questa lezione, quindi, ref_a
e a
sono entrambe variabili di tipo intero.
Variabili reference nulle
Il meccanismo di inizializzazione contestuale alla dichiarazione è un metodo abbastanza efficace per garantire che una variabile reference sia sempre associata ad una locazione di memoria valida.
Tuttavia quando usiamo le variabili reference come alias di una variabile puntatore, è abbastanza semplice trasgredire questa politica.
Il frammento seguente mostra l'inizializzazione di due variabili reference associate a dei puntatori.
// inizializzazione di una variabile reference a partire da un puntatore valido
int *p = new int;
*p = 2;
int& ref_p = *p;
// inizializzazione di una variabile reference con un puntatore nullo
int *np = nullptr;
int& ref_np = *np;
std::cout << ref_np << std::endl; // errore di segmentazione!
Nel primo caso, il puntatore è correttamente inizializzato e ciò ci garantisce di poter usare la variabile ref_p
in tutta sicurezza fin tanto che il puntatore p
rimane valido.
Nel secondo caso, invece, il puntatore è nullo, e ciò produce una violazione nell'accesso alla memoria quando tentiamo di leggere il valore contenuto nella sua variabile alias.
È chiaro che in questo caso abbiamo volutamente forzato la mano per fini didattici. Ciò serve per rimarcare che, anche se implicitamente, anche le variabili reference hanno l'onere della dereferenziaziare. Pertanto, il loro uso presenta le medesime criticità di quello dei puntatori.