Un altro nome per la stessa cosa
Nella pagina sui parametri di funzione, ogni argomento veniva copiato nella funzione. Quella copia è il motivo per cui una funzione non può cambiare la variabile del chiamante: vede sempre e solo il proprio duplicato. Un riferimento abbatte quel muro. È un alias: un secondo nome legato a una variabile esistente, che ne condivide esattamente la stessa memoria.
Crei un riferimento con & nella dichiarazione. Una volta legati, il riferimento e l'originale sono indistinguibili:
Due regole rendono i riferimenti sicuri e prevedibili: un riferimento deve essere inizializzato nel momento in cui viene dichiarato (int& r; è un errore di compilazione), e non può mai essere riassegnato per puntare a qualcos'altro in seguito. Assegnare a un riferimento scrive sempre in ciò a cui era stato legato all'inizio.
Passaggio per riferimento: lascia che una funzione arrivi al chiamante
Il vero vantaggio si vede nelle funzioni. Metti & su un parametro e la funzione riceve un alias dell'argomento del chiamante invece di una copia. Ora le modifiche dentro la funzione sono visibili anche fuori:
Togli la & e addBonus incrementerebbe una copia usa e getta, lasciando total a 100. Quel singolo carattere fa tutta la differenza. È il modo canonico di scrivere una funzione che restituisce più di un risultato o modifica il suo input sul posto. L'esempio classico è lo scambio di due variabili:
Senza riferimenti, swapValues scambierebbe solo copie locali e x/y resterebbero 1 2. (La libreria standard ha già std::swap, ma scriverla da te mostra esattamente cosa ti dà un parametro per riferimento.)
Riferimenti const: leggi in fretta, prometti di non toccare
Il passaggio per riferimento evita anche la copia, e per un oggetto grande quella copia può essere costosa. Ma un semplice parametro T& comunica "potrei modificarlo", il che è fuorviante quando vuoi solo leggere. La soluzione è const T&: ottieni la velocità senza copie di un riferimento e una promessa, garantita dal compilatore, che la funzione non cambierà l'argomento.
Un riferimento non const può legarsi solo a una variabile modificabile, ma un riferimento const può legarsi anche a letterali e temporanei: ecco perché greet("literal works too") compila. Una regola pratica per scegliere il tipo di un parametro:
void f(int x) // tipo economico, sola lettura -> copialo e basta
void f(const string& s) // tipo pesante, sola lettura -> riferimento const
void f(string& s) // vuoi modificare l'oggetto del chiamante
Usa di default const T& per qualsiasi tipo classe che leggi soltanto (string, vector, le tue struct), e riserva un riferimento non const a quando intendi davvero scriverci.
Restituire un riferimento
Una funzione può anche restituire un riferimento, consegnando al chiamante un alias di qualcosa che esiste già. È comune nel codice simile ai container: è ciò che fa funzionare v[i] = 5 ed è ciò che l'overloading degli operatori fa dietro le quinte:
Poiché at restituisce int&, l'espressione di chiamata at(data, 1) è essa stessa un lvalue a cui puoi assegnare. Restituisci invece un semplice int e at(data, 1) = 42 non compilerebbe: staresti assegnando a una copia temporanea.
La grande trappola: i riferimenti pendenti
Un riferimento non possiede nulla; punta soltanto a memoria che vive altrove. Se quella memoria muore mentre il riferimento è ancora in uso, hai un riferimento pendente (dangling reference), e leggere tramite esso è undefined behavior: potrebbe stampare spazzatura, andare in crash o sembrare funzionare finché non ti rovina la giornata in produzione. L'errore classico è restituire un riferimento a una variabile locale:
int& broken() {
int local = 42;
return local; // BUG: local viene distrutta quando broken() termina
} // il riferimento restituito è pendente
int main() {
int& r = broken();
cout << r << "\n"; // UNDEFINED BEHAVIOR: legge memoria morta
}
La variabile local sparisce nell'istante in cui broken termina, quindi il riferimento punta a spazio dello stack già recuperato. Restituisci un riferimento solo a qualcosa che sopravvive alla chiamata: un parametro passato per riferimento, un dato membro o una variabile static. Se il valore viene calcolato dentro la funzione, restituiscilo per valore e lascia che il compilatore ottimizzi via la copia. La stessa trappola colpisce i cicli basati su range e qualsiasi riferimento legato a un temporaneo: non tenere mai un riferimento oltre la durata di vita di ciò che nomina.
Prossimo passo: l'overloading delle funzioni
I riferimenti ti danno una seconda manopola su ogni parametro (copia o alias, modificabile o const), e quella manopola interagisce direttamente con il prossimo argomento. Tra poco, l'overloading delle funzioni ti permetterà di definire più funzioni con lo stesso nome ma liste di parametri diverse, e il compilatore sceglierà quella giusta confrontando i tipi degli argomenti, compreso il fatto che siano passati per valore, per riferimento o per riferimento const.
Domande frequenti
Cos'è un riferimento in C++?
Un riferimento è un alias di una variabile esistente, cioè un altro nome per la stessa memoria. Lo crei con & nella dichiarazione: int& r = x;. Da lì in poi r e x sono intercambiabili; cambiare uno cambia l'altro. I riferimenti vanno inizializzati quando vengono dichiarati e non possono mai essere riassegnati a una variabile diversa.
Qual è la differenza tra passaggio per valore e per riferimento in C++?
Il passaggio per valore (void f(int x)) copia l'argomento, quindi la funzione lavora sulla propria copia e la variabile del chiamante resta intatta. Il passaggio per riferimento (void f(int& x)) dà alla funzione accesso diretto alla variabile del chiamante, quindi le modifiche sono visibili dopo la chiamata, e non viene fatta alcuna copia, cosa che conta per gli oggetti grandi.
Quando usare parametri per riferimento const in C++?
Usa const T& quando una funzione deve solo leggere un parametro ma il tipo è costoso da copiare (string, vector, struct grandi). Ottieni la velocità senza copie di un riferimento più la garanzia del compilatore che la funzione non modificherà il valore del chiamante. Per tipi economici come int o double, il semplice passaggio per valore è più semplice e altrettanto veloce.