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


3264

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

Ma quali sono le differenze?


100
Penso che il punto 2 dovrebbe essere "Un puntatore può essere NULL ma non un riferimento. Solo il codice non valido può creare un riferimento NULL e il suo comportamento non è definito."
Mark Ransom,

19
I puntatori sono solo un altro tipo di oggetto e, come qualsiasi oggetto in C ++, possono essere una variabile. I riferimenti invece non sono mai oggetti, solo variabili.
Kerrek SB,

19
Questo viene compilato senza avvisi: int &x = *(int*)0;su gcc. Il riferimento può effettivamente indicare NULL.
Calmarius,

20
il riferimento è un alias variabile
Khaled.K il

20
Mi piace come la prima frase sia un errore totale. I riferimenti hanno la loro semantica.
Razze di leggerezza in orbita

Risposte:


1708
  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 di memoria e dimensioni 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 variabile originale stessa, è sicuro pensare a un riferimento come a un altro nome per la stessa variabile. Nota: a cosa punta un puntatore può essere nello stack o nell'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 è nello stack. Poiché un riferimento ha il suo spazio nello stack e poiché l'indirizzo è lo stesso della variabile a cui fa riferimento. Altro su stack vs heap. Ciò implica che esiste un indirizzo reale di un 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 puntatori a puntatori a puntatori che offrono livelli extra di indiretta. Considerando che 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. Un puntatore può essere assegnato nullptrdirettamente, mentre il riferimento no. Se ci provi abbastanza e sai come fare, puoi fare 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 un puntatore e + 4per andare al 5 ° elemento. Non importa quale sia la dimensione dell'oggetto a cui punta il puntatore.

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

  7. I riferimenti non possono essere inseriti in un array, mentre i puntatori possono essere (menzionati dall'utente @litb)

  8. I riferimenti costanti possono essere associati ai provvisori. I puntatori non possono (non senza qualche direzione indiretta):

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

    Questo rende const&più sicuro l'uso negli elenchi di argomenti e così via.


23
... ma il dereferenziamento di NULL non è definito. Ad esempio, non è possibile verificare se un riferimento è NULL (ad es. & Ref == NULL).
Pat Notz,

69
Il numero 2 non è vero. Un riferimento non è semplicemente "un altro nome per la stessa variabile". I riferimenti possono essere passati a funzioni, memorizzati in classi, ecc. In un modo molto simile ai puntatori. Esistono indipendentemente dalle variabili a cui indicano.
Derek Park,

31
Brian, lo stack non è rilevante. Riferimenti e puntatori non devono occupare spazio nello stack. Entrambi possono essere allocati sull'heap.
Derek Park,

22
Brian, il fatto che una variabile (in questo caso un puntatore o un riferimento) richieda spazio non significa che richieda spazio nello stack. Puntatori e riferimenti possono non solo indicare l'heap, ma possono essere effettivamente allocati sull'heap.
Derek Park,

38
un'altra differenza importante: i riferimenti non possono essere inseriti in un array
Johannes Schaub -

384

Che cos'è un riferimento C ++ ( per programmatori C )

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

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

I programmatori C potrebbero non gradire i riferimenti C ++ poiché non sarà più evidente quando si verifica l'indirizzamento 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 poiché sono considerati non sicuri - anche se i riferimenti non sono in realtà più sicuri dei puntatori costanti, tranne nei casi più banali - mancano della comodità dell'indirizzamento automatico e portano una diversa connotazione semantica.

Considera la seguente dichiarazione dalle Domande frequenti su C ++ :

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

Ma se un riferimento fosse davvero l'oggetto, come potrebbero esserci riferimenti penzolanti? Nei linguaggi non gestiti, è impossibile che i riferimenti siano più "sicuri" dei puntatori: in genere non esiste un modo per aliasare i valori in modo affidabile oltre i limiti dell'ambito!

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 ove possibile: l'indirizzamento automatico è conveniente e i riferimenti diventano particolarmente utili quando si ha a che fare con RAII - ma non a causa della sicurezza percepita vantaggio, ma piuttosto perché rendono meno imbarazzante scrivere codice idiomatico.

RAII è uno dei concetti centrali di C ++, ma interagisce in modo non banale con la copia della semantica. Il passaggio di oggetti per riferimento evita questi problemi poiché non è prevista alcuna copia. Se i riferimenti non fossero presenti nella lingua, dovresti invece utilizzare i puntatori, che sono più ingombranti da usare, violando così il principio di progettazione del linguaggio secondo cui la soluzione delle migliori pratiche dovrebbe essere più facile delle alternative.


17
@kriss: No, puoi anche ottenere un riferimento pendente restituendo una variabile automatica per riferimento.
Ben Voigt,

12
@kriss: è praticamente impossibile per un compilatore rilevare nel caso generale. Considera una funzione membro che restituisce un riferimento a una variabile membro della classe: è sicuro e non dovrebbe essere proibito dal compilatore. Quindi un chiamante che ha un'istanza automatica di quella classe, chiama quella funzione membro e restituisce il riferimento. Presto: riferimento penzolante. E sì, causerà problemi, @kriss: questo è il mio punto. Molte persone affermano che un vantaggio dei riferimenti rispetto ai puntatori è che i riferimenti sono sempre validi, ma non è così.
Ben Voigt,

4
@kriss: No, un riferimento a un oggetto con durata di memorizzazione automatica è molto diverso da un oggetto temporaneo. Ad ogni modo, stavo solo fornendo un contro-esempio alla tua affermazione che puoi ottenere un riferimento non valido dereferenziando un puntatore non valido. Christoph ha ragione: i riferimenti non sono più sicuri dei puntatori, un programma che utilizza esclusivamente riferimenti può comunque violare la sicurezza dei tipi.
Ben Voigt,

7
I riferimenti non sono un tipo di puntatore. Sono un nuovo nome per un oggetto esistente.
catphive del

18
@catphive: true se si passa dalla semantica del linguaggio, non true se si osserva effettivamente l'implementazione; C ++ è un linguaggio molto più "magico" di C, e se rimuovi la magia dai riferimenti, finisci con un puntatore
Christoph,

191

Se vuoi essere veramente pedante, c'è una cosa che puoi fare con un riferimento che non puoi fare con un puntatore: prolungare la durata di un oggetto temporaneo. In C ++ se si associa un riferimento const a un oggetto temporaneo, la durata di tale oggetto diventa la durata del riferimento.

std::string s1 = "123";
std::string s2 = "456";

std::string s3_copy = s1 + s2;
const std::string& s3_reference = s1 + s2;

In questo esempio s3_copy copia l'oggetto temporaneo che è il risultato della concatenazione. Considerando che s3_reference in sostanza diventa l'oggetto temporaneo. È davvero un riferimento a un oggetto temporaneo che ora ha la stessa durata del riferimento.

Se lo provi senza di constesso non dovrebbe compilare. Non puoi associare un riferimento non const a un oggetto temporaneo, né puoi prendere il suo indirizzo per quella materia.


5
ma qual è il caso d'uso per questo?
Ahmad Mushtaq,

20
Bene, s3_copy creerà un temporaneo e quindi lo costruirà in s3_copy mentre s3_reference utilizza direttamente il temporaneo. Quindi, per essere veramente pedanti, è necessario esaminare l'ottimizzazione del valore di ritorno in base al quale al compilatore è consentito eludere la costruzione della copia nel primo caso.
Matt Price,

6
@digitalSurgeon: la magia è abbastanza potente. La durata dell'oggetto viene estesa dal fatto dell'associazione const &e solo quando il riferimento esce dal campo di applicazione viene chiamato il distruttore del tipo di riferimento effettivo (rispetto al tipo di riferimento, che potrebbe essere una base). Dal momento che è un riferimento, non verrà effettuata alcuna suddivisione in mezzo.
David Rodríguez - dribeas,

9
Aggiornamento per C ++ 11: ultima frase dovrebbe leggere "Non si può legare un lvalue di riferimento non-const ad un temporaneo" perché si può associare un non-const rvalue riferimento ad una temporanea, e ha lo stesso comportamento di vita-estensione.
Oktalist,

4
@AhmadMushtaq: l'uso chiave di questo è classi derivate . Se non è coinvolta alcuna eredità, è possibile utilizzare anche la semantica di valore, che sarà economica o gratuita a causa della costruzione di RVO / move. Ma se hai Animal x = fast ? getHare() : getTortoise()allora xaffronterai il classico problema di taglio, mentre Animal& x = ...funzionerà correttamente.
Arthur Tacca,

128

Oltre allo zucchero sintattico, un riferimento è un constpuntatore ( non un puntatore a const). È necessario stabilire a cosa si riferisce quando si dichiara la variabile di riferimento e non è possibile modificarla in un secondo momento.

Aggiornamento: ora che ci penso ancora, c'è una differenza importante.

Il bersaglio di un puntatore const può essere sostituito prendendo il suo indirizzo e usando un cast const.

Il target di un riferimento non può essere sostituito in alcun modo a meno di UB.

Ciò dovrebbe consentire al compilatore di eseguire ulteriori ottimizzazioni su un riferimento.


8
Penso che questa sia di gran lunga la migliore risposta. Altri parlano di riferimenti e indicazioni come se fossero bestie diverse e quindi spiegano come differiscono nel comportamento. Non rende le cose più facili. Ho sempre capito i riferimenti come a T* constcon uno zucchero sintattico diverso (ciò accade per eliminare un sacco di * e & dal tuo codice).
Carlo Wood,

2
"Il bersaglio di un puntatore const può essere sostituito prendendo il suo indirizzo e usando un cast const." Ciò è un comportamento indefinito. Per ulteriori dettagli, consultare stackoverflow.com/questions/25209838/… .
dgnuff il

1
Cercare di cambiare il referente di un riferimento o il valore di un puntatore const (o qualsiasi scalare const) è uguaglianza illegale. Cosa puoi fare: rimuovere una qualifica const che è stata aggiunta dalla conversione implicita: int i; int const *pci = &i; /* implicit conv to const int* */ int *pi = const_cast<int*>(pci);va bene.
curiousguy,

1
La differenza qui è UB contro letteralmente impossibile. Non esiste sintassi in C ++ che ti consenta di modificare i punti di riferimento in.

Non impossibile, più difficile, puoi semplicemente accedere all'area di memoria del puntatore che sta modellando quel riferimento e modificarne il contenuto. Questo può certamente essere fatto.
Nicolas Bousquet,

126

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

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

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

Tecnicamente si tratta di un riferimento non valido , non di un riferimento null. C ++ non supporta riferimenti null come concetto, come si potrebbe trovare in altre lingue. Esistono anche altri tipi di riferimenti non validi. Qualsiasi riferimento non valido solleva lo spettro di comportamenti indefiniti , proprio come farebbe con un puntatore non valido.

L'errore effettivo si trova nel dereferenziamento del puntatore NULL, prima dell'assegnazione a un riferimento. Ma non sono a conoscenza di alcun compilatore che genererà errori in quella condizione - l'errore si propaga a un punto più lungo nel codice. Questo è ciò che rende questo problema così insidioso. Il più delle volte, se si dereferenzia un puntatore NULL, si blocca proprio in quel punto e non ci vuole molto debug per capirlo.

Il mio esempio sopra è breve e inventato. Ecco un esempio più reale.

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 è tramite 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 inesistente! Un riferimento valido non può mai essere NULL, quindi dal punto di vista del compilatore il confronto è sempre falso ed è libero di eliminare la ifclausola come codice morto: questa è l'essenza di un comportamento indefinito.

Il modo corretto per evitare problemi è evitare di dereferenziare un puntatore NULL per creare un riferimento. Ecco un modo automatizzato per ottenere questo risultato.

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 parte di qualcuno con migliori capacità di scrittura, vedere Riferimenti null di Jim Hyslop e Herb Sutter.

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


63
Il codice in questione contiene un comportamento indefinito. Tecnicamente, non puoi fare nulla con un puntatore null tranne impostarlo e confrontarlo. Una volta che il tuo programma invoca comportamenti indefiniti, può fare qualsiasi cosa, incluso sembrare funzionare correttamente fino a quando non stai dando una demo al grande capo.
KeithB,

9
mark ha un argomento valido. l'argomento che un puntatore potrebbe essere NULL e che quindi devi controllare non è neanche reale: se dici che una funzione richiede non NULL, il chiamante deve farlo. quindi se il chiamante non sta invocando un comportamento indefinito. proprio come ha fatto Mark con il riferimento negativo
Johannes Schaub -

13
La descrizione è errata Questo codice potrebbe o meno creare un riferimento NULL. Il suo comportamento è indefinito. Potrebbe creare un riferimento perfettamente valido. Potrebbe non riuscire a creare alcun riferimento.
David Schwartz,

7
@David Schwartz, se stavo parlando del modo in cui le cose dovevano funzionare secondo lo standard, avresti ragione. Ma non è di questo che sto parlando: sto parlando del comportamento effettivamente osservato con un compilatore molto popolare e dell'estrapolazione basata sulla mia conoscenza dei compilatori tipici e delle architetture CPU a ciò che probabilmente accadrà. Se ritieni che i riferimenti siano superiori ai puntatori perché sono più sicuri e non ritieni che i riferimenti possano essere cattivi, un giorno sarai sconcertato da un semplice problema proprio come me.
Mark Ransom,

6
Dereferenziare un puntatore nullo è sbagliato. Qualsiasi programma che lo fa, anche per inizializzare un riferimento, è sbagliato. Se si sta inizializzando un riferimento da un puntatore, verificare sempre che il puntatore sia valido. Anche se ciò riesce, l'oggetto sottostante può essere cancellato in qualsiasi momento lasciando il riferimento a riferirsi ad oggetto inesistente, giusto? Quello che stai dicendo è roba buona. Penso che il vero problema qui sia che il riferimento NON deve essere verificato per "nullità" quando ne vedi uno e il puntatore dovrebbe, come minimo, essere affermato.
t0rakka,

115

Hai dimenticato la parte più importante:

accesso membri con puntatori utilizza ->
accesso membri con usi riferimenti.

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


4
@Orion Edwards> accesso membri con usi puntatori ->> accesso membri con usi riferimenti. Questo non è vero al 100%. Puoi avere un riferimento a un puntatore. In questo caso si accede ai membri del puntatore non referenziato usando -> nodo Nodo {Nodo * successivo; }; Nodo * prima; // p è un riferimento a un puntatore void foo (Node * & p) {p-> next = first; } Nodo * bar = nuovo nodo; foo (bar); - OP: hai familiarità con i concetti di valori e valori?

3
I puntatori intelligenti hanno entrambi. (metodi sulla classe del puntatore intelligente) e -> (metodi sul tipo sottostante).
JBR Wilkinson,

1
@ user6105 L' istruzione Orion Edwards è in realtà vera al 100%. "accedi ai membri del [il] puntatore de-referenziato" Un puntatore non ha alcun membro. L'oggetto a cui fa riferimento il puntatore ha membri e l'accesso a questi è esattamente ciò che ->fornisce riferimenti ai puntatori, proprio come con il puntatore stesso.
Max Truxa,

1
perché è così .e ->ha qualcosa a che fare con vi ed emacs :)
artm

10
@artM - era uno scherzo, e probabilmente non ha senso per chi non parla inglese. Mie scuse. Spiegare se vi sia meglio di emacs è del tutto soggettivo. Alcune persone pensano che vi sia di gran lunga superiore, e altri pensano l'esatto contrario. Allo stesso modo, penso che usare .sia meglio dell'uso ->, ma proprio come vi vs emacs, è del tutto soggettivo e non puoi provare nulla
Orion Edwards,

74

I riferimenti sono molto simili ai puntatori, ma sono appositamente creati per essere utili per ottimizzare i compilatori.

  • I riferimenti sono progettati in modo tale che per il compilatore sia sostanzialmente più semplice tracciare quali alias di riferimento quali variabili. Due caratteristiche principali sono molto importanti: nessuna "aritmetica di riferimento" e nessuna riassegnazione di riferimenti. Ciò consente al compilatore di capire quali riferimenti alias quali variabili al momento della compilazione.
  • I riferimenti possono fare riferimento a variabili che non hanno indirizzi di memoria, come quelle che il compilatore sceglie di inserire nei registri. Se prendi 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 può rendersi conto che stiamo accedendo a uno [0] e un [1] piuttosto un mucchio. Amerebbe 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 effettuare tale ottimizzazione, deve dimostrare che nulla può cambiare l'array [1] durante la chiamata. Questo è piuttosto facile da fare. non è mai inferiore a 2, quindi l'array [i] non può mai fare riferimento all'array [1]. maybeModify () viene dato a0 come riferimento (aliasing array [0]). Poiché non esiste un'aritmetica "di riferimento", il compilatore deve solo dimostrare che forseModify non ottiene mai l'indirizzo di x, e ha dimostrato che nulla cambia l'array [1].

Deve anche dimostrare che non ci sono modi in cui una chiamata futura possa leggere / scrivere uno [0] mentre ne abbiamo una copia temporanea in a0. Questo è spesso banale da dimostrare, perché in molti casi è ovvio che il riferimento non viene mai archiviato 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 provare che forseModify 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 maybeModify per dimostrare che non scrive mai su & x + 1. Deve anche dimostrare che non salva mai un puntatore che può fare riferimento all'array [0], che è solo come difficile.

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

Naturalmente, a parte tali ottimizzazioni intelligenti, i compilatori trasformeranno effettivamente i riferimenti in puntatori quando necessario.

EDIT: Cinque anni dopo aver pubblicato questa risposta, ho trovato un'effettiva differenza tecnica in cui i riferimenti sono diversi rispetto a un modo diverso di guardare lo stesso concetto di indirizzamento. I riferimenti possono modificare la durata 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;
};

Gli oggetti normalmente temporanei come quello creato dalla chiamata a createF(5)vengono distrutti alla fine dell'espressione. Tuttavia, vincolando tale oggetto a un riferimento, refC ++ prolungherà la durata di vita di tale oggetto temporaneo fino a quando non refesce dall'ambito.


È vero, il corpo deve essere visibile. Tuttavia, determinare che maybeModifynon prende l'indirizzo di nulla a cui si xfa riferimento è sostanzialmente più semplice che provare che non si verifica un mucchio di aritmetica del puntatore.
Cort Ammon,

Credo che l'ottimizzatore faccia già che "non si verifica un gruppo di puntatori arithemetici" per una serie di altri motivi.
Ben Voigt,

"I riferimenti sono molto simili ai puntatori" - semanticamente, in contesti appropriati - ma in termini di codice generato, solo in alcune implementazioni e non attraverso alcuna definizione / requisito. So che lo hai sottolineato e non sono in disaccordo con nessuno dei tuoi post in termini pratici, ma abbiamo già troppi problemi con le persone che leggono troppo in descrizioni stenografiche come "i riferimenti sono come / di solito implementati come puntatori" .
underscore_d,

Ho la sensazione che qualcuno abbia erroneamente segnalato come obsoleto un commento sulla falsariga di void maybeModify(int& x) { 1[&x]++; }cui stanno discutendo gli altri commenti sopra
Ben Voigt,

69

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

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

Quando crei un riferimento, dici solo al compilatore 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 semplicemente aggiungendo un altro nome al contenuto della memoria che contiene il valore di a. Questa memoria ora ha due nomi ae b, e può essere indirizzata usando uno dei due 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 su cui copiare. La firma della funzione definisce gli spazi da creare e fornisce il nome da utilizzare per questi spazi. La dichiarazione di un parametro come riferimento indica semplicemente al compilatore di utilizzare lo spazio di memoria della variabile di input invece di allocare un nuovo spazio di memoria durante la chiamata del metodo. Può sembrare strano dire che la tua funzione manipolerà direttamente una variabile dichiarata nell'ambito di chiamata, ma ricorda che quando si esegue il codice compilato, non c'è più ambito; c'è solo una semplice memoria piatta e il tuo codice funzione potrebbe 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 puntatore nel codice sottostante. Ma negli esempi che ti ho dato, molto probabilmente non sarà implementato con un puntatore.


2
Un riferimento è un riferimento al valore l, non necessariamente a una variabile. Per questo motivo, è molto più vicino a un puntatore che a un vero alias (un costrutto in fase di compilazione). Esempi di espressioni a cui è possibile fare riferimento sono * p o addirittura * p ++

5
Bene, stavo solo indicando il fatto che un riferimento potrebbe non sempre spingere una nuova variabile nello stack come farà un nuovo puntatore.
Vincent Robert

1
@VincentRobert: funzionerà come un puntatore ... se la funzione è incorporata, sia il riferimento che il puntatore verranno ottimizzati. Se c'è una chiamata di funzione, l'indirizzo dell'oggetto dovrà essere passato alla funzione.
Ben Voigt,

1
int * p = NULL; int & r = * p; riferimento che punta a NULL; if (r) {} -> boOm;)
sree

2
Questa attenzione sulla fase di compilazione sembra buona, fino a quando non ricordi che i riferimenti possono essere passati in fase di esecuzione, a quel punto l'aliasing statico esce dalla finestra. (E poi, i riferimenti sono generalmente implementati come puntatori, ma lo standard non richiede questo metodo.)
underscore_d

45

Un riferimento non può mai essere NULL.


10
Vedi la risposta di Mark Ransom per un controesempio. Questo è il mito più spesso affermato sui riferimenti, ma è un mito. L'unica garanzia che hai secondo lo standard è che hai immediatamente UB quando hai un riferimento NULL. Ma questo è simile a dire "Questa macchina è sicura, non può mai scendere dalla strada. (Non ci assumiamo alcuna responsabilità per ciò che può accadere se lo si allontana comunque dalla strada. Potrebbe solo esplodere.)"
cmaster - Ripristina Monica

17
@cmaster: in un programma valido , un riferimento non può essere nullo. Ma un puntatore può. Questo non è un mito, questo è un dato di fatto.
user541686

8
@Mehrdad Sì, i programmi validi rimangono in viaggio. Ma non vi è alcun ostacolo al traffico per far rispettare il tuo programma. Grandi parti della strada sono in realtà segni mancanti. Quindi è estremamente facile scendere di strada di notte. Ed è cruciale per il debug di tali bug che sai che ciò può accadere: il riferimento null può propagarsi prima che si blocchi il tuo programma, proprio come può fare un puntatore null. E quando lo fai hai un codice come void Foo::bar() { virtual_baz(); }quello segfaults. Se non si è a conoscenza del fatto che i riferimenti potrebbero essere nulli, non è possibile risalire al valore zero fino alla sua origine.
cmaster - ripristina monica il

4
int * p = NULL; int & r = * p; riferimento che punta a NULL; if (r) {} -> boOm;) -
sree

10
@sree int &r=*p;è un comportamento indefinito. A quel punto, non c'è bisogno di una "punta di riferimento a NULL," si dispone di un programma che non può più essere motivato sulla affatto .
cdhowie,

35

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 si riferisce sempre a un oggetto: è un errore definire un riferimento senza inizializzarlo. Il comportamento dell'assegnazione è la seconda differenza importante: l'assegnazione a un riferimento modifica l'oggetto a cui è associato il riferimento; non associa 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 modifica il valore di pi, facendolo puntare a un oggetto diverso. Consideriamo ora un programma simile che assegna due riferimenti:

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

Questa assegnazione cambia ival, il valore a cui fa riferimento ri e non il riferimento stesso. Dopo l'assegnazione, i due riferimenti fanno ancora riferimento ai loro oggetti originali e anche il valore di tali oggetti è lo stesso.


"un riferimento si riferisce sempre a un oggetto" è completamente falso
Ben Voigt,

32

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

Al livello più alto, l'idea dei riferimenti è che sono "alias" trasparenti. Il tuo computer potrebbe usare un indirizzo per farli funzionare, ma non dovresti preoccupartene: dovresti pensarli come "solo un altro nome" per un oggetto esistente e la sintassi lo riflette. Sono più rigorosi dei puntatori, quindi il compilatore può avvisarti in modo più affidabile quando stai per creare un riferimento pendente, rispetto a quando stai per creare un puntatore pendente.

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


27

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. 

20

Non importa quanto spazio occupa poiché non puoi effettivamente vedere alcun effetto collaterale (senza eseguire il codice) di qualsiasi spazio occuperebbe.

D'altra parte, una delle principali differenze tra riferimenti e puntatori è che i temporali assegnati ai riferimenti const rimangono attivi fino a quando il riferimento const non esce dall'ambito.

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.


1
Non puoi prendere l'indirizzo di un riferimento, ma ciò non significa che non occupino spazio fisicamente. Tranne l'ottimizzazione, sicuramente possono farlo.
Razze di leggerezza in orbita

2
Nonostante ciò, "Un riferimento nello stack non occupa affatto spazio" è palesemente falso.
Corse di leggerezza in orbita

1
@Tomalak, beh, dipende anche dal compilatore. Ma sì, dire che è un po 'confuso. Suppongo che sarebbe meno confuso rimuoverlo.
MSN,

1
In ogni caso specifico può o non può. Quindi "non lo fa" poiché un'asserzione categorica è errata. È quello che sto dicendo. :) [Non ricordo cosa dice lo standard sul problema; le regole dei membri di riferimento possono impartire una regola generale di "riferimenti che possono occupare spazio", ma non ho la mia copia dello standard con me qui sulla spiaggia: D]
Lightness Races in Orbit

20

Questo si basa sul tutorial . Ciò che è scritto 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 per 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 tutte le esercitazioni 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 alias of a variable(diverso con typedef, che è alias of a type) Tom. Va anche bene dimenticare che la terminologia di tale affermazione è quella di creare un riferimento di Tom.


1
E se una classe ha una variabile di riferimento, dovrebbe essere inizializzata con un nullptr o un oggetto valido nell'elenco di inizializzazione.
Misgevolution

1
La formulazione in questa risposta è troppo confusa per essere di grande utilità. Inoltre, @Misgevolution, stai seriamente raccomandando ai lettori di inizializzare un riferimento con un nullptr? Hai letto qualche altra parte di questa discussione o ...?
underscore_d,

1
Mio male, scusa per quella cosa stupida che ho detto. A quel punto dovevo essere stato privato del sonno. 'inizializza con nullptr' è totalmente sbagliato.
Misgevolution,

19

Un riferimento non è un altro nome dato a qualche memoria. È un puntatore immutabile che viene automaticamente de-referenziato sull'uso. Fondamentalmente si riduce a:

int& j = i;

Lo diventa internamente

int* const j = &i;

13
Questo non è ciò che dice lo standard C ++ e non è necessario che il compilatore implementi i riferimenti nel modo descritto dalla risposta.
jogojapan,

@jogojapan: Qualsiasi modo valido per un compilatore C ++ per implementare un riferimento è anche un modo valido per implementare un constpuntatore. Tale flessibilità non dimostra che esiste una differenza tra un riferimento e un puntatore.
Ben Voigt,

2
@BenVoigt Può essere vero che qualsiasi implementazione valida dell'una sia anche un'implementazione valida dell'altra, ma ciò non segue in modo ovvio dalle definizioni di questi due concetti. Una buona risposta sarebbe partita dalle definizioni e dimostrò perché l'affermazione che i due fossero alla fine la stessa è vera. Questa risposta sembra essere una sorta di commento su alcune delle altre risposte.
jogojapan,

Un riferimento è un altro nome dato a un oggetto. Al compilatore è consentito eseguire qualsiasi tipo di implementazione, purché non sia possibile stabilire la differenza, questa è nota come regola "as-if". La parte importante qui è che non puoi dire la differenza. Se riesci a scoprire che un puntatore non ha memoria, il compilatore è in errore. Se riesci a scoprire che un riferimento non ha memoria, il compilatore è ancora conforme.
sp2danny,

18

La risposta diretta

Qual è un riferimento in C ++? Qualche istanza specifica di tipo che non è un tipo di oggetto .

Che cos'è un puntatore in C ++? Qualche istanza specifica di tipo che è un tipo di oggetto .

Dalla definizione ISO C ++ del tipo di oggetto :

Un oggetto tipo è un (possibilmente cv Qualificato) tipo che non è un tipo di funzione, non un tipo di riferimento, e non cv vuoto.

Potrebbe essere importante sapere che il tipo di oggetto è una categoria di livello superiore dell'universo del tipo in C ++. Anche il riferimento è una categoria di livello superiore. Ma il puntatore non lo è.

Puntatori e riferimenti sono citati insieme nel contesto del tipo composto . Ciò è sostanzialmente dovuto alla natura della sintassi del dichiaratore ereditata da (e estesa) C, che non ha riferimenti. (Inoltre, ci sono più di un tipo di dichiaratore di riferimenti dal C ++ 11, mentre i puntatori sono ancora "unityped": &+ &&vs *). Così la stesura di una specifica lingua "estensione" con stile simile di C in questo contesto è un po 'ragionevole . (Io ancora sostengono che la sintassi del dichiaratori rifiuti dell'espressività sintattico un sacco , rende gli utenti umani e implementazioni frustrante. Quindi, tutti loro non sono qualificati per essere incorporatoin un nuovo design del linguaggio. Questo è un argomento completamente diverso sul design PL, però.)

Altrimenti, è insignificante che i puntatori possano essere qualificati come tipi specifici di tipi con riferimenti insieme. Condividono semplicemente troppo poche proprietà comuni oltre alla somiglianza di sintassi, quindi non è necessario metterle insieme nella maggior parte dei casi.

Nota le affermazioni sopra menzionano solo i "puntatori" e i "riferimenti" come tipi. Ci sono alcune domande interessanti sulle loro istanze (come le variabili). Ci sono anche troppe idee sbagliate.

Le differenze delle categorie di livello superiore possono già rivelare molte differenze concrete non direttamente legate ai puntatori:

  • I tipi di oggetto possono avere cvqualificatori di livello superiore . I riferimenti non possono.
  • La variabile dei tipi di oggetto occupa l'archiviazione secondo la semantica della macchina astratta . Il riferimento non è necessario per occupare spazio di archiviazione (per i dettagli vedere la sezione sulle idee sbagliate di seguito).
  • ...

Alcune altre regole speciali sui riferimenti:

  • I dichiaranti composti sono più restrittivi sui riferimenti.
  • I riferimenti possono crollare .
    • Regole speciali sui &&parametri (come i "riferimenti di inoltro") basate sulla compressione dei riferimenti durante la deduzione dei parametri del modello consentono "inoltro perfetto" dei parametri.
  • I riferimenti hanno regole speciali nell'inizializzazione. La durata della variabile dichiarata come tipo di riferimento può essere diversa dagli oggetti ordinari tramite l'estensione.
    • A proposito, alcuni altri contesti come l'inizializzazione che std::initializer_listseguono seguono alcune regole simili di estensione della durata di riferimento. È un'altra lattina di vermi.
  • ...

Le idee sbagliate

Zucchero sintattico

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

Tecnicamente, questo è chiaramente sbagliato. I riferimenti non sono zucchero sintattico di altre funzionalità in C ++, poiché non possono essere esattamente sostituiti da altre funzionalità senza differenze semantiche.

(Allo stesso modo, le espressioni lambda non sono zucchero sintattico di altre caratteristiche in C ++ perché non possono essere simulate con precisione con proprietà "non specificate" come l'ordine di dichiarazione delle variabili catturate , il che può essere importante perché l'ordine di inizializzazione di tali variabili può essere significativo.)

Il C ++ ha solo alcuni tipi di zuccheri sintattici in questo senso stretto. Un'istanza è (ereditata da C) l'operatore incorporato (non sovraccarico) [], che è definito esattamente con le stesse proprietà semantiche di specifiche forme di combinazione rispetto all'operatore incorporato unario *e binario+ .

Conservazione

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

L'affermazione sopra è semplicemente sbagliata. Per evitare tali idee sbagliate, guarda invece le regole ISO C ++:

Da [intro.object] / 1 :

... Un oggetto occupa una regione di stoccaggio nel suo periodo di costruzione, per tutta la sua vita e nel suo periodo di distruzione. ...

Da [dcl.ref] / 4 :

Non è specificato se un riferimento richiede o meno la memorizzazione.

Nota che queste sono proprietà semantiche .

Pragmatica

Anche se i puntatori non sono abbastanza qualificati da essere uniti a riferimenti nel senso del design del linguaggio, ci sono ancora alcuni argomenti che rendono discutibile la scelta tra di loro in alcuni altri contesti, ad esempio quando si fanno scelte sui tipi di parametri.

Ma questa non è tutta la storia. Voglio dire, ci sono più cose che puntatori contro riferimenti che devi considerare.

Se non devi attenersi a tali scelte troppo specifiche, nella maggior parte dei casi la risposta è breve: non hai la necessità di utilizzare i puntatori, quindi non lo fai . I puntatori di solito sono abbastanza male perché implicano troppe cose che non ti aspetti e faranno affidamento su troppe ipotesi implicite che minano la manutenibilità e (anche) la portabilità del codice. Affidarsi inutilmente ai puntatori è sicuramente uno stile negativo e dovrebbe essere evitato nel senso del C ++ moderno. Riconsidera il tuo scopo e scoprirai finalmente che il puntatore è la caratteristica degli ultimi tipi nella maggior parte dei casi.

  • A volte le regole del linguaggio richiedono esplicitamente tipi specifici da utilizzare. Se si desidera utilizzare queste funzionalità, attenersi alle regole.
    • I costruttori di copie richiedono tipi specifici di cv - &tipo di riferimento come primo tipo di parametro. (E di solito dovrebbe essere constqualificato.)
    • I costruttori di spostamento richiedono tipi specifici di cv - &&tipo di riferimento come primo tipo di parametro. (E di solito non dovrebbero esserci qualificatori.)
    • I sovraccarichi specifici degli operatori richiedono tipi di riferimento o non di riferimento. Per esempio:
      • Sovraccaricato operator=come funzioni membro speciale richiede tipi di riferimento simili al primo parametro dei costruttori di copia / spostamento.
      • Postfix ++richiede un manichino int.
      • ...
  • Se si conosce il valore per passaggio (ovvero l'utilizzo di tipi non di riferimento) è sufficiente, utilizzarlo direttamente, in particolare quando si utilizza un'implementazione che supporta l'eliminazione della copia obbligatoria C ++ 17. ( Attenzione : tuttavia, ragionare esaurientemente sulla necessità può essere molto complicato .)
  • Se si desidera utilizzare alcuni handle con proprietà, utilizzare i puntatori intelligenti come unique_ptre shared_ptr(o anche con quelli homebrew da soli se si richiede che siano opachi ), anziché i puntatori non elaborati.
  • Se si stanno eseguendo alcune iterazioni su un intervallo, utilizzare iteratori (o alcuni intervalli che non sono ancora forniti dalla libreria standard), piuttosto che puntatori non elaborati a meno che non si sia convinti che i puntatori non elaborati faranno meglio (ad esempio per meno dipendenze delle intestazioni) in modo molto specifico casi.
  • Se sai che il valore per passaggio è sufficiente e desideri una semantica esplicita nullable, usa i wrapper like std::optional, piuttosto che i puntatori non elaborati.
  • Se sai che il pass-by-value non è l'ideale per i motivi sopra riportati e non vuoi una semantica nullable, usa {lvalue, rvalue, forwarding} -references.
  • Anche quando vuoi la semantica come un puntatore tradizionale, ci sono spesso qualcosa di più appropriato, come observer_ptrin Library Fundamental TS.

Le uniche eccezioni non possono essere risolte nella lingua corrente:

  • Quando si implementano i puntatori intelligenti sopra, potrebbe essere necessario gestire i puntatori non elaborati.
  • Routine specifiche di interoperabilità linguistiche richiedono puntatori, come operator new. (Tuttavia, cv - void*è ancora abbastanza diverso e più sicuro rispetto ai normali puntatori di oggetti perché esclude aritmetiche di puntatori impreviste a meno che non si faccia affidamento su un'estensione non conforme su void*come GNU.)
  • I puntatori a funzione possono essere convertiti da espressioni lambda senza acquisizioni, mentre i riferimenti a funzioni non possono essere convertiti. In questi casi è necessario utilizzare i puntatori a funzione nel codice non generico, anche se non si desidera deliberatamente valori nullable.

Quindi, in pratica, la risposta è così ovvia: in caso di dubbio, evitare i puntatori . Devi usare i puntatori solo quando ci sono ragioni molto esplicite che nient'altro è più appropriato. Tranne alcuni casi eccezionali sopra menzionati, tali scelte non sono quasi sempre esclusivamente C ++ specifiche (ma probabilmente specifiche della lingua). Tali casi possono essere:

  • Devi servire per API (C) vecchio stile.
  • Devi soddisfare i requisiti ABI di specifiche implementazioni C ++.
  • È necessario interagire in fase di runtime con implementazioni di lingue diverse (inclusi vari assembly, runtime di lingua e FFI di alcune lingue client di alto livello) in base a ipotesi di implementazioni specifiche.
  • Devi migliorare l'efficienza della traduzione (compilazione e collegamento) in alcuni casi estremi.
  • In alcuni casi estremi devi evitare il gonfiamento dei simboli.

Avvertenze sulla neutralità della lingua

Se vieni a vedere la domanda tramite alcuni risultati di ricerca di Google (non specifici per C ++) , è molto probabile che questo sia il posto sbagliato.

I riferimenti in C ++ è abbastanza "strana", in quanto non è essenzialmente di prima classe: essi saranno trattati come gli oggetti o le funzioni da cui quindi non hanno la possibilità di sostenere alcune operazioni di prima classe come essere l'operando sinistro del operatore di accesso membro indipendentemente dal tipo di oggetto riferito. Altre lingue possono o meno avere restrizioni simili sui loro riferimenti.

I riferimenti in C ++ probabilmente non manterranno il significato in diverse lingue. Ad esempio, i riferimenti in generale non implicano proprietà non nulle su valori come quelli in C ++, quindi tali ipotesi potrebbero non funzionare in alcuni altri linguaggi (e troverete controesempi abbastanza facilmente, ad esempio Java, C #, ...).

Possono esserci ancora alcune proprietà comuni tra i riferimenti in diversi linguaggi di programmazione in generale, ma lasciamolo per alcune altre domande in SO.

(Una nota a margine: la domanda potrebbe essere significativa prima di qualsiasi linguaggio "C-like", come ALGOL 68 vs. PL / I. )


17

Un riferimento a un puntatore è possibile in C ++, ma il contrario non è possibile significa che un puntatore a un riferimento non è possibile. Un riferimento a un puntatore fornisce una sintassi più pulita per modificare il puntatore. Guarda questo esempio:

#include<iostream>
using namespace std;

void swap(char * &str1, char * &str2)
{
  char *temp = str1;
  str1 = str2;
  str2 = temp;
}

int main()
{
  char *str1 = "Hi";
  char *str2 = "Hello";
  swap(str1, str2);
  cout<<"str1 is "<<str1<<endl;
  cout<<"str2 is "<<str2<<endl;
  return 0;
}

E considera la versione C del programma sopra. In C devi usare il puntatore al puntatore (indiretta multipla), e ciò crea confusione e il programma può sembrare complicato.

#include<stdio.h>
/* Swaps strings by swapping pointers */
void swap1(char **str1_ptr, char **str2_ptr)
{
  char *temp = *str1_ptr;
  *str1_ptr = *str2_ptr;
  *str2_ptr = temp;
}

int main()
{
  char *str1 = "Hi";
  char *str2 = "Hello";
  swap1(&str1, &str2);
  printf("str1 is %s, str2 is %s", str1, str2);
  return 0;
}

Per ulteriori informazioni sul riferimento al puntatore, visitare quanto segue:

Come ho detto, un puntatore a un riferimento non è possibile. Prova il seguente programma:

#include <iostream>
using namespace std;

int main()
{
   int x = 10;
   int *ptr = &x;
   int &*ptr1 = ptr;
}

16

Uso i riferimenti a meno che non sia necessario uno di questi:

  • I puntatori null possono essere usati come valore sentinella, spesso un modo economico per evitare il sovraccarico della funzione o l'uso di un bool.

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


5
Puoi scrivere &r + offsetdove è rstato dichiarato come riferimento
MM

15

Esiste una differenza fondamentale tra puntatori e riferimenti che non ho visto menzionato da nessuno: i riferimenti abilitano la semantica pass-by-reference negli argomenti delle funzioni. I puntatori, sebbene all'inizio non siano visibili: non forniscono solo semantica pass-by-value. Questo è stato molto ben descritto in questo articolo .

Saluti, & rzej


1
Riferimenti e puntatori sono entrambi handle. Entrambi forniscono la semantica in cui l' oggetto viene passato per riferimento, ma l' handle viene copiato. Nessuna differenza. (Esistono anche altri modi per avere le maniglie, come una chiave per la ricerca in un dizionario)
Ben Voigt

Pensavo anche così. Ma vedi l'articolo collegato che descrive perché non è così.
Andrzej,

2
@Andrzj: Questa è solo una versione molto lunga della singola frase nel mio commento: l'handle viene copiato.
Ben Voigt,

Ho bisogno di maggiori spiegazioni su questo "L'handle è stato copiato". Comprendo qualche idea di base ma penso fisicamente che il riferimento e il puntatore puntino entrambi la posizione di memoria della variabile. È come se l'alias memorizzi la variabile value e la aggiorni mentre il valore della variabile è change o qualcos'altro? Sono alle prime armi, e per favore non contrassegnarlo come una domanda stupida.
Asim,

1
@Andrzej False. In entrambi i casi, si sta verificando il valore per passaggio. Il riferimento viene passato per valore e il puntatore viene passato per valore. Dire altrimenti confonde i principianti.
Miles Rout,

14

A rischio di aggiungere confusione, voglio aggiungere qualche input, sono sicuro che dipende principalmente da come il compilatore implementa i riferimenti, ma nel caso di gcc l'idea che un riferimento possa puntare solo a una variabile nello stack non è effettivamente corretto, prendi questo per 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 produce 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 nell'heap! Ora, se vuoi davvero diventare strano, questo 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 produce questo:

THIS IS A STRING

Quindi un riferimento È un puntatore sotto il cofano, entrambi stanno solo memorizzando un indirizzo di memoria, dove l'indirizzo a cui punta è irrilevante, cosa pensi che accadrebbe se avessi chiamato std :: cout << str_ref; DOPO aver chiamato delete & str_ref? Bene, ovviamente si compila bene, ma provoca un errore di segmentazione in fase di runtime perché non punta più a una variabile valida, essenzialmente abbiamo un riferimento rotto che esiste ancora (fino a quando non cade dall'ambito), ma è inutile.

In altre parole, un riferimento non è altro che un puntatore che ha sottratto la meccanica del puntatore, rendendolo più sicuro e più facile da usare (nessuna matematica accidentale del puntatore, nessuna confusione tra '.' E '->', ecc.), Supponendoti 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 cofano, perché un riferimento deve fare riferimento a una variabile specifica in un indirizzo di memoria specifico per funzionare come previsto, quindi 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 quelli, esempi che dimostrano cosa sia un riferimento, non vorresti mai usare un riferimento in quei modi! Per un corretto utilizzo di un riferimento ci sono già molte risposte qui che colpiscono l'unghia sulla testa


14

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

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

Non posso dire di essere davvero contento di questa particolare differenza. Preferirei di gran lunga che sarebbe consentito con il riferimento 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.


13

Inoltre, un riferimento che è un parametro di una funzione in linea 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 inseriscono la versione del puntatore forzano effettivamente una scrittura in memoria (stiamo prendendo l'indirizzo in modo esplicito). Tuttavia, lasceranno il riferimento in un registro che è più ottimale.

Naturalmente, per le funzioni che non sono evidenziate, il puntatore e il riferimento generano lo stesso codice ed è sempre meglio passare valori intrinseci per valore che per riferimento se non sono modificati e restituiti dalla funzione.


11

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 predefinito utilizza il riferimento "bind const a un aspetto temporaneo" dei riferimenti.


11

Questo programma potrebbe aiutare a comprendere la risposta alla domanda. Questo è un semplice programma di un 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


11

Sento che c'è ancora un altro punto che non è stato trattato qui.

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

Sebbene ciò possa apparire superficiale, credo che questa proprietà sia cruciale per una serie di funzionalità C ++, ad esempio:

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

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

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

  • Sovraccarichi dell'operatore . Con i riferimenti è possibile introdurre un riferimento indiretto a una chiamata dell'operatore, ad esempio operator+(const T& a, const T& b)mantenendo la stessa notazione infissa. Questo funziona anche per le normali funzioni sovraccaricate.

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


" cast implicito " un cast è un costrutto di sintassi, esiste nella grammatica; un cast è sempre esplicito
curiousguy il

9

Esiste una differenza non tecnica molto importante tra puntatori e riferimenti: un argomento passato a una funzione tramite puntatore è molto più visibile di un argomento passato a una funzione mediante 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)
}

Di nuovo in C, una chiamata simile fn(x)può essere passata solo per valore, quindi sicuramente non può essere modificata x; per modificare un argomento è necessario passare un puntatore fn(&x). Quindi se una discussione non fosse preceduta da una &sapevi che non sarebbe stata modificata. (Il contrario, &significa modificato, non era vero perché a volte dovresti passare grandi strutture di sola lettura tramite constpuntatore.)

Alcuni sostengono che questa sia una caratteristica così utile durante la lettura del codice, che i parametri del puntatore dovrebbero sempre essere usati per parametri modificabili piuttosto che 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 Google in stile C ++ ne sono un esempio.


8

Forse alcune metafore aiuteranno; Nel contesto dello spazio 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.

6

Differenza tra puntatore e riferimento

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

int* p = 0;

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

Infatti per farlo correttamente, dobbiamo aver dichiarato e definito un oggetto all'inizio, 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 nulla di simile può essere fatto in caso di riferimento che deve fare sempre riferimento a variabile o oggetto. Tuttavia, tale uso di un puntatore è rischioso, quindi generalmente controlliamo se il puntatore sta effettivamente puntando a qualcosa o no. Nel caso di un riferimento non è necessario tale controllo, 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, tuttavia 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 "="

1
Possiamo ancora avere const int& i = 0.
Revolver_Ocelot,

1
In questo caso il riferimento verrà usato solo in lettura non possiamo modificare questo riferimento const anche usando "const_cast" perché "const_cast" accetta solo puntatore e non riferimento.
dhokar.w,

1
const_cast funziona abbastanza bene con i riferimenti: coliru.stacked-crooked.com/a/eebb454ab2cfd570
Revolver_Ocelot

1
stai facendo un cast per fare riferimento a non lanciare un riferimento prova questo; const int & i =; const_cast <int> (i); cerco di eliminare la costanza del riferimento per rendere possibile la scrittura e l'assegnazione di nuovo valore al riferimento, ma ciò non è possibile. per favore concentrati !!
dhokar.w,

5

La differenza è che la variabile del puntatore non costante (da non confondere con un puntatore alla costante) può essere cambiata in qualche momento durante l'esecuzione del programma, richiede l'uso della semantica del puntatore (&, *) operatori, mentre i riferimenti possono essere impostati al momento dell'inizializzazione solo (è per questo che puoi impostarli solo nella lista di inizializzatori del costruttore, ma non in qualche altro modo) e usare il valore ordinario accedendo alla semantica. Fondamentalmente sono stati introdotti riferimenti per consentire il supporto agli operatori di sovraccarico, come avevo letto in un libro molto vecchio. Come qualcuno ha affermato in questo thread - il puntatore può essere impostato su 0 o qualunque valore tu voglia. 0 (NULL, nullptr) significa che il puntatore è inizializzato con nulla. Dereference null pointer è un errore. Ma in realtà il puntatore può contenere un valore che non punta a una posizione di memoria corretta. I riferimenti a loro volta tentano di non consentire a un utente di inizializzare un riferimento a qualcosa a cui non è possibile fare riferimento a causa del fatto che gli si fornisce sempre un valore di tipo corretto. Sebbene ci siano molti modi per inizializzare la variabile di riferimento in una posizione di memoria errata, è meglio non scavare così in profondità nei dettagli. A livello di macchina, sia il puntatore che il riferimento funzionano in modo uniforme - tramite i puntatori. Diciamo che nei riferimenti essenziali sono lo zucchero sintattico. i riferimenti di valore sono diversi da questo - sono naturalmente oggetti stack / heap. Sebbene ci siano molti modi per inizializzare la variabile di riferimento in una posizione di memoria errata, è meglio non scavare così in profondità nei dettagli. A livello di macchina, sia il puntatore che il riferimento funzionano in modo uniforme - tramite i puntatori. Diciamo che nei riferimenti essenziali sono lo zucchero sintattico. i riferimenti di valore sono diversi da questo - sono naturalmente oggetti stack / heap. Sebbene ci siano molti modi per inizializzare la variabile di riferimento in una posizione di memoria errata, è meglio non scavare così in profondità nei dettagli. A livello di macchina, sia il puntatore che il riferimento funzionano in modo uniforme - tramite i puntatori. Diciamo che nei riferimenti essenziali sono lo zucchero sintattico. i riferimenti di valore sono diversi da questo - sono naturalmente oggetti stack / heap.


4

in parole semplici, possiamo dire che un riferimento è un nome alternativo per una variabile mentre, un puntatore è una variabile che contiene l'indirizzo di un'altra variabile. per esempio

int a = 20;
int &r = a;
r = 40;  /* now the value of a is changed to 40 */

int b =20;
int *ptr;
ptr = &b;  /*assigns address of b to ptr not the value */
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.