Due modi per riferirsi a qualcosa
Conosci già i puntatori: variabili che memorizzano un indirizzo e ti permettono di raggiungere l'oggetto che vi si trova. I riferimenti sono l'altro strumento che C++ ti offre per lavorare indirettamente con un oggetto esistente. Si sovrappongono abbastanza da confondere chi inizia sulla scelta, quindi questa pagina li mette a confronto.
In breve: un riferimento è un alias. Una volta eseguito int& r = x;, r è x: stesso oggetto, nome diverso. Un puntatore è un oggetto separato che contiene l'indirizzo di un altro. Questa sola differenza determina tutto il resto.
Un riferimento è un alias
Un riferimento deve essere legato a un oggetto nel momento in cui viene creato, e da lì in poi ogni uso del riferimento tocca l'originale.
Nota che quando lo usi non c'è alcun * per dereferenziare né alcuna & per "prendere l'indirizzo": leggi e scrivi alias esattamente come un semplice int. La & in int& alias fa parte del tipo, non è l'operatore indirizzo.
Dove differiscono
I comportamenti qui sotto sono l'intera ragione per cui esistono entrambi gli strumenti. È la tabella da imparare a memoria.
// reference pointer
// must be initialized? yes no (but should be)
// can be null? no yes (nullptr)
// can be reseated? no yes
// pointer arithmetic? no yes
// syntax to use it just the name *p or p->member
// taking address &ref == &original &p is the pointer's own address
Due di questi punti mettono in difficoltà più degli altri. Primo, un riferimento non può mai essere riassegnato: assegnargli qualcosa copia un valore dentro l'oggetto a cui si riferisce, non lo fa puntare a qualcos'altro.
Un puntatore, invece, è libero di puntare altrove in qualsiasi momento:
Secondo, un riferimento non può mai essere nullo in modo lecito, mentre un puntatore sì. Questo rende "nessun valore" esprimibile con un puntatore ma non con un riferimento, una proprietà su cui farai affidamento di continuo.
Scegliere nei parametri di funzione
È qui che la scelta emerge più spesso. Quando una funzione deve leggere o modificare un oggetto del chiamante, funzionano entrambi, ma comunicano intenzioni diverse.
La versione con riferimento (addTax(cart)) non si può chiamare con "niente", quindi dentro la funzione non controlli mai il valore nullo: l'oggetto è garantito. La versione con puntatore (applyDiscount(&cart)) segnala nel punto della chiamata, tramite la &, che l'argomento potrebbe essere modificato, e permette al chiamante di passare nullptr per dire "non applicabile". Scegli quella la cui garanzia corrisponde alla tua funzione.
Per i parametri in sola lettura di tipi grandi, la scelta idiomatica è const T&: evita una copia e promette di non modificare. Consulta i parametri di funzione per saperne di più sul passaggio per valore e per riferimento.
Una regola pratica semplice
Nel dubbio, usa di default un riferimento e passa a un puntatore solo quando ti serve una capacità che il riferimento non ha:
- Usa un riferimento quando l'oggetto esiste sempre e la sua identità non cambia mai: il caso comune per parametri di funzione e alias.
- Usa un puntatore quando vale una di queste condizioni:
- "Niente" è uno stato valido (argomento facoltativo, una ricerca che potrebbe non trovare nulla): un puntatore può essere
nullptr. - Devi puntare a oggetti diversi nel tempo: un puntatore può essere riassegnato.
- Gestisci memoria dell'heap su cui farai
delete(o, meglio, uno smart pointer), oppure percorri un array con l'aritmetica dei puntatori.
- "Niente" è uno stato valido (argomento facoltativo, una ricerca che potrebbe non trovare nulla): un puntatore può essere
Se nessuna di queste condizioni vale, un riferimento è la scelta più pulita e sicura, perché è il compilatore a garantire per te "sempre valido, mai riassegnato".
Errori comuni da evitare
- Aspettarsi che
ref = otherriassegni il riferimento. Invece assegna un valore dentro l'oggetto riferito. I riferimenti sono legati per sempre; se ti serve riassegnare, usa un puntatore. - Restituire un riferimento (o un puntatore) a una variabile locale.
int& f() { int x = 5; return x; }restituisce un riferimento pendente:xmuore quandoftermina e usare il risultato è undefined behavior. La stessa trappola colpisce i puntatori (return &x;). - Fabbricare un "riferimento nullo". Scrivere
int& r = *p;quandopènullptrè undefined behavior nel momento stesso in cui dereferenzi, non un riferimento "vuoto" sicuro. Esprimi l'opzionalità con un puntatore o constd::optional. - Usare un puntatore per abitudine. Se l'argomento esiste sempre e non lo riassegnerai mai, un riferimento elimina un'intera categoria di controlli sul nullo e di crash. Non pagare per capacità che non usi.
Prossimo passo: la memoria dinamica
Finora ogni oggetto a cui ti sei riferito o hai puntato è stato creato automaticamente sullo stack. La prossima pagina, memoria dinamica, tratta new e delete: chiedere memoria al sistema operativo a runtime, perché a possederla sono i puntatori (non i riferimenti) e come dimenticare di liberarla causa memory leak.
Domande frequenti
Qual è la differenza tra un riferimento e un puntatore in C++?
Un riferimento è un alias di un oggetto esistente: deve essere inizializzato, non può mai essere nullo e in seguito non può mai riferirsi a un oggetto diverso. Un puntatore è una variabile separata che contiene un indirizzo: può essere nullo, può essere fatto puntare altrove e supporta l'aritmetica dei puntatori. Con i riferimenti si usa la sintassi &, con i puntatori */->.
Quando usare un puntatore invece di un riferimento in C++?
Usa un puntatore quando "niente" è uno stato valido (un argomento facoltativo, un risultato non trovato), quando devi farlo puntare a oggetti diversi nel tempo o quando possiedi memoria dell'heap su cui farai delete. Usa un riferimento quando l'oggetto esiste sempre e non cambia mai identità, il che copre la maggior parte dei parametri di funzione.
Un riferimento può essere nullo in C++?
No. Un riferimento valido si riferisce sempre a un oggetto reale, quindi non devi mai controllare se è nullo. Se crei un riferimento dereferenziando un puntatore nullo (int& r = *p; con p nullo) ottieni undefined behavior, non un riferimento nullo. Quando devi esprimere "forse niente", usa un puntatore o std::optional.