Frage Was sind die Unterschiede zwischen einer Zeigervariablen und einer Referenzvariablen in C ++?


Ich weiß, Referenzen sind syntaktischer Zucker, so dass Code einfacher zu lesen und zu schreiben ist.

Aber was sind die Unterschiede?


Zusammenfassung aus Antworten und Links unten:

  1. Ein Zeiger kann beliebig oft neu zugewiesen werden, während eine Referenz nach dem Binden nicht neu zugewiesen werden kann.
  2. Zeiger können nirgends zeigen (NULL), während eine Referenz immer auf ein Objekt verweist.
  3. Sie können die Adresse einer Referenz nicht wie mit Zeigern verwenden.
  4. Es gibt keine "Referenzarithmetik" (aber Sie können die Adresse eines Objekts nehmen, auf das eine Referenz zeigt, und die Zeigerarithmetik auf sie anwenden, wie in &obj + 5).

Um ein Missverständnis zu klären:

Der C ++ - Standard ist sehr vorsichtig, um nicht zu diktieren, wie ein Compiler aussehen könnte   Referenzen implementieren, aber jeder C ++ - Compiler implementiert   Referenzen als Zeiger. Das heißt, eine Erklärung wie:

int &ri = i;

wenn es nicht komplett weg optimiert ist, reserviert die gleiche Menge an Speicherplatz   als Zeiger, und platziert die Adresse   von i in diesen Speicher.

Ein Zeiger und eine Referenz verwenden also dieselbe Speichermenge.

Generell,

  • Verwenden Sie Referenzen in Funktionsparametern und Rückgabetypen, um nützliche und selbstdokumentierende Schnittstellen bereitzustellen.
  • Verwenden Sie Zeiger zum Implementieren von Algorithmen und Datenstrukturen.

Interessantes lesen:


2617
2017-09-11 20:03


Ursprung


Antworten:


  1. Ein Zeiger kann neu zugewiesen werden:

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

    Eine Referenz kann und muss bei der Initialisierung nicht vergeben werden:

    int x = 5;
    int y = 6;
    int &r = x;
    
  2. Ein Zeiger hat seine eigene Speicheradresse und Größe auf dem Stapel (4 Bytes auf x86), während ein Verweis die gleiche Speicheradresse (mit der ursprünglichen Variablen) teilt, aber auch etwas Platz auf dem Stapel einnimmt. Da eine Referenz dieselbe Adresse hat wie die ursprüngliche Variable selbst, ist es sicher, sich eine Referenz als einen anderen Namen für dieselbe Variable vorzustellen. Hinweis: Worauf ein Zeiger zeigt, kann sich auf dem Stapel oder Heap befinden. Dito eine Referenz. Meine Behauptung in dieser Aussage ist nicht, dass ein Zeiger auf den Stapel zeigen muss. Ein Zeiger ist nur eine Variable, die eine Speicheradresse enthält. Diese Variable befindet sich auf dem Stapel. Da eine Referenz einen eigenen Platz auf dem Stapel hat und die Adresse dieselbe ist wie die Variable, auf die sie verweist. Mehr dazu Stapel gegen Heap. Dies bedeutet, dass es eine reale Adresse einer Referenz gibt, die der Compiler Ihnen nicht mitteilen wird.

    int x = 0;
    int &r = x;
    int *p = &x;
    int *p2 = &r;
    assert(p == p2);
    
  3. Sie können Zeiger auf Zeiger auf Zeiger haben, die zusätzliche Indirektionsgrade bieten. Während Referenzen nur eine Ebene der Umleitung bieten.

    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. Zeiger kann zugewiesen werden nullptr direkt, während Verweis nicht kann. Wenn Sie sich stark genug bemühen, und Sie wissen, wie, können Sie die Adresse einer Referenz machen nullptr. Wenn Sie sich intensiv genug bemühen, können Sie auch einen Verweis auf einen Zeiger haben, der dann enthalten sein kann 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. Zeiger können über ein Array iterieren, das Sie verwenden können ++um zum nächsten Element zu gehen, auf das ein Zeiger zeigt, und + 4 um zum fünften Element zu gehen. Dies ist unabhängig von der Größe des Objekts, auf das der Zeiger zeigt.

  6. Ein Zeiger muss mit dereferenziert werden * um auf den Speicherort zuzugreifen, auf den es zeigt, während eine Referenz direkt verwendet werden kann. Ein Zeiger auf eine Klasse / Struktur verwendet -> um auf seine Mitglieder zuzugreifen, während eine Referenz eine verwendet ..

  7. Ein Zeiger ist eine Variable, die eine Speicheradresse enthält. Unabhängig davon, wie eine Referenz implementiert wird, hat eine Referenz dieselbe Speicheradresse wie das Objekt, auf das sie verweist.

  8. Verweise können nicht in ein Array eingefügt werden, wohingegen Zeiger angegeben werden können (Erwähnung durch Benutzer @litb)

  9. Const-Referenzen können an Provisorien gebunden sein. Zeiger können nicht (nicht ohne Umweg):

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

    Das macht const& sicherer für die Verwendung in Argumentlisten und so weiter.


1369
2018-02-27 21:26



Was ist eine C ++ Referenz (für C-Programmierer)

EIN Referenz kann als gedacht werden konstanter Zeiger (nicht zu verwechseln mit einem Zeiger auf einen konstanten Wert!) mit automatischer Indirection, dh der Compiler wird die * Betreiber für Sie.

Alle Verweise müssen mit einem Wert ungleich Null initialisiert werden, andernfalls schlägt die Kompilierung fehl. Es ist auch nicht möglich, die Adresse eines Verweises zu erhalten - der Adressoperator gibt stattdessen die Adresse des referenzierten Wertes zurück - und es ist auch nicht möglich, Referenzen arithmetisch zu machen.

C-Programmierer mögen C ++ - Referenzen nicht mögen, da es nicht mehr offensichtlich ist, wenn eine Indirection stattfindet oder wenn ein Argument durch einen Wert oder durch einen Zeiger übergeben wird, ohne auf Funktionssignaturen zu schauen.

C ++ - Programmierer mögen es nicht, Zeiger zu verwenden, da sie als unsicher betrachtet werden - obwohl Verweise nicht wirklich sicherer sind als konstante Zeiger, außer in den einfachsten Fällen - nicht den Komfort einer automatischen Indirektion haben und eine andere semantische Bedeutung haben.

Betrachten Sie die folgende Aussage aus der C ++ - FAQ:

Auch wenn eine Referenz oft über eine Adresse in der   zugrunde liegende Assemblersprache, bitte tun nicht denke an eine Referenz als   lustig aussehender Zeiger auf ein Objekt. Eine Referenz ist das Objekt. Es ist   kein Zeiger auf das Objekt, noch eine Kopie des Objekts. Es ist das   Objekt.

Aber wenn eine Referenz Ja wirklich war das Objekt, wie konnte es baumelnde Referenzen geben? In nicht verwalteten Sprachen ist es unmöglich, dass Referenzen "sicherer" sind als Zeiger - es gibt im Allgemeinen keine Möglichkeit, Werte über Bereichsgrenzen hinweg zuverlässig zu aliasieren!

Warum finde ich C ++ Referenzen nützlich?

Von einem C-Hintergrund her kommend, mögen C ++ - Verweise wie ein etwas albernes Konzept aussehen, aber man sollte sie immer noch anstelle von Zeigern verwenden, wo es möglich ist: Automatische Indirektion ist praktisch, und Referenzen werden besonders nützlich beim Umgang mit RAII - Aber nicht wegen eines wahrgenommenen Sicherheitsvorteils, sondern weil sie das Schreiben von idiomatischem Code weniger unangenehm machen.

RAII ist eines der zentralen Konzepte von C ++, interagiert aber nicht-trivial mit der Kopiersemantik. Wenn Sie Objekte per Referenz übergeben, werden diese Probleme vermieden, da kein Kopieren erforderlich ist. Wenn Verweise in der Sprache nicht vorhanden wären, müssten Sie stattdessen Zeiger verwenden, die umständlicher zu verwenden sind, wodurch das Sprachdesign-Prinzip verletzt wird, dass die Best-Practice-Lösung einfacher als die Alternativen sein sollte.


301
2017-09-11 21:43



Wenn Sie wirklich pedantisch sein wollen, gibt es eine Sache, die Sie mit einer Referenz machen können, die Sie mit einem Zeiger nicht machen können: Erweitern Sie die Lebensdauer eines temporären Objekts. Wenn Sie in C ++ eine Konstante const an ein temporäres Objekt binden, wird die Lebensdauer dieses Objekts zur Lebensdauer der Referenz.

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

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

In diesem Beispiel kopiert s3_copy das temporäre Objekt, das ein Ergebnis der Verkettung ist. Während s3_reference im Wesentlichen zum temporären Objekt wird. Es ist wirklich ein Verweis auf ein temporäres Objekt, das jetzt die gleiche Lebensdauer wie die Referenz hat.

Wenn du das ohne das versuchst const Es sollte nicht kompiliert werden. Sie können eine nicht-konstante Referenz nicht an ein temporäres Objekt binden, noch können Sie die Adresse für diese Angelegenheit nehmen.


151
2017-09-11 21:06



Entgegen der landläufigen Meinung ist es möglich, eine Referenz zu haben, die NULL ist.

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

Zugegeben, es ist viel schwieriger, mit einer Referenz zu tun - aber wenn Sie es schaffen, werden Sie Ihre Haare reißen, um es zu finden. Referenzen sind nicht von Natur aus sicher in C ++!

Technisch ist das ein ungültige Referenz, keine Nullreferenz. C ++ unterstützt keine Null-Referenzen als Konzept, wie Sie es in anderen Sprachen finden. Es gibt auch andere Arten von ungültigen Referenzen. Irgendein Ungültige Referenz hebt das Gespenst von undefiniertes Verhalten, genauso wie die Verwendung eines ungültigen Zeigers.

Der eigentliche Fehler liegt in der Dereferenzierung des NULL-Zeigers vor der Zuweisung zu einer Referenz. Aber mir sind keine Compiler bekannt, die unter dieser Bedingung Fehler erzeugen - der Fehler breitet sich im Code weiter aus. Das macht dieses Problem so heimtückisch. Die meiste Zeit, wenn Sie einen NULL-Zeiger dereferenzieren, stürzen Sie direkt auf diesen Punkt und es braucht nicht viel Debugging, um es herauszufinden.

Mein Beispiel oben ist kurz und erfunden. Hier ist ein Beispiel aus der Praxis.

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);

Ich möchte wiederholen, dass der einzige Weg, um eine NULL-Referenz zu erhalten, durch missgebildeten Code ist, und sobald Sie es haben, erhalten Sie undefined Verhalten. Es noch nie macht Sinn, nach einer Nullreferenz zu suchen; zum Beispiel können Sie es versuchen if(&bar==NULL)... aber der Compiler könnte die Aussage außer Kraft setzen! Ein gültiger Verweis kann niemals NULL sein, daher ist der Vergleich aus der Sicht des Compilers immer falsch, und es ist frei, das zu eliminieren if Klausel als toter Code - das ist die Essenz von undefiniertem Verhalten.

Das Vermeiden von Dereferenzierung eines NULL-Zeigers, um eine Referenz zu erstellen, ist der richtige Weg, um Probleme zu vermeiden. Hier ist ein automatisierter Weg, dies zu erreichen.

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));

Für einen älteren Blick auf dieses Problem von jemandem mit besseren Schreibfähigkeiten, siehe Null Referenzen von Jim Hyslop und Herb Sutter.

Für ein weiteres Beispiel der Gefahren der Dereferenzierung eines Nullzeigers siehe Undefiniertes Verhalten beim Versuch, Code auf eine andere Plattform zu portieren von Raymond Chen.


104
2017-09-11 22:10



Du hast den wichtigsten Teil vergessen:

Member-Access mit Zeigern verwendet -> 
member-access mit Referenzen verwendet .

foo.bar ist deutlich besser als foo->bar auf dieselbe Art und Weise vi ist deutlich besser als Emacs :-)


96
2017-09-11 20:07



Abgesehen von syntaktischem Zucker ist eine Referenz a const Zeiger (nicht Zeiger auf a const). Sie müssen festlegen, auf was Bezug genommen wird, wenn Sie die Referenzvariable deklarieren, und Sie können sie später nicht mehr ändern.

Update: Jetzt, wo ich darüber nachdenke, gibt es einen wichtigen Unterschied.

Das Ziel eines Const-Zeigers kann ersetzt werden, indem seine Adresse genommen und eine Const-Umwandlung verwendet wird.

Das Ziel einer Referenz kann in keiner Weise durch UB ersetzt werden.

Dies sollte dem Compiler erlauben, mehr Optimierung für eine Referenz durchzuführen.


95
2017-09-19 12:23



Eigentlich ist eine Referenz nicht wirklich wie ein Zeiger.

Ein Compiler behält "Referenzen" auf Variablen bei, indem er einen Namen mit einer Speicheradresse verknüpft; das ist seine Aufgabe, einen Variablennamen beim Kompilieren in eine Speicheradresse zu übersetzen.

Wenn Sie eine Referenz erstellen, teilen Sie dem Compiler nur mit, dass Sie der Zeigervariablen einen anderen Namen zuweisen. Aus diesem Grund können Referenzen nicht auf Null zeigen, da eine Variable nicht sein kann und nicht sein kann.

Zeiger sind Variablen; Sie enthalten die Adresse einer anderen Variablen oder können null sein. Wichtig ist, dass ein Zeiger einen Wert hat, während ein Verweis nur eine Variable hat, auf die er verweist.

Jetzt eine Erklärung von echtem Code:

int a = 0;
int& b = a;

Hier erstellen Sie keine andere Variable, auf die verwiesen wird a; Sie fügen dem Speicherinhalt, der den Wert von enthält, einfach einen anderen Namen hinzu a. Dieser Speicher hat jetzt zwei Namen, a und b, und es kann mit beiden Namen angesprochen werden.

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

int a;
increment(a);

Wenn eine Funktion aufgerufen wird, generiert der Compiler normalerweise Speicherbereiche für die Argumente, auf die kopiert werden soll. Die Funktionssignatur definiert die zu erstellenden Räume und gibt den Namen an, der für diese Räume verwendet werden soll. Wenn ein Parameter als Referenz deklariert wird, wird der Compiler angewiesen, den Speicherbereich für die Eingabevariablen zu verwenden, anstatt während des Methodenaufrufs einen neuen Speicherplatz zuzuweisen. Es mag seltsam erscheinen, wenn Sie sagen, dass Ihre Funktion direkt eine Variable manipuliert, die im aufrufenden Bereich deklariert ist, aber denken Sie daran, dass beim Ausführen von kompiliertem Code kein Bereich mehr vorhanden ist. Es gibt nur einen flachen Speicher, und Ihr Funktionscode könnte beliebige Variablen manipulieren.

Nun kann es vorkommen, dass Ihr Compiler die Referenz beim Kompilieren nicht kennt, etwa bei Verwendung einer externen Variable. Daher kann eine Referenz als ein Zeiger in dem zugrunde liegenden Code implementiert sein oder nicht. Aber in den Beispielen, die ich Ihnen gegeben habe, wird es höchstwahrscheinlich nicht mit einem Zeiger implementiert werden.


57
2017-09-01 03:44



Referenzen sind den Zeigern sehr ähnlich, aber sie sind speziell so gestaltet, dass sie zur Optimierung von Compilern hilfreich sind.

  • Referenzen sind so angelegt, dass es für den Compiler wesentlich einfacher ist, welche Referenzaliasnamen welche Variablen haben. Zwei Hauptmerkmale sind sehr wichtig: keine "Referenzarithmetik" und keine Neuzuordnung von Referenzen. Diese ermöglichen dem Compiler herauszufinden, welche Referenzen zur Kompilierungszeit welche Variablen aliasieren.
  • Verweise dürfen sich auf Variablen beziehen, die keine Speicheradressen haben, wie sie der Compiler in Register einträgt. Wenn Sie die Adresse einer lokalen Variablen nehmen, ist es sehr schwierig für den Compiler, sie in ein Register zu setzen.

Als Beispiel:

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];
    }
}

Ein optimierender Compiler kann erkennen, dass wir ziemlich auf eine [0] und eine [1] zugreifen. Es würde gerne den Algorithmus optimieren, um:

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
}

Um eine solche Optimierung zu machen, muss bewiesen werden, dass nichts Array [1] während des Aufrufs ändern kann. Das ist ziemlich einfach zu machen. i ist nie kleiner als 2, daher kann sich array [i] niemals auf array [1] beziehen. maybeModify () erhält a0 als Referenz (Alias-Array [0]). Da es keine "Referenz" -Arithmetik gibt, muss der Compiler nur beweisen, dass vielleichtModify nie die Adresse von x bekommt, und es hat sich herausgestellt, dass nichts Array [1] ändert.

Es muss auch beweisen, dass es keine Möglichkeiten gibt, wie ein zukünftiger Aufruf a [0] lesen / schreiben könnte, während wir eine temporäre Registerkopie davon in a0 haben. Dies ist oft trivial, weil es in vielen Fällen offensichtlich ist, dass die Referenz niemals in einer permanenten Struktur wie einer Klasseninstanz gespeichert wird.

Jetzt mache dasselbe mit Zeigern

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];
    }
}

Das Verhalten ist das gleiche; nur jetzt ist es viel schwieriger zu beweisen, dass vielleicht Modify array [1] nicht ändert, weil wir ihm bereits einen Zeiger gegeben haben; Die Katze ist aus der Tasche. Jetzt muss es den viel schwierigeren Beweis machen: eine statische Analyse von vielleichtModify, um zu beweisen, dass es niemals in & x + 1 schreibt. Es muss auch beweisen, dass es nie einen Zeiger abspeichert, der auf Array [0] verweisen kann, was gerecht ist so schwierig.

Moderne Compiler werden immer besser bei statischen Analysen, aber es ist immer schön, ihnen zu helfen und Referenzen zu verwenden.

Abgesehen von solchen cleveren Optimierungen werden Compiler die Referenzen bei Bedarf tatsächlich in Zeiger umwandeln.


50
2017-09-11 20:12