c++ übergeben als - Was sind die Unterschiede zwischen einer Zeigervariable und einer Referenzvariablen in C ++?




15 Answers

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

Eine Referenz kann als konstanter Zeiger (nicht zu verwechseln mit einem Zeiger auf einen konstanten Wert!) Mit automatischer Indirektion verstanden werden, dh der Compiler wendet den Operator * für Sie an.

Alle Referenzen müssen mit einem Wert ungleich Null initialisiert werden, da sonst die Kompilierung fehlschlägt. Es ist weder möglich, die Adresse einer Referenz abzurufen - der Adressoperator gibt stattdessen die Adresse des referenzierten Werts zurück, und es ist auch nicht möglich, Berechnungen mit Referenzen durchzuführen.

C-Programmierer mögen C ++ - Referenzen nicht mögen, da es nicht mehr offensichtlich ist, wenn eine Indirektion erfolgt oder wenn ein Argument als Wert oder als Zeiger übergeben wird, ohne die Funktionssignaturen zu betrachten.

C ++ - Programmierer mögen Zeiger nicht mögen, da sie als unsicher angesehen werden - obwohl Referenzen nicht wirklich sicherer sind als konstante Zeiger, außer in den trivialsten Fällen - sie die Bequemlichkeit der automatischen Indirektion haben und eine andere semantische Konnotation haben.

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

Auch wenn eine Referenz häufig mithilfe einer Adresse in der zugrunde liegenden Assemblersprache implementiert wird, denken Sie bitte nicht an eine Referenz als einen witzig aussehenden Zeiger auf ein Objekt. Eine Referenz ist das Objekt. Es ist weder ein Zeiger auf das Objekt noch eine Kopie des Objekts. Es ist das Objekt.

Aber wenn eine Referenz wirklich das Objekt wäre, wie könnte es dann hängende Referenzen geben? In nicht verwalteten Sprachen können Referenzen nicht "sicherer" als Zeiger sein - im Allgemeinen gibt es keine Möglichkeit, Alias-Werte über Bereichsgrenzen hinweg zuverlässig zu Alias-Werten zu verwenden!

Warum halte ich C ++ - Referenzen für nützlich?

Aus einem C-Hintergrund stammend, können C ++ - Verweise wie ein dummes Konzept aussehen, sollten jedoch nach Möglichkeit immer anstelle von Zeigern verwendet werden: Die automatische Indirektion ist praktisch, und Verweise sind besonders nützlich, wenn sie mit RAII - aber nicht aufgrund der wahrgenommenen Sicherheit Vorteil, aber eher, weil sie das Schreiben von idiomatischem Code weniger umständlich machen.

RAII ist eines der zentralen Konzepte von C ++, es interagiert jedoch nicht unerheblich mit der Kopiersemantik. Durch das Übergeben von Objekten als Referenz werden diese Probleme vermieden, da kein Kopieren erforderlich ist. Wenn Verweise nicht in der Sprache vorhanden wären, müssten stattdessen Zeiger verwendet werden, deren Verwendung umständlicher ist. Dies verstößt gegen das Prinzip des Sprachdesigns, wonach die Best-Practice-Lösung einfacher sein sollte als die Alternativen.

rückgabewert referenzen vs

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

Aber was sind die Unterschiede?

Zusammenfassung aus den Antworten und Links unten:

  1. Ein Zeiger kann beliebig oft neu zugewiesen werden, während eine Referenz nach dem Binden nicht erneut zugewiesen werden kann.
  2. Zeiger können nirgendwo zeigen ( NULL ), wohingegen eine Referenz immer auf ein Objekt verweist.
  3. Sie können die Adresse einer Referenz nicht wie bei Zeigern verwenden.
  4. Es gibt keine "Referenzarithmetik" (Sie können jedoch die Adresse eines Objekts, auf das durch eine Referenz verwiesen wird, verwenden und Zeigerarithmetik wie in &obj + 5 ).

Um ein Missverständnis zu klären:

Der C ++ - Standard achtet sehr darauf, zu vermeiden, wie ein Compiler Verweise implementieren darf, aber jeder C ++ - Compiler implementiert Verweise als Zeiger. Das heißt, eine Erklärung wie:

int &ri = i;

Wenn es nicht vollständig wegoptimiert ist, weist es dieselbe Menge an Speicher zu wie ein Zeiger und speichert die Adresse von i in diesen Speicher.

Ein Zeiger und eine Referenz verwenden also dieselbe Menge an Speicher.

Generell,

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

Interessante Lektüre:




Im Gegensatz zur allgemeinen 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 arbeiten - aber wenn Sie es schaffen, reißen Sie sich die Haare aus und versuchen, sie zu finden. Referenzen sind in C ++ nicht unbedingt sicher!

Technisch ist dies eine ungültige Referenz , keine Nullreferenz. C ++ unterstützt keine Nullreferenzen als Konzept, wie Sie es vielleicht in anderen Sprachen finden. Es gibt auch andere Arten ungültiger Verweise. Jede ungültige Referenz wirft das Spektrum undefinierten Verhaltens auf , genau wie bei einem ungültigen Zeiger.

Der tatsächliche Fehler liegt in der Dereferenzierung des NULL-Zeigers vor der Zuweisung zu einer Referenz. Mir sind jedoch keine Compiler bekannt, die unter dieser Bedingung Fehler erzeugen - der Fehler breitet sich im Code weiter aus. Das macht dieses Problem so heimtückisch. Wenn Sie einen NULL-Zeiger dereferenzieren, stürzen Sie meistens an dieser Stelle ab, und es ist nicht viel Debugging erforderlich, um dies herauszufinden.

Mein Beispiel oben ist kurz und verständlich. Hier ist ein realistischeres Beispiel.

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 noch einmal darauf hinweisen, dass der einzige Weg, eine Nullreferenz zu erhalten, durch missgebildeten Code besteht, und wenn Sie ihn haben, erhalten Sie undefiniertes Verhalten. Es ist nie sinnvoll, nach einer Nullreferenz zu suchen. Sie können beispielsweise if(&bar==NULL)... versuchen if(&bar==NULL)... aber der Compiler optimiert möglicherweise die Anweisung nicht mehr! Eine gültige Referenz kann niemals NULL sein, daher ist der Vergleich aus Sicht des Compilers immer falsch, und es ist frei, die if Klausel als toten Code zu löschen. Dies ist der Kern von undefiniertem Verhalten.

Der richtige Weg, um Probleme zu vermeiden, besteht darin, zu vermeiden, dass ein NULL-Zeiger dereferenziert wird, um eine Referenz zu erstellen. 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));

Einen älteren Blick auf dieses Problem von jemandem mit besseren Schreibfähigkeiten finden Sie unter Null-Referenzen von Jim Hyslop und Herb Sutter.

Ein anderes Beispiel für die Gefahren der Dereferenzierung eines Nullzeigers finden Sie unter Offenlegen von undefiniertem Verhalten beim Versuch, Code von Raymond Chen auf eine andere Plattform zu portieren .




Sie haben den wichtigsten Teil vergessen:

Mitgliederzugriff mit Zeigern verwendet ->
Mitgliederzugang mit Verweisen .

foo.bar ist foo->bar in der gleichen Weise deutlich überlegen, wie vi Emacs eindeutig überlegen ist :-)




Referenzen sind Zeigern sehr ähnlich, aber sie sind speziell darauf ausgelegt, bei der Optimierung von Compilern hilfreich zu sein.

  • Referenzen sind so gestaltet, dass der Compiler wesentlich einfacher nachvollziehen kann, welche Referenzaliase welche Variablen haben. Zwei Hauptmerkmale sind sehr wichtig: keine "Referenzarithmetik" und keine Neuzuweisung von Referenzen. Dadurch kann der Compiler ermitteln, welche Verweise auf Alias ​​welche Variablen zur Kompilierzeit enthalten.
  • Verweise dürfen sich auf Variablen beziehen, die keine Speicheradressen haben, z. B. solche, die der Compiler für die Registrierung in Register wählt. Wenn Sie die Adresse einer lokalen Variablen verwenden, kann der Compiler diese nur schwer in ein Register eintragen.

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 feststellen, dass wir auf eine ganze Reihe von [0] und [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
}

Für eine solche Optimierung muss nachgewiesen werden, dass Array [1] während des Aufrufs durch nichts geändert werden kann. Das ist ziemlich einfach. i ist nie kleiner als 2, daher kann sich array [i] niemals auf array [1] beziehen. maybeModify () erhält als Referenz a0 (Aliasing-Array [0]). Da es keine "Referenz" -Arithmetik gibt, muss der Compiler nur beweisen, dass vielleichtModify niemals die Adresse von x erhält, und es hat sich gezeigt, dass Array nichts ändert [1].

Es muss auch gezeigt werden, dass es keine Möglichkeiten gibt, mit denen ein zukünftiger Aufruf eine [0] lesen / schreiben kann, während in a0 eine temporäre Registerkopie davon vorhanden ist. Dies ist oftmals trivial zu beweisen, da in vielen Fällen offensichtlich ist, dass die Referenz niemals in einer permanenten Struktur wie einer Klasseninstanz gespeichert wird.

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 dasselbe; erst jetzt ist es viel schwieriger zu beweisen, dass maybeModify array [1] nie ändert, weil wir ihm bereits einen Zeiger gegeben haben; Die Katze ist aus der Tasche. Jetzt muss es den viel schwierigeren Beweis leisten: Eine statische Analyse von maybeModify, um zu beweisen, dass es niemals nach & x + 1 schreibt, muss auch beweisen, dass es niemals einen Zeiger speichert, der sich auf array [0] beziehen kann, was gerecht ist so knifflig.

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

Natürlich können Compiler Verweise in Zeiger umwandeln, wenn dies nicht möglich ist.

EDIT: Fünf Jahre nach der Veröffentlichung dieser Antwort fand ich einen tatsächlichen technischen Unterschied, bei dem Referenzen anders sind als nur eine andere Sichtweise auf dasselbe Adressierungskonzept. Referenzen können die Lebensdauer von temporären Objekten auf eine Weise verändern, die 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 der durch den Aufruf von createF(5) am Ende des Ausdrucks zerstört.Indem refC ++ dieses Objekt an eine Referenz gebunden wird, verlängert C ++ jedoch die Lebensdauer dieses temporären Objekts, bis refes den Gültigkeitsbereich verlässt.




Während sowohl Referenzen als auch Zeiger für den indirekten Zugriff auf einen anderen Wert verwendet werden, gibt es zwei wichtige Unterschiede zwischen Referenzen und Zeigern. Der erste ist, dass eine Referenz immer auf ein Objekt verweist: Es ist ein Fehler, eine Referenz zu definieren, ohne sie zu initialisieren. Das Zuweisungsverhalten ist der zweite wichtige Unterschied: Durch das Zuweisen zu einer Referenz wird das Objekt geändert, an das die Referenz gebunden ist. Der Verweis auf ein anderes Objekt wird nicht erneut gebunden. Nach der Initialisierung bezieht sich eine Referenz immer auf das gleiche zugrunde liegende Objekt.

Betrachten Sie diese beiden Programmfragmente. Im ersten weisen wir einen Zeiger einem anderen zu:

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

Nach der Zuweisung ival bleibt das von pi adressierte Objekt unverändert. Durch die Zuweisung wird der Wert von pi geändert, sodass er auf ein anderes Objekt verweist. Betrachten Sie nun ein ähnliches Programm, das zwei Referenzen zuweist:

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

Diese Zuweisung ändert sich in ival, den von ri referenzierten Wert, und nicht die Referenz selbst. Nach der Zuweisung beziehen sich die beiden Verweise immer noch auf ihre ursprünglichen Objekte, und der Wert dieser Objekte ist jetzt ebenfalls gleich.




Eine Referenz ist ein Alias ​​für eine andere Variable, während ein Zeiger die Speicheradresse einer Variablen enthält. Referenzen werden im Allgemeinen als Funktionsparameter verwendet, so dass das übergebene Objekt nicht die Kopie, sondern das Objekt selbst ist.

    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. 



Dies basiert auf dem tutorial . Was geschrieben wird, macht es klarer:

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

Einfach daran zu erinnern,

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

Da wir uns auf fast jedes Zeiger-Tutorial beziehen können, ist ein Zeiger ein Objekt, das von der Zeigerarithmetik unterstützt wird, wodurch der Zeiger einem Array ähnelt.

Schauen Sie sich die folgende Aussage an:

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

alias_Tomkann als alias of a variable(anders mit typedef, was ist alias of a type) verstanden werden Tom. Es ist auch in Ordnung zu vergessen, dass die Terminologie einer solchen Aussage darin besteht, eine Referenz von zu erstellen Tom.




Ein Verweis auf einen Zeiger ist in C ++ möglich, das Umkehren ist jedoch nicht möglich, wenn ein Zeiger auf einen Verweis nicht möglich ist. Ein Verweis auf einen Zeiger bietet eine sauberere Syntax zum Ändern des Zeigers. Schau dir dieses Beispiel an:

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

Und betrachten Sie die C-Version des obigen Programms. In C müssen Sie Zeiger auf Zeiger verwenden (multiple Indirektion). Dies führt zu Verwirrung und das Programm kann kompliziert aussehen.

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

Besuchen Sie die folgenden Informationen, um weitere Informationen zum Verweis auf den Zeiger zu erhalten:

Wie gesagt, ein Zeiger auf einen Verweis ist nicht möglich. Versuchen Sie folgendes Programm:

#include <iostream>
using namespace std;

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



Ich verwende Referenzen, es sei denn, ich brauche eine dieser

  • Nullzeiger können als Sentinel-Wert verwendet werden. Dies ist häufig eine kostengünstige Möglichkeit, Funktionsüberlastung oder die Verwendung eines Bool zu vermeiden.

  • Sie können Arithmetik mit einem Zeiger ausführen. Zum Beispiel,p += offset;




Ein weiterer Unterschied ist, dass Sie Zeiger auf einen ungültigen Typ haben können (und zwar Zeiger auf irgendetwas), aber Verweise auf ungültig sind verboten.

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

Ich kann nicht sagen, dass ich mit diesem besonderen Unterschied wirklich glücklich bin. Ich würde es sehr vorziehen, wenn die Bedeutung der Referenz auf alles mit einer Adresse und sonst das gleiche Verhalten für Referenzen erlaubt wäre. Damit könnten einige Äquivalente von C-Bibliotheksfunktionen wie memcpy unter Verwendung von Referenzen definiert werden.




Außerdem kann eine Referenz, die ein Parameter für eine Funktion ist, die inliniert ist, anders als ein Zeiger behandelt werden.

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

Viele Compiler erzwingen beim Inlinieren der Zeigerversion eins tatsächlich ein Schreiben in den Speicher (wir nehmen die Adresse explizit). Sie lassen jedoch die Referenz in einem Register, das optimaler ist.

Für Funktionen, die nicht inline sind, erzeugen Zeiger und Referenz natürlich den gleichen Code. Es ist immer besser, Intrinsics nach Wert zu übergeben, als nach Referenz, wenn sie nicht geändert und von der Funktion zurückgegeben werden.




Eine weitere interessante Verwendung von Referenzen besteht darin, ein Standardargument eines benutzerdefinierten Typs anzugeben:

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

Das Standardaroma verwendet den 'bind const'-Verweis auf einen temporären Aspekt von Verweisen.




Vielleicht helfen einige Metaphern; Im Kontext Ihres Desktop-Screenspaces -

  • Für eine Referenz müssen Sie ein aktuelles Fenster angeben.
  • Ein Zeiger erfordert die Position eines Platzes auf dem Bildschirm, der sicherstellt, dass er keine oder mehr Instanzen dieses Fenstertyps enthält.



Es gibt einen sehr wichtigen nichttechnischen Unterschied zwischen Zeigern und Verweisen: Ein Argument, das von einem Zeiger an eine Funktion übergeben wird, ist viel sichtbarer als ein Argument, das von einer nicht konstanten Referenz an eine Funktion übergeben wird. Zum Beispiel:

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

Zurück in C kann ein Aufruf, der so aussieht, fn(x)nur per Wert übergeben werden, er kann also definitiv nicht geändert werden x. Um ein Argument zu ändern, müssen Sie einen Zeiger übergeben fn(&x). Wenn also einem Argument kein Argument vorangestellt wurde &, wussten Sie, dass es nicht geändert werden würde. (Das Gegenteil, &bedeutet modifiziert, war nicht wahr, da manchmal große schreibgeschützte Strukturen per constZeiger übergeben werden mussten.)

Einige argumentieren, dass dies eine nützliche Funktion beim Lesen von Code ist. Zeigerparameter sollten immer für modifizierbare Parameter und nicht für Nichtverweise verwendet werden const, auch wenn die Funktion niemals a erwartet nullptr. Das heißt, diese Leute argumentieren, dass Funktionssignaturen wie fn3()oben nicht erlaubt sein sollten. Ein Beispiel dafür sind die C ++ - Stilrichtlinien von Google .




Ich entscheide immer nach this Regel aus den C ++ Core Guidelines:

T * gegenüber T vorziehen, wenn "kein Argument" eine gültige Option ist




Related