3929 Stimmen

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

Ich weiß, dass Referenzen syntaktischer Zucker sind, damit der Code leichter zu lesen und zu schreiben ist.

Aber was ist der Unterschied zwischen einer Zeigervariablen und einer Referenzvariablen?

1 Stimmen

Ein lokaler Verweis (d.h. ein Verweis, der nicht in einer Struktur oder Klasse enthalten ist) weist nicht unbedingt Speicherplatz zu. Das erkennen Sie an dem Unterschied zwischen sizeof(int &) und sizeof(struct { int &x; }).

136 Stimmen

Ich denke, Punkt 2 sollte lauten: "Ein Zeiger darf NULL sein, eine Referenz jedoch nicht. Nur fehlerhafter Code kann eine NULL-Referenz erzeugen und ihr Verhalten ist undefiniert."

32 Stimmen

Zeiger sind nur ein weiterer Objekttyp, und wie jedes Objekt in C++ können sie eine Variable sein. Referenzen hingegen sind niemals Objekte, nur Variablen.

130voto

Orion Edwards Punkte 117361

Sie haben den wichtigsten Teil vergessen:

member-Zugriff mit Zeigern verwendet ->
Mitglied-Zugang mit Referenzen verwendet .

foo.bar es eindeutig besser als foo->bar auf die gleiche Weise, wie vi es eindeutig besser als Emacs :-)

6 Stimmen

@Orion Edwards >Mitgliederzugriff mit Zeigern verwendet -> >Mitgliederzugriff mit Referenzen verwendet . Dies ist nicht 100%ig richtig. Sie können eine Referenz auf einen Zeiger haben. In diesem Fall würden Sie auf die Mitglieder eines de-referenzierten Zeigers zugreifen mit -> struct Node { Node *next; }; Node *first; // p ist eine Referenz auf einen Zeiger void foo(Node*&p) { p->next = first; } Node *bar = new Node; foo(bar); -- OP: Sind Sie mit den Konzepten von rvalues und lvalues vertraut?

4 Stimmen

Smart Pointer haben sowohl . (Methoden auf Smart-Pointer-Klasse) und -> (Methoden auf zugrunde liegenden Typ).

2 Stimmen

@user6105 Orion Edwards Aussage ist tatsächlich zu 100 % wahr. "Zugriff auf die Elemente [des] de-referenzierten Zeigers" Ein Zeiger hat keine Mitglieder. Das Objekt, auf das sich der Zeiger bezieht, hat Mitglieder, und der Zugriff auf diese ist genau das, was -> sieht Verweise auf Zeiger vor, genau wie beim Zeiger selbst.

89voto

Cort Ammon Punkte 9576

Referenzen sind Zeigern sehr ähnlich, aber sie sind speziell für die Optimierung von Compilern entwickelt worden.

  • Referenzen sind so konzipiert, dass es für den Compiler wesentlich einfacher ist, nachzuvollziehen, welche Referenz auf welche Variablen verweist. Zwei Hauptmerkmale sind sehr wichtig: keine "Referenzarithmetik" und keine Neuzuweisung von Referenzen. Dadurch kann der Compiler zur Compilierzeit herausfinden, welche Referenzen welche Variablen verweisen.
  • Referenzen können sich auf Variablen beziehen, die keine Speicheradressen haben, z. B. solche, die der Compiler in Registern ablegt. Wenn Sie die Adresse einer lokalen Variablen nehmen, ist es für den Compiler sehr schwierig, sie in ein Register zu setzen.

Ein 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 könnte erkennen, dass wir auf a[0] und a[1] ziemlich oft zugreifen. Er würde den Algorithmus gerne dahingehend optimieren:

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 vorzunehmen, muss nachgewiesen werden, dass sich array[1] während des Aufrufs nicht ändern kann. Dies ist recht einfach zu bewerkstelligen. i ist nie kleiner als 2, so dass array[i] nie auf array[1] verweisen kann. maybeModify() erhält a0 als Referenz (alias array[0]). Da es keine "Referenz"-Arithmetik gibt, muss der Compiler nur beweisen, dass maybeModify niemals die Adresse von x erhält, und er hat bewiesen, dass sich array[1] nicht ändert.

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

Nun machen Sie 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 ist es jetzt viel schwieriger zu beweisen, dass maybeModify niemals array[1] verändert, weil wir ihm bereits einen Zeiger gegeben haben; die Katze ist aus dem Sack. Jetzt muss der viel schwierigere Beweis erbracht werden: eine statische Analyse von maybeModify, um zu beweisen, dass es niemals in &x + 1 schreibt. Es muss auch beweisen, dass es niemals einen Zeiger speichert, der auf array[0] verweisen kann, was genauso knifflig ist.

Moderne Compiler werden bei der statischen Analyse immer besser, aber es ist immer gut, ihnen zu helfen und Referenzen zu verwenden.

Natürlich werden Compiler, wenn sie nicht so clever optimiert sind, bei Bedarf tatsächlich Referenzen in Zeiger umwandeln.

EDIT: Fünf Jahre, nachdem ich diese Antwort veröffentlicht habe, habe ich einen tatsächlichen technischen Unterschied gefunden, bei dem Referenzen nicht nur eine andere Sichtweise auf dasselbe Adressierungskonzept darstellen. Referenzen können die Lebensdauer von temporären Objekten auf eine Weise verändern, wie es Zeiger nicht können.

F createF(int argument);

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

Normalerweise werden temporäre Objekte, wie das durch den Aufruf von createF(5) werden am Ende des Ausdrucks zerstört. Durch die Bindung dieses Objekts an eine Referenz wird es jedoch zerstört, ref wird C++ die Lebensdauer dieses temporären Objekts verlängern, bis ref aus dem Rahmen fällt.

0 Stimmen

Es stimmt, dass der Körper sichtbar sein muss. Allerdings ist die Feststellung, dass maybeModify nimmt nicht die Adresse von etwas, das mit x ist wesentlich einfacher, als zu beweisen, dass ein Haufen Zeigerarithmetik nicht vorkommt.

0 Stimmen

Ich glaube, dass der Optimierer bereits tut, dass "ein Bündel von Zeiger Arithemetik nicht auftreten" Überprüfung für eine Reihe von anderen Gründen.

0 Stimmen

"Referenzen sind Zeigern sehr ähnlich" - semantisch, in geeigneten Kontexten - aber in Bezug auf den generierten Code nur in einigen Implementierungen und nicht durch irgendeine Definition/Anforderung. Ich weiß, dass Sie darauf hingewiesen haben, und ich widerspreche Ihrem Beitrag in praktischer Hinsicht nicht, aber wir haben bereits zu viele Probleme mit Leuten, die zu viel in Kurzbeschreibungen wie "Referenzen sind wie Zeiger" lesen.

83voto

Vincent Robert Punkte 34478

Eigentlich ist ein Verweis nicht wirklich wie ein Zeiger.

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

Wenn Sie einen Verweis erstellen, teilen Sie dem Compiler lediglich mit, dass Sie der Zeigervariablen einen anderen Namen zuweisen; deshalb können Verweise nicht auf "null" zeigen, denn eine Variable kann nicht sein und nicht sein.

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 enthält, auf die er verweist.

Nun einige Erläuterungen zum echten Code:

int a = 0;
int& b = a;

Hier erstellen Sie keine weitere Variable, die auf a ; Sie fügen lediglich einen weiteren Namen zum Speicherinhalt hinzu, der den Wert von a . Dieser Speicher hat jetzt zwei Namen, a y b und kann mit beiden Namen angesprochen werden.

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

int a;
increment(a);

Beim Aufruf einer Funktion erzeugt der Compiler in der Regel Speicherplätze für die Argumente, in die sie kopiert werden. Die Funktionssignatur definiert die zu erzeugenden Speicherplätze und gibt den Namen an, der für diese Speicherplätze verwendet werden soll. Die Deklaration eines Parameters als Referenz weist den Compiler lediglich an, den Speicherplatz der Eingangsvariablen zu verwenden, anstatt während des Methodenaufrufs einen neuen Speicherplatz zuzuweisen. Es mag seltsam erscheinen zu sagen, dass Ihre Funktion eine im aufrufenden Bereich deklarierte Variable direkt manipuliert, aber denken Sie daran, dass es bei der Ausführung von kompiliertem Code keinen Bereich mehr gibt; es gibt nur noch einfachen flachen Speicher, und Ihr Funktionscode könnte beliebige Variablen manipulieren.

Nun kann es Fälle geben, in denen Ihr Compiler die Referenz beim Kompilieren nicht erkennen kann, z. B. bei Verwendung einer externen Variablen. Eine Referenz kann also im zugrunde liegenden Code als Zeiger implementiert sein oder auch nicht. Aber in den Beispielen, die ich Ihnen gegeben habe, wird sie höchstwahrscheinlich nicht mit einem Zeiger implementiert werden.

2 Stimmen

Ein Verweis ist ein Verweis auf einen l-Wert, nicht unbedingt auf eine Variable. Aus diesem Grund ist sie viel näher an einem Zeiger als an einem echten Alias (ein Konstrukt zur Kompilierzeit). Beispiele für Ausdrücke, die referenziert werden können, sind *p oder sogar *p++

6 Stimmen

Richtig, ich habe nur auf die Tatsache hingewiesen, dass eine Referenz nicht immer eine neue Variable auf den Stack schiebt, wie es ein neuer Zeiger tut.

4 Stimmen

@VincentRobert: Es verhält sich genauso wie ein Zeiger... wenn die Funktion inlined ist, werden sowohl Referenz als auch Zeiger wegoptimiert. Wenn es einen Funktionsaufruf gibt, muss die Adresse des Objekts an die Funktion übergeben werden.

48voto

Eine Referenz kann niemals NULL .

13 Stimmen

In der Antwort von Mark Ransom finden Sie ein Gegenbeispiel. Dies ist der am häufigsten behauptete Mythos über Referenzen, aber es ist ein Mythos. Die einzige Garantie, die Sie durch die Norm haben, ist, dass Sie sofort UB haben, wenn Sie eine NULL-Referenz haben. Aber das ist ungefähr so, als würde man sagen: "Dieses Auto ist sicher, es kann nie von der Straße abkommen. (Wir übernehmen keine Verantwortung für das, was passieren könnte, wenn Sie es trotzdem von der Straße abkommen lassen. Es könnte einfach explodieren.)"

20 Stimmen

@cmaster: In einem gültigen Programm kann eine Referenz nicht null sein. Ein Zeiger aber schon. Das ist kein Mythos, das ist eine Tatsache.

11 Stimmen

@Mehrdad Ja, gültige Programme bleiben auf der Straße. Aber es gibt keine Verkehrsschranke, die erzwingt, dass Ihr Programm tatsächlich funktioniert. Große Teile der Straße sind tatsächlich nicht markiert. Es ist also extrem einfach, nachts von der Straße abzukommen. Und für das Debuggen solcher Fehler ist es entscheidend, dass Sie wissen Das kann passieren: Die Null-Referenz kann sich ausbreiten, bevor sie Ihr Programm zum Absturz bringt, genau wie ein Null-Zeiger es kann. Und wenn das passiert, haben Sie Code wie void Foo::bar() { virtual_baz(); } die segfaults. Wenn Sie sich nicht bewusst sind, dass Referenzen null sein können, können Sie die Null nicht zu ihrem Ursprung zurückverfolgen.

38voto

Es gibt einen semantischen Unterschied, der esoterisch anmuten mag, wenn Sie nicht mit dem abstrakten oder sogar akademischen Studium von Computersprachen vertraut sind.

Auf höchster Ebene sind Referenzen transparente "Aliasnamen". Ihr Computer kann eine Adresse verwenden, damit sie funktionieren, aber darüber sollten Sie sich keine Gedanken machen: Sie sollten sie als "nur einen anderen Namen" für ein vorhandenes Objekt betrachten, und die Syntax spiegelt dies wider. Sie sind strenger als Zeiger, so dass Ihr Compiler Sie zuverlässiger warnen kann, wenn Sie im Begriff sind, einen "Dangling Reference" zu erzeugen, als wenn Sie im Begriff sind, einen "Dangling Pointer" zu erzeugen.

Darüber hinaus gibt es natürlich einige praktische Unterschiede zwischen Zeigern und Referenzen. Die Syntax für die Verwendung von Zeigern und Referenzen ist offensichtlich unterschiedlich, und es ist nicht möglich, Referenzen "neu zu setzen", Referenzen auf das Nichts zu haben oder Zeiger auf Referenzen zu haben.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X