corso Quali sono le differenze tra una variabile puntatore e una variabile di riferimento in C++?




comandi pointers (24)

So che i riferimenti sono zucchero sintattico, quindi il codice è più facile da leggere e scrivere.

Ma quali sono le differenze?

Riepilogo delle risposte e dei collegamenti seguenti:

  1. Un puntatore può essere riassegnato qualsiasi numero di volte mentre un riferimento non può essere riassegnato dopo l'associazione.
  2. I puntatori non possono puntare da nessuna parte ( NULL ), mentre un riferimento fa sempre riferimento a un oggetto.
  3. Non puoi prendere l'indirizzo di un riferimento come puoi con i puntatori.
  4. Non c'è "aritmetica di riferimento" (ma puoi prendere l'indirizzo di un oggetto puntato da un riferimento e fare l'aritmetica del puntatore su di esso come in &obj + 5 ).

Per chiarire un equivoco:

Lo standard C ++ è molto attento a evitare di dettare come un compilatore può implementare riferimenti, ma ogni compilatore C ++ implementa riferimenti come puntatori. Cioè, una dichiarazione come:

int &ri = i;

se non è ottimizzato completamente , assegna la stessa quantità di memoria di un puntatore e inserisce l'indirizzo di i in tale archivio.

Quindi, un puntatore e un riferimento utilizzano entrambi la stessa quantità di memoria.

Come regola generale,

  • Utilizzare i riferimenti nei parametri di funzione e nei tipi restituiti per fornire interfacce utili e autodocumentanti.
  • Utilizzare i puntatori per l'implementazione di algoritmi e strutture dati.

Lettura interessante:


Answer #1

Forse alcune metafore aiuteranno; Nel contesto dello schermo del desktop -

  • Un riferimento richiede di specificare una finestra effettiva.
  • Un puntatore richiede la posizione di un pezzo di spazio sullo schermo che assicuri che conterrà zero o più istanze di quel tipo di finestra.

Answer #2

Un'altra differenza è che puoi avere dei puntatori a un tipo di vuoto (e significa puntatore a qualsiasi cosa) ma i riferimenti al vuoto sono proibiti.

int a;
void * p = &a; // ok
void & p = a;  //  forbidden

Non posso dire di essere davvero contento di questa particolare differenza. Preferirei molto che fosse permesso con il riferimento di significato a qualsiasi cosa con un indirizzo e altrimenti lo stesso comportamento per i riferimenti. Permetterebbe di definire alcuni equivalenti delle funzioni della libreria C come memcpy usando i riferimenti.


Answer #3

Contrariamente all'opinione comune, è possibile avere un riferimento che è NULL.

int * p = NULL;
int & r = *p;
r = 1;  // crash! (if you're lucky)

Certo, è molto più difficile da fare con un riferimento - ma se lo gestisci, ti strapperai i capelli cercando di trovarlo. I riferimenti non sono intrinsecamente sicuri in C ++!

Tecnicamente questo è un riferimento non valido , non un riferimento null. Il C ++ non supporta i riferimenti null come concetto che potresti trovare in altre lingue. Esistono anche altri tipi di riferimenti non validi. Qualsiasi riferimento non valido solleva lo spettro del comportamento non definito , proprio come farebbe con un puntatore non valido.

L'errore effettivo è nel dereferenziamento del puntatore NULL, prima dell'assegnazione a un riferimento. Ma non sono a conoscenza di alcun compilatore che genererà errori su tale condizione - l'errore si propagherà in un punto più avanti nel codice. Questo è ciò che rende questo problema così insidioso. La maggior parte delle volte, se si denota un puntatore NULL, si blocca proprio in quel punto e non ci vuole molto a fare il debug per capirlo.

Il mio esempio sopra è breve e forzato. Ecco un esempio più realistico.

class MyClass
{
    ...
    virtual void DoSomething(int,int,int,int,int);
};

void Foo(const MyClass & bar)
{
    ...
    bar.DoSomething(i1,i2,i3,i4,i5);  // crash occurs here due to memory access violation - obvious why?
}

MyClass * GetInstance()
{
    if (somecondition)
        return NULL;
    ...
}

MyClass * p = GetInstance();
Foo(*p);

Voglio ribadire che l'unico modo per ottenere un riferimento null è attraverso il codice non valido e, una volta ottenuto, si ottiene un comportamento indefinito. Non ha mai senso verificare un riferimento null; per esempio puoi provare if(&bar==NULL)... ma il compilatore potrebbe ottimizzare l'affermazione fuori dall'esistenza! Un riferimento valido non può mai essere NULL, quindi dal punto di vista del compilatore il confronto è sempre falso ed è libero di eliminare la clausola if come dead code - questa è l'essenza del comportamento non definito.

Il modo corretto per evitare problemi consiste nell'evitare il dereferenziamento di un puntatore NULL per creare un riferimento. Ecco un modo automatico per realizzare questo.

template<typename T>
T& deref(T* p)
{
    if (p == NULL)
        throw std::invalid_argument(std::string("NULL reference"));
    return *p;
}

MyClass * p = GetInstance();
Foo(deref(p));

Per uno sguardo più vecchio a questo problema da qualcuno con migliori capacità di scrittura, vedi Null References di Jim Hyslop e Herb Sutter.

Per un altro esempio dei pericoli del dereferenziamento, un puntatore nullo può vedere Esporre un comportamento indefinito quando si tenta di portare il codice su un'altra piattaforma di Raymond Chen.


Answer #4

Non importa quanto spazio occupi dal momento che non si può effettivamente vedere alcun effetto collaterale (senza l'esecuzione di codice) di qualsiasi spazio occupi.

D'altra parte, una delle principali differenze tra riferimenti e puntatori è che i temporaries assegnati ai riferimenti const vivono fino a quando il riferimento const non rientra nello scope.

Per esempio:

class scope_test
{
public:
    ~scope_test() { printf("scope_test done!\n"); }
};

...

{
    const scope_test &test= scope_test();
    printf("in scope\n");
}

stamperà:

in scope
scope_test done!

Questo è il meccanismo linguistico che consente a ScopeGuard di funzionare.


Answer #5

Inoltre, un riferimento che è un parametro di una funzione che è inline può essere gestito in modo diverso rispetto a un puntatore.

void increment(int *ptrint) { (*ptrint)++; }
void increment(int &refint) { refint++; }
void incptrtest()
{
    int testptr=0;
    increment(&testptr);
}
void increftest()
{
    int testref=0;
    increment(testref);
}

Molti compilatori quando si incorpora la versione puntatore uno imporrà effettivamente una scrittura in memoria (stiamo prendendo esplicitamente l'indirizzo). Tuttavia, lasceranno il riferimento in un registro che è più ottimale.

Ovviamente, per le funzioni che non sono in linea, il puntatore e il riferimento generano lo stesso codice ed è sempre meglio passare i valori intrinseci in base al valore anziché mediante riferimento se non vengono modificati e restituiti dalla funzione.


Answer #6

A rischio di aggiungere confusione, voglio introdurre alcuni input, sono sicuro che dipende in gran parte da come il compilatore implementa i riferimenti, ma nel caso di gcc l'idea che un riferimento possa solo puntare a una variabile sullo stack non è corretto, prendi questo ad esempio:

#include <iostream>
int main(int argc, char** argv) {
    // Create a string on the heap
    std::string *str_ptr = new std::string("THIS IS A STRING");
    // Dereference the string on the heap, and assign it to the reference
    std::string &str_ref = *str_ptr;
    // Not even a compiler warning! At least with gcc
    // Now lets try to print it's value!
    std::cout << str_ref << std::endl;
    // It works! Now lets print and compare actual memory addresses
    std::cout << str_ptr << " : " << &str_ref << std::endl;
    // Exactly the same, now remember to free the memory on the heap
    delete str_ptr;
}

Che emette questo:

THIS IS A STRING
0xbb2070 : 0xbb2070

Se noti che anche gli indirizzi di memoria sono esattamente gli stessi, significa che il riferimento punta correttamente a una variabile sull'heap! Ora se vuoi davvero diventare pazzo, funziona anche:

int main(int argc, char** argv) {
    // In the actual new declaration let immediately de-reference and assign it to the reference
    std::string &str_ref = *(new std::string("THIS IS A STRING"));
    // Once again, it works! (at least in gcc)
    std::cout << str_ref;
    // Once again it prints fine, however we have no pointer to the heap allocation, right? So how do we free the space we just ignorantly created?
    delete &str_ref;
    /*And, it works, because we are taking the memory address that the reference is
    storing, and deleting it, which is all a pointer is doing, just we have to specify
    the address with '&' whereas a pointer does that implicitly, this is sort of like
    calling delete &(*str_ptr); (which also compiles and runs fine).*/
}

Che emette questo:

THIS IS A STRING

Quindi un riferimento È un puntatore sotto il cappuccio, entrambi stanno semplicemente memorizzando un indirizzo di memoria, dove l'indirizzo sta puntando è irrilevante, cosa pensate che succederebbe se chiamassi std :: cout << str_ref; DOPO chiamare delete & str_ref? Bene, ovviamente compila bene, ma causa un errore di segmentazione in fase di esecuzione perché non sta puntando più a una variabile valida, in pratica abbiamo un riferimento interrotto che esiste ancora (finché non rientra nello scope), ma è inutile.

In altre parole, un riferimento non è altro che un puntatore che ha la meccanica del puntatore sottratta, rendendola più sicura e più facile da usare (nessuna puntatore accidentale di matematica, nessuna confusione '.' E '->', ecc.), Assumendoti non provare assurdità come i miei esempi sopra;)

Ora, indipendentemente da come un compilatore gestisce i riferimenti, avrà sempre una sorta di puntatore sotto il cappuccio, perché un riferimento deve fare riferimento a una variabile specifica in uno specifico indirizzo di memoria affinché funzioni come previsto, non c'è modo di aggirare questo (quindi il termine "riferimento").

L'unica regola importante che è importante ricordare con i riferimenti è che devono essere definiti al momento della dichiarazione (ad eccezione di un riferimento in un'intestazione, in tal caso deve essere definito nel costruttore, dopo che l'oggetto in cui è contenuto è costruito è troppo tardi per definirlo).

Ricorda, i miei esempi sopra sono solo che, esempi che dimostrano che cos'è un riferimento, non vorrai mai usare un riferimento in quei modi! Per un uso corretto di un riferimento ci sono già molte risposte qui che hanno colpito il chiodo sulla testa


Answer #7

Hai dimenticato la parte più importante:

accesso membro con i puntatori utilizza ->
accesso membro con riferimenti usa .

foo.bar è chiaramente superiore a foo->bar nello stesso modo in cui vi è chiaramente superiore a Emacs :-)


Answer #8

C'è una differenza semantica che può sembrare esoterica se non si ha familiarità con lo studio dei linguaggi del computer in modo astratto o addirittura accademico.

Al livello più alto, l'idea di riferimento è che sono "alias" trasparenti. Il tuo computer potrebbe usare un indirizzo per farli funzionare, ma non dovresti preoccuparti di questo: dovresti pensare a loro come "solo un altro nome" per un oggetto esistente e la sintassi lo riflette. Sono più rigidi dei puntatori in modo che il compilatore possa avvisarti in modo più affidabile quando stai per creare un riferimento ciondolante, rispetto a quando stai per creare un puntatore pendente.

Oltre a ciò, ci sono naturalmente alcune differenze pratiche tra puntatori e riferimenti. La sintassi per usarli è ovviamente diversa e non è possibile "ri-inserire" riferimenti, avere riferimenti al nulla o avere riferimenti a riferimenti.


Answer #9

Un riferimento è un alias per un'altra variabile mentre un puntatore contiene l'indirizzo di memoria di una variabile. I riferimenti vengono generalmente utilizzati come parametri di funzione in modo che l'oggetto passato non sia la copia ma l'oggetto stesso.

    void fun(int &a, int &b); // A common usage of references.
    int a = 0;
    int &b = a; // b is an alias for a. Not so common to use. 

Answer #10

Un riferimento non può mai essere NULL .


Answer #11

C'è una differenza non tecnica molto importante tra puntatori e riferimenti: un argomento passato a una funzione dal puntatore è molto più visibile di un argomento passato a una funzione dal riferimento non const. Per esempio:

void fn1(std::string s);
void fn2(const std::string& s);
void fn3(std::string& s);
void fn4(std::string* s);

void bar() {
    std::string x;
    fn1(x);  // Cannot modify x
    fn2(x);  // Cannot modify x (without const_cast)
    fn3(x);  // CAN modify x!
    fn4(&x); // Can modify x (but is obvious about it)
}

Tornando in C, una chiamata simile fn(x)può essere passata solo in base al valore, quindi non può assolutamente essere modificata x; per modificare un argomento è necessario passare un puntatore fn(&x). Quindi se un argomento non era preceduto da un argomento &sapevi che non sarebbe stato modificato. (L'inverso, &significa modificato, non era vero perché a volte bisognava passare strutture di sola lettura per constpuntatore.)

Alcuni sostengono che questa sia una funzione così utile nella lettura del codice, che i parametri del puntatore debbano sempre essere usati per i parametri modificabili piuttosto che per i non constriferimenti, anche se la funzione non si aspetta mai a nullptr. Cioè, quelle persone sostengono che le firme di funzioni come fn3()sopra non dovrebbero essere consentite. Le linee guida di stile C ++ di Google ne sono un esempio.


Answer #12
  1. Un puntatore può essere riassegnato:

    int x = 5;
    int y = 6;
    int *p;
    p =  &x;
    p = &y;
    *p = 10;
    assert(x == 5);
    assert(y == 10);
    

    Un riferimento non può e deve essere assegnato all'inizializzazione:

    int x = 5;
    int y = 6;
    int &r = x;
    
  2. Un puntatore ha il proprio indirizzo e dimensione di memoria nello stack (4 byte su x86), mentre un riferimento condivide lo stesso indirizzo di memoria (con la variabile originale) ma occupa anche dello spazio nello stack. Poiché un riferimento ha lo stesso indirizzo della stessa variabile originale, è sicuro pensare a un riferimento come un altro nome per la stessa variabile. Nota: ciò a cui punta il puntatore può essere sullo stack o sull'heap. Idem un riferimento. La mia affermazione in questa affermazione non è che un puntatore deve puntare allo stack. Un puntatore è solo una variabile che contiene un indirizzo di memoria. Questa variabile è in pila. Poiché un riferimento ha il proprio spazio nello stack e poiché l'indirizzo è uguale alla variabile a cui fa riferimento. Altro su stack vs heap . Ciò implica che esiste un vero indirizzo di riferimento che il compilatore non ti dirà.

    int x = 0;
    int &r = x;
    int *p = &x;
    int *p2 = &r;
    assert(p == p2);
    
  3. Puoi avere dei puntatori ai puntatori ai puntatori che offrono livelli aggiuntivi di riferimento indiretto. Mentre i riferimenti offrono solo un livello di riferimento indiretto.

    int x = 0;
    int y = 0;
    int *p = &x;
    int *q = &y;
    int **pp = &p;
    pp = &q;//*pp = q
    **pp = 4;
    assert(y == 4);
    assert(x == 0);
    
  4. Il puntatore può essere assegnato direttamente a nullptr , mentre il riferimento non può. Se ci provi abbastanza e sai come, puoi creare l'indirizzo di un riferimento nullptr . Allo stesso modo, se ci provi abbastanza puoi avere un riferimento a un puntatore, e quindi quel riferimento può contenere nullptr .

    int *p = nullptr;
    int &r = nullptr; <--- compiling error
    int &r = *p;  <--- likely no compiling error, especially if the nullptr is hidden behind a function call, yet it refers to a non-existent int at address 0
    
  5. I puntatori possono scorrere su un array, puoi usare ++ per andare all'elemento successivo a cui punta il puntatore, e + 4 per andare al quinto elemento. Non importa quale sia l'oggetto a cui punta il puntatore.

  6. Un puntatore deve essere dereferenziato con * per accedere alla posizione di memoria a cui punta, mentre un riferimento può essere utilizzato direttamente. Un puntatore a una classe / struct usa -> per accedere ai suoi membri mentre un riferimento usa a . .

  7. Un puntatore è una variabile che contiene un indirizzo di memoria. Indipendentemente da come viene implementato un riferimento, un riferimento ha lo stesso indirizzo di memoria dell'elemento a cui fa riferimento.

  8. I riferimenti non possono essere inseriti in una matrice, mentre i puntatori possono essere (menzionati dall'utente @litb)

  9. I riferimenti di Const possono essere associati a provvisori. I puntatori non possono (non senza qualche indiretta):

    const int &x = int(12); //legal C++
    int *y = &int(12); //illegal to dereference a temporary.
    

    Ciò rende const& sicuro l'uso negli elenchi di argomenti e così via.


Answer #13

La differenza è che la variabile puntatore non costante (da non confondere con un puntatore a costante) può essere modificata in qualche momento durante l'esecuzione del programma, richiede la semantica del puntatore da utilizzare (&, *) operatori, mentre i riferimenti possono essere impostati al momento dell'inizializzazione solo (è per questo che è possibile impostarli solo nella lista di inizializzazione del costruttore, ma non in qualcos'altro) e utilizzare la semantica di accesso al valore ordinario. Fondamentalmente sono stati introdotti riferimenti per consentire il supporto per il sovraccarico degli operatori, come avevo letto in un libro molto vecchio. Come qualcuno ha affermato in questo thread - il puntatore può essere impostato su 0 o qualsiasi valore tu voglia. 0 (NULL, nullptr) significa che il puntatore è inizializzato con nulla. È un errore di dereferenziare il puntatore nullo. Ma in realtà il puntatore potrebbe contenere un valore che non punta ad una corretta posizione di memoria.I riferimenti a loro volta non tentano di consentire a un utente di inizializzare un riferimento a qualcosa che non può essere referenziato a causa del fatto che si fornisce sempre un valore di tipo corretto ad esso. Sebbene ci siano molti modi per rendere inizializzata la variabile di riferimento in una posizione di memoria errata, è meglio non scavare in profondità nei dettagli. A livello macchina sia il puntatore che il riferimento lavorano uniformemente, tramite puntatori. Diciamo che i riferimenti essenziali sono lo zucchero sintattico. i riferimenti rvalue sono diversi da questo: sono naturalmente oggetti stack / heap.Sebbene ci siano molti modi per rendere inizializzata la variabile di riferimento in una posizione di memoria errata, è meglio non scavare in profondità nei dettagli. A livello macchina sia il puntatore che il riferimento lavorano uniformemente, tramite puntatori. Diciamo che i riferimenti essenziali sono lo zucchero sintattico. i riferimenti rvalue sono diversi da questo: sono naturalmente oggetti stack / heap.Sebbene ci siano molti modi per rendere inizializzata la variabile di riferimento in una posizione di memoria errata, è meglio non scavare in profondità nei dettagli. A livello macchina sia il puntatore che il riferimento lavorano uniformemente, tramite puntatori. Diciamo che i riferimenti essenziali sono lo zucchero sintattico. i riferimenti rvalue sono diversi da questo: sono naturalmente oggetti stack / heap.


Answer #14

Mi sembra che ci sia ancora un altro punto che non è stato trattato qui.

A differenza dei puntatori, i riferimenti sono sintatticamente equivalenti all'oggetto a cui si riferiscono, cioè qualsiasi operazione che può essere applicata a un oggetto funziona per un riferimento e con la stessa sintassi esatta (l'eccezione è ovviamente l'inizializzazione).

Anche se questo può sembrare superficiale, credo che questa proprietà sia cruciale per un certo numero di funzionalità C ++, ad esempio:

  • Modelli . Poiché i parametri del modello sono tipografici anatra, le proprietà sintattiche di un tipo sono tutto ciò che conta, così spesso lo stesso modello può essere utilizzato con entrambi Te T&.
    (o std::reference_wrapper<T>che si basa ancora su un cast implicito su T&)
    Modelli che coprono entrambi T&e T&&sono ancora più comuni.

  • Lvalues . Considera la frase str[0] = 'X';Senza riferimenti, funzionerebbe solo con c-string ( char* str). Restituire il carattere per riferimento consente alle classi definite dall'utente di avere la stessa notazione.

  • Copia i costruttori . Sintatticamente ha senso passare gli oggetti per copiare i costruttori e non i puntatori agli oggetti. Ma non c'è un modo per un costruttore di copia di prendere un oggetto in base al valore: ciò comporterebbe una chiamata ricorsiva allo stesso costruttore di copie. Questo lascia i riferimenti come l'unica opzione qui.

  • Sovraccarichi operatore . Con i riferimenti è possibile introdurre l'indirezione indiretta a una chiamata dell'operatore, ad esempio operator+(const T& a, const T& b)mantenendo la stessa notazione infissa. Questo funziona anche per funzioni regolari sovraccariche.

Questi punti potenziano una parte considerevole del C ++ e della libreria standard, quindi questa è una proprietà piuttosto importante dei riferimenti.


Answer #15

In realtà, un riferimento non è proprio come un puntatore.

Un compilatore mantiene i "riferimenti" alle variabili, associando un nome ad un indirizzo di memoria; questo è il suo lavoro per tradurre qualsiasi nome di variabile in un indirizzo di memoria durante la compilazione.

Quando crei un riferimento, dici al compilatore solo che assegni un altro nome alla variabile puntatore; ecco perché i riferimenti non possono "puntare a null", perché una variabile non può essere, e non essere.

I puntatori sono variabili; contengono l'indirizzo di qualche altra variabile o possono essere nulli. L'importante è che un puntatore abbia un valore, mentre un riferimento ha solo una variabile a cui fa riferimento.

Ora qualche spiegazione del codice reale:

int a = 0;
int& b = a;

Qui non stai creando un'altra variabile che punta a a ; stai solo aggiungendo un altro nome al contenuto di memoria che contiene il valore di a . Questa memoria ora ha due nomi, a e b , e può essere indirizzata usando entrambi i nomi.

void increment(int& n)
{
    n = n + 1;
}

int a;
increment(a);

Quando si chiama una funzione, il compilatore di solito genera spazi di memoria per gli argomenti da copiare. La firma della funzione definisce gli spazi che dovrebbero essere creati e fornisce il nome che dovrebbe essere usato per questi spazi. La dichiarazione di un parametro come riferimento indica semplicemente al compilatore di utilizzare lo spazio di memoria della variabile di input anziché allocare un nuovo spazio di memoria durante la chiamata al metodo. Può sembrare strano dire che la tua funzione manipolerà direttamente una variabile dichiarata nell'ambito della chiamata, ma ricorda che quando si esegue il codice compilato, non c'è più spazio; c'è solo una memoria piatta e il tuo codice funzione può manipolare qualsiasi variabile.

Ora ci possono essere alcuni casi in cui il compilatore potrebbe non essere in grado di conoscere il riferimento durante la compilazione, come quando si utilizza una variabile esterna. Quindi un riferimento può o non può essere implementato come un puntatore nel codice sottostante. Ma negli esempi che ti ho dato, molto probabilmente non sarà implementato con un puntatore.


Answer #16

Cos'è un riferimento C ++ ( per i programmatori C )

Un riferimento può essere pensato come un puntatore costante (da non confondere con un puntatore a un valore costante!) Con l'indirezione automatica, cioè il compilatore applicherà l'operatore * per te.

Tutti i riferimenti devono essere inizializzati con un valore non nullo o la compilazione avrà esito negativo. Non è possibile ottenere l'indirizzo di un riferimento - l'operatore di indirizzo restituirà invece l'indirizzo del valore di riferimento - né è possibile fare aritmetica sui riferimenti.

I programmatori C potrebbero non gradire i riferimenti C ++ in quanto non saranno più ovvi quando si verifica indiretta o se un argomento viene passato per valore o per puntatore senza guardare le firme delle funzioni.

Ai programmatori C ++ potrebbe non piacere usare i puntatori perché sono considerati non sicuri - sebbene i riferimenti non siano realmente più sicuri dei puntatori costanti tranne nei casi più banali - mancano della convenienza dell'indirizzamento automatico e portano una diversa connotazione semantica.

Considera la seguente dichiarazione dalle FAQ del C ++ :

Anche se un riferimento viene spesso implementato utilizzando un indirizzo nella lingua dell'assembly sottostante, non pensare a un riferimento come a un puntatore dall'aspetto divertente a un oggetto. Un riferimento è l'oggetto. Non è un puntatore all'oggetto, né una copia dell'oggetto. È l'oggetto

Ma se un riferimento fosse davvero l'oggetto, come potrebbero esserci riferimenti ciondolanti? Nei linguaggi non gestiti, è impossibile che i riferimenti siano "più sicuri" dei puntatori: in genere non è un modo per fare in modo affidabile l'alias dei valori attraverso i limiti del campo di applicazione!

Perché considero utili i riferimenti C ++

Provenienti da uno sfondo C, i riferimenti C ++ possono sembrare un concetto un po 'sciocco, ma dovremmo comunque usarli al posto dei puntatori laddove possibile: l'indirezione automatica è conveniente e i riferimenti diventano particolarmente utili quando si ha a che fare con RAII - ma non a causa di alcuna sicurezza percepita vantaggio, ma piuttosto perché rendono la scrittura del codice idiomatico meno imbarazzante.

RAII è uno dei concetti centrali del C ++, ma interagisce in modo non banale con la semantica della copia. Passare gli oggetti per riferimento evita questi problemi poiché non è coinvolta alcuna copia. Se i riferimenti non fossero presenti nella lingua, dovresti invece usare dei puntatori, che sono più complicati da usare, violando così il principio del linguaggio design che la soluzione delle migliori pratiche dovrebbe essere più semplice delle alternative.


Answer #17

Uso i riferimenti a meno che non abbia bisogno di uno di questi:

  • I puntatori nulli possono essere utilizzati come valore sentinella, spesso un modo economico per evitare il sovraccarico di funzioni o l'uso di un bool.

  • Puoi fare aritmetica su un puntatore. Per esempio,p += offset;


Answer #18

Questo è basato sul tutorial . Ciò che è scritto lo rende più chiaro:

>>> The address that locates a variable within memory is
    what we call a reference to that variable. (5th paragraph at page 63)

>>> The variable that stores the reference to another
    variable is what we call a pointer. (3rd paragraph at page 64)

Semplicemente ricordarlo,

>>> reference stands for memory location
>>> pointer is a reference container (Maybe because we will use it for
several times, it is better to remember that reference.)

Inoltre, poiché possiamo fare riferimento a quasi tutti i tutorial sui puntatori, un puntatore è un oggetto supportato dall'aritmetica del puntatore che rende il puntatore simile a un array.

Guarda la seguente dichiarazione,

int Tom(0);
int & alias_Tom = Tom;

alias_Tompuò essere inteso come un alias of a variable(diverso con typedef, che è alias of a type) Tom. Va anche bene dimenticare che la terminologia di tale affermazione è di creare un riferimento a Tom.


Answer #19

I riferimenti sono molto simili ai puntatori, ma sono creati appositamente per essere utili all'ottimizzazione dei compilatori.

  • I riferimenti sono progettati in modo tale che è sostanzialmente più semplice per il compilatore tracciare quali alias di riferimento quali variabili. Due importanti caratteristiche sono molto importanti: nessuna "aritmetica di riferimento" e nessuna riassegnazione di riferimenti. Questi permettono al compilatore di capire quali riferimenti alias quali variabili in fase di compilazione.
  • I riferimenti possono riferirsi a variabili che non hanno indirizzi di memoria, come quelli che il compilatore sceglie di inserire nei registri. Se si prende l'indirizzo di una variabile locale, è molto difficile per il compilatore inserirlo in un registro.

Come esempio:

void maybeModify(int& x); // may modify x in some way

void hurtTheCompilersOptimizer(short size, int array[])
{
    // This function is designed to do something particularly troublesome
    // for optimizers. It will constantly call maybeModify on array[0] while
    // adding array[1] to array[2]..array[size-1]. There's no real reason to
    // do this, other than to demonstrate the power of references.
    for (int i = 2; i < (int)size; i++) {
        maybeModify(array[0]);
        array[i] += array[1];
    }
}

Un compilatore ottimizzante potrebbe rendersi conto che stiamo accedendo a [0] e a [1] un bel po '. Mi piacerebbe ottimizzare l'algoritmo per:

void hurtTheCompilersOptimizer(short size, int array[])
{
    // Do the same thing as above, but instead of accessing array[1]
    // all the time, access it once and store the result in a register,
    // which is much faster to do arithmetic with.
    register int a0 = a[0];
    register int a1 = a[1]; // access a[1] once
    for (int i = 2; i < (int)size; i++) {
        maybeModify(a0); // Give maybeModify a reference to a register
        array[i] += a1;  // Use the saved register value over and over
    }
    a[0] = a0; // Store the modified a[0] back into the array
}

Per fare una tale ottimizzazione, è necessario dimostrare che nulla può cambiare array [1] durante la chiamata. Questo è piuttosto facile da fare. non è mai inferiore a 2, quindi array [i] non può mai fare riferimento a array [1]. mayModify () è dato a0 come riferimento (aliasing array [0]). Poiché non esiste un'aritmetica di "riferimento", il compilatore deve solo dimostrare che forse Modify non ottiene mai l'indirizzo di x, e ha dimostrato che nulla cambia array [1].

Deve anche dimostrare che non ci sono modi in cui una chiamata futura potrebbe leggere / scrivere un [0] mentre abbiamo una copia temporanea del registro in a0. Questo è spesso banale da dimostrare, perché in molti casi è ovvio che il riferimento non è mai memorizzato in una struttura permanente come un'istanza di classe.

Ora fai la stessa cosa con i puntatori

void maybeModify(int* x); // May modify x in some way

void hurtTheCompilersOptimizer(short size, int array[])
{
    // Same operation, only now with pointers, making the
    // optimization trickier.
    for (int i = 2; i < (int)size; i++) {
        maybeModify(&(array[0]));
        array[i] += array[1];
    }
}

Il comportamento è lo stesso; solo ora è molto più difficile dimostrare che forse Modify non modifica mai l'array [1], perché gli abbiamo già dato un puntatore; il gatto è fuori dalla borsa. Ora deve fare la prova molto più difficile: un'analisi statica di forseModify per dimostrare che non scrive mai in & x + 1. Deve anche dimostrare che non salva mai un puntatore che può riferirsi all'array [0], che è solo così difficile.

I compilatori moderni stanno migliorando sempre di più nell'analisi statica, ma è sempre bello aiutarli e utilizzare i riferimenti.

Ovviamente, escludendo tali intelligenti ottimizzazioni, i compilatori trasformeranno effettivamente i riferimenti in puntatori quando necessario.

EDIT: Cinque anni dopo aver postato questa risposta, ho trovato una differenza tecnica effettiva in cui i riferimenti sono diversi rispetto a un modo diverso di guardare lo stesso concetto di indirizzamento. I riferimenti possono modificare la durata di vita degli oggetti temporanei in un modo che i puntatori non possono.

F createF(int argument);

void extending()
{
    const F& ref = createF(5);
    std::cout << ref.getArgument() << std::endl;
};

Normalmente oggetti temporanei come quello creato dalla chiamata a createF(5) vengono distrutti alla fine dell'espressione. Tuttavia, legando tale oggetto a un riferimento, ref , il C ++ estenderà la durata di vita di quell'oggetto temporaneo fino a quando il riferimento non esce dall'ambito.


Answer #20

Differenza tra puntatore e riferimento

Un puntatore può essere inizializzato a 0 e un riferimento no. In effetti, un riferimento deve anche riferirsi a un oggetto, ma un puntatore può essere il puntatore nullo:

int* p = 0;

Ma non possiamo avere int& p = 0;e ancheint& p=5 ; .

Infatti per farlo correttamente, dobbiamo aver dichiarato e definito un oggetto al primo quindi possiamo fare un riferimento a quell'oggetto, quindi la corretta implementazione del codice precedente sarà:

Int x = 0;
Int y = 5;
Int& p = x;
Int& p1 = y;

Un altro punto importante è che possiamo fare la dichiarazione del puntatore senza inizializzazione, tuttavia non è possibile fare nulla in caso di riferimento che deve fare sempre riferimento a variabili o oggetti. Tuttavia, tale uso di un puntatore è rischioso, quindi in genere controlliamo se il puntatore effettivamente sta puntando a qualcosa o no. In caso di riferimento, tale controllo non è necessario, poiché sappiamo già che il riferimento a un oggetto durante la dichiarazione è obbligatorio.

Un'altra differenza è che il puntatore può puntare a un altro oggetto, ma il riferimento fa sempre riferimento allo stesso oggetto, prendiamo questo esempio:

Int a = 6, b = 5;
Int& rf = a;

Cout << rf << endl; // The result we will get is 6, because rf is referencing to the value of a.

rf = b;
cout << a << endl; // The result will be 5 because the value of b now will be stored into the address of a so the former value of a will be erased

Un altro punto: quando abbiamo un modello come un modello STL, questo tipo di modello di classe restituirà sempre un riferimento, non un puntatore, per facilitare la lettura o l'assegnazione di un nuovo valore utilizzando l'operatore []:

Std ::vector<int>v(10); // Initialize a vector with 10 elements
V[5] = 5; // Writing the value 5 into the 6 element of our vector, so if the returned type of operator [] was a pointer and not a reference we should write this *v[5]=5, by making a reference we overwrite the element by using the assignment "="

Answer #21

Questo programma potrebbe aiutare a comprendere la risposta della domanda. Questo è un semplice programma di riferimento "j" e un puntatore "ptr" che punta alla variabile "x".

#include<iostream>

using namespace std;

int main()
{
int *ptr=0, x=9; // pointer and variable declaration
ptr=&x; // pointer to variable "x"
int & j=x; // reference declaration; reference to variable "x"

cout << "x=" << x << endl;

cout << "&x=" << &x << endl;

cout << "j=" << j << endl;

cout << "&j=" << &j << endl;

cout << "*ptr=" << *ptr << endl;

cout << "ptr=" << ptr << endl;

cout << "&ptr=" << &ptr << endl;
    getch();
}

Esegui il programma e dai un'occhiata all'output e capirai.

Inoltre, risparmia 10 minuti e guarda questo video: https://www.youtube.com/watch?v=rlJrrGV0iOg


Answer #22

Mentre sia i riferimenti che i puntatori vengono utilizzati per accedere indirettamente a un altro valore, esistono due importanti differenze tra riferimenti e puntatori. Il primo è che un riferimento fa sempre riferimento a un oggetto: è un errore definire un riferimento senza inizializzarlo. Il comportamento dell'assegnazione è la seconda importante differenza: l'assegnazione a un riferimento modifica l'oggetto a cui è associato il riferimento; non ricollega il riferimento a un altro oggetto. Una volta inizializzato, un riferimento si riferisce sempre allo stesso oggetto sottostante.

Considera questi due frammenti di programma. Nel primo, assegniamo un puntatore a un altro:

int ival = 1024, ival2 = 2048;
int *pi = &ival, *pi2 = &ival2;
pi = pi2;    // pi now points to ival2

Dopo l'assegnazione, ival, l'oggetto indirizzato da pi rimane invariato. L'assegnazione cambia il valore di pi, facendo in modo che punti a un oggetto diverso. Ora considera un programma simile che assegna due riferimenti:

int &ri = ival, &ri2 = ival2;
ri = ri2;    // assigns ival2 to ival

Questo incarico cambia ival, il valore riferito da ri e non il riferimento stesso. Dopo l'assegnazione, i due riferimenti si riferiscono ancora ai loro oggetti originali e il valore di tali oggetti ora è lo stesso.


Answer #23

Un altro uso interessante dei riferimenti è fornire un argomento predefinito di un tipo definito dall'utente:

class UDT
{
public:
   UDT() : val_d(33) {};
   UDT(int val) : val_d(val) {};
   virtual ~UDT() {};
private:
   int val_d;
};

class UDT_Derived : public UDT
{
public:
   UDT_Derived() : UDT() {};
   virtual ~UDT_Derived() {};
};

class Behavior
{
public:
   Behavior(
      const UDT &udt = UDT()
   )  {};
};

int main()
{
   Behavior b; // take default

   UDT u(88);
   Behavior c(u);

   UDT_Derived ud;
   Behavior d(ud);

   return 1;
}

L'aroma di default usa l'aspetto 'bind const riferimento a temporaneo' dei riferimenti.


Answer #24

Sia i riferimenti che i puntatori possono essere utilizzati per modificare le variabili locali di una funzione all'interno di un'altra funzione. Entrambi possono essere utilizzati anche per salvare la copia di oggetti grandi quando vengono passati come argomenti alle funzioni o restituiti da funzioni, per ottenere un guadagno di efficienza. Nonostante le somiglianze sopra, ci sono seguenti differenze tra riferimenti e puntatori.

I riferimenti sono meno potenti dei puntatori

1) Una volta creato un riferimento, non può essere successivamente fatto per fare riferimento a un altro oggetto; non può essere reinserito. Questo è spesso fatto con i puntatori.

2) I riferimenti non possono essere NULL. I puntatori vengono spesso resi NULL per indicare che non stanno puntando a nessuna cosa valida.

3) Un riferimento deve essere inizializzato quando dichiarato. Non esiste una tale restrizione con i puntatori

A causa delle limitazioni di cui sopra, i riferimenti in C ++ non possono essere utilizzati per l'implementazione di strutture di dati come Elenco collegato, Albero, ecc. In Java, i riferimenti non hanno restrizioni superiori e possono essere utilizzati per implementare tutte le strutture di dati. Le referenze sono più potenti in Java, è la ragione principale per cui Java non ha bisogno di indicazioni.

I riferimenti sono più sicuri e più facili da usare:

1) Più sicuro: poiché i riferimenti devono essere inizializzati, è improbabile che esistano riferimenti selvaggi come puntatori selvaggi. È ancora possibile avere riferimenti che non si riferiscono a una posizione valida

2) Più facile da usare: i riferimenti non richiedono l'operatore di dereferenziamento per accedere al valore. Possono essere usati come normali variabili. L'operatore '&' è necessario solo al momento della dichiarazione. Inoltre, è possibile accedere ai membri di un riferimento a un oggetto con l'operatore punto ('.'), Diversamente dai puntatori in cui è necessario l'operatore di freccia (->) per accedere ai membri.

Insieme alle ragioni sopra riportate, ci sono pochi posti come l'argomento del costruttore di copia in cui il puntatore non può essere usato. Il riferimento deve essere utilizzato passare l'argomento nel costruttore di copie. Analogamente, i riferimenti devono essere usati per sovraccaricare alcuni operatori come ++ .







c++-faq