Gibt es Vorteile beim Übergeben eines Zeigers gegenüber dem Übergeben eines Verweises in C ++?

225

Welche Vorteile bietet das Übergeben eines Zeigers gegenüber dem Übergeben eines Verweises in C ++?

In letzter Zeit habe ich eine Reihe von Beispielen gesehen, bei denen die Übergabe von Funktionsargumenten durch Zeiger anstelle der Übergabe als Referenz ausgewählt wurde. Gibt es Vorteile dabei?

Beispiel:

func(SPRITE *x);

mit einem Anruf von

func(&mySprite);

vs.

func(SPRITE &x);

mit einem Anruf von

func(mySprite);
Matt Pascoe
quelle
Vergessen Sie nicht new, einen Zeiger und die daraus resultierenden Eigentumsfragen zu erstellen.
Martin York

Antworten:

216

Ein Zeiger kann einen NULL-Parameter empfangen, ein Referenzparameter nicht. Wenn die Möglichkeit besteht, dass Sie "kein Objekt" übergeben möchten, verwenden Sie einen Zeiger anstelle einer Referenz.

Durch Übergeben eines Zeigers können Sie außerdem an der Aufrufstelle explizit sehen, ob das Objekt als Wert oder als Referenz übergeben wird:

// Is mySprite passed by value or by reference?  You can't tell 
// without looking at the definition of func()
func(mySprite);

// func2 passes "by pointer" - no need to look up function definition
func2(&mySprite);
Adam Rosenfield
quelle
18
Unvollständige Antwort. Die Verwendung von Zeigern autorisiert weder die Verwendung von temporären / heraufgestuften Objekten noch die Verwendung von spitzen Objekten als stapelartige Objekte. Und es wird vorgeschlagen, dass das Argument NULL sein kann, wenn meistens ein NULL-Wert verboten werden sollte. Lesen Sie die Antwort von litb, um eine vollständige Antwort zu erhalten.
Paercebal
Der zweite Funktionsaufruf wurde mit Anmerkungen versehen func2 passes by reference. Ich weiß zwar zu schätzen, dass Sie gemeint haben, dass es aus einer übergeordneten Perspektive "als Referenz" übergeben wird, implementiert durch Übergeben eines Zeigers auf einer Codeebene , aber dies war sehr verwirrend (siehe stackoverflow.com/questions/13382356/… ).
Leichtigkeitsrennen im Orbit
Ich kaufe das einfach nicht. Ja, Sie übergeben einen Zeiger, daher muss es sich um einen Ausgabeparameter handeln, denn auf was gezeigt wird, kann nicht const sein?
Deworde
Haben wir diese vorübergehende Referenz nicht in C? Ich verwende die neueste Version von Codeblock (mingw) und wähle dabei ein C-Projekt aus. Es funktioniert immer noch als Referenz (func (int & a)). Oder ist es ab C99 oder C11 verfügbar?
Jon Wheelock
1
@ JonWheelock: Nein, C hat überhaupt keine Referenzübergabe. func(int& a)ist in keiner Version des Standards gültig. Sie kompilieren Ihre Dateien wahrscheinlich versehentlich als C ++.
Adam Rosenfield
245

Übergabe durch Zeiger

  • Anrufer muss die Adresse nehmen -> nicht transparent
  • Ein 0-Wert kann angegeben werden, um zu bedeuten nothing. Dies kann verwendet werden, um optionale Argumente bereitzustellen.

Als Referenz übergeben

  • Der Anrufer übergibt nur das Objekt -> transparent. Muss für das Überladen von Operatoren verwendet werden, da ein Überladen für Zeigertypen nicht möglich ist (Zeiger sind eingebaute Typen). Sie können also keine string s = &str1 + &str2;Zeiger verwenden.
  • Keine 0-Werte möglich -> Die aufgerufene Funktion muss nicht nach ihnen suchen
  • Der Verweis auf const akzeptiert auch temporäre : void f(const T& t); ... f(T(a, b, c));, Zeiger können nicht so verwendet werden, da Sie die Adresse eines temporären nicht annehmen können.
  • Last but not least sind Referenzen einfacher zu verwenden -> weniger Fehlerwahrscheinlichkeit.
Johannes Schaub - litb
quelle
7
Durch Übergeben eines Zeigers wird auch die Meldung "Wird das Eigentum übertragen oder nicht?" Frage. Dies ist bei Referenzen nicht der Fall.
Frerich Raabe
43
Ich bin nicht einverstanden mit "weniger Chance für Fehler". Wenn Sie die Anrufstelle überprüfen und der Leser "foo (& s)" sieht, ist sofort klar, dass s geändert werden kann. Wenn Sie "foo (s)" lesen, ist überhaupt nicht klar, ob s geändert werden kann. Dies ist eine Hauptquelle für Fehler. Vielleicht gibt es weniger Chancen für eine bestimmte Klasse von Fehlern, aber insgesamt ist das Übergeben von Referenzen eine große Fehlerquelle.
William Pursell
25
Was meinst du mit "transparent"?
Gbert90
2
@ Gbert90, wenn Sie foo (& a) an einer Aufrufstelle sehen, wissen Sie, dass foo () einen Zeigertyp verwendet. Wenn Sie foo (a) sehen, wissen Sie nicht, ob es sich um eine Referenz handelt.
Michael J. Davenport
3
@ MichaelJ.Davenport - In Ihrer Erklärung schlagen Sie "transparent" vor, um etwas im Sinne von "offensichtlich, dass der Anrufer einen Zeiger übergibt, aber nicht offensichtlich, dass der Anrufer eine Referenz übergibt" zu bedeuten. In Johannes 'Post sagt er "Übergeben durch Zeiger - Anrufer muss die Adresse annehmen -> nicht transparent" und "Übergeben - Der Anrufer übergibt nur das Objekt -> transparent" - was fast das Gegenteil von dem ist, was Sie sagen . Ich denke, Gbert90s Frage "Was meinst du mit" transparent "" ist immer noch gültig.
Happy Green Kid Nickerchen
66

Ich mag die Argumentation eines Artikels von "cplusplus.com:"

  1. Übergeben Sie den Wert, wenn die Funktion den Parameter nicht ändern möchte und der Wert einfach zu kopieren ist (Ints, Doubles, Char, Bool usw.). Einfache Typen. Std :: string, std :: vector und alle anderen STL Behälter sind KEINE einfachen Typen.)

  2. Übergeben Sie den const-Zeiger, wenn das Kopieren des Werts teuer ist UND die Funktion den Wert, auf den AND NULL zeigt, nicht ändern möchte. Dies ist ein gültiger, erwarteter Wert, den die Funktion verarbeitet.

  3. Übergeben Sie einen nicht konstanten Zeiger, wenn das Kopieren des Werts teuer ist UND die Funktion den Wert ändern möchte, auf den AND NULL zeigt. Dies ist ein gültiger, erwarteter Wert, den die Funktion verarbeitet.

  4. Übergeben Sie die Konstantenreferenz, wenn das Kopieren des Werts teuer ist UND die Funktion den auf AND NULL bezogenen Wert nicht ändern möchte. Dies wäre kein gültiger Wert, wenn stattdessen ein Zeiger verwendet würde.

  5. Übergeben Sie eine nicht fortlaufende Referenz, wenn das Kopieren des Werts teuer ist UND die Funktion den auf AND NULL bezogenen Wert ändern möchte. Dies wäre kein gültiger Wert, wenn stattdessen ein Zeiger verwendet würde.

  6. Beim Schreiben von Vorlagenfunktionen gibt es keine eindeutige Antwort, da einige Kompromisse zu berücksichtigen sind, die über den Rahmen dieser Diskussion hinausgehen. Es genügt jedoch zu sagen, dass die meisten Vorlagenfunktionen ihre Parameter nach Wert oder (const) Referenz verwenden Da jedoch die Iteratorsyntax der von Zeigern (Sternchen zu "Dereferenzierung") ähnlich ist, akzeptiert jede Vorlagenfunktion, die Iteratoren als Argumente erwartet, standardmäßig auch Zeiger (und prüft nicht auf NULL, da das NULL-Iteratorkonzept eine andere Syntax hat ).

http://www.cplusplus.com/articles/z6vU7k9E/

Was ich daraus nehme, ist, dass der Hauptunterschied zwischen der Wahl eines Zeigers oder eines Referenzparameters darin besteht, ob NULL ein akzeptabler Wert ist. Das ist es.

Ob der Wert eingegeben, ausgegeben, geändert usw. werden kann, sollte schließlich in der Dokumentation / den Kommentaren zur Funktion enthalten sein.

R. Navega
quelle
Ja, für mich sind die NULL-bezogenen Begriffe hier das Hauptanliegen. Danke fürs Zitieren ..
binaryguy
62

Allen Holubs "Genug Seil, um sich in den Fuß zu schießen" listet die folgenden 2 Regeln auf:

120. Reference arguments should always be `const`
121. Never use references as outputs, use pointers

Er listet mehrere Gründe auf, warum Referenzen zu C ++ hinzugefügt wurden:

  • Sie sind erforderlich, um Kopierkonstruktoren zu definieren
  • Sie sind für Überlastungen des Bedieners erforderlich
  • const Mit Referenzen können Sie eine Semantik für die Übergabe von Werten verwenden und gleichzeitig eine Kopie vermeiden

Sein Hauptpunkt ist, dass Referenzen nicht als Ausgabeparameter verwendet werden sollten, da an der Aufrufstelle kein Hinweis darauf vorhanden ist, ob der Parameter eine Referenz oder ein Wertparameter ist. Seine Regel ist es also, nur constReferenzen als Argumente zu verwenden.

Persönlich halte ich dies für eine gute Faustregel, da dadurch klarer wird, ob ein Parameter ein Ausgabeparameter ist oder nicht. Obwohl ich dem im Allgemeinen persönlich zustimme, erlaube ich mir, mich von den Meinungen anderer in meinem Team beeinflussen zu lassen, wenn sie für Ausgabeparameter als Referenz argumentieren (einige Entwickler mögen sie sehr).

Michael Burr
quelle
8
Mein Standpunkt in diesem Argument ist, dass, wenn der Funktionsname völlig offensichtlich macht, ohne die Dokumente zu überprüfen, dass der Parameter geändert wird, eine nicht konstante Referenz in Ordnung ist. Also persönlich würde ich "getDetails (DetailStruct & result)" zulassen. Ein Zeiger dort erhöht die hässliche Möglichkeit einer NULL-Eingabe.
Steve Jessop
3
Das ist irreführend. Auch wenn einige Referenzen nicht mögen, sind sie ein wichtiger Teil der Sprache und sollten als solche verwendet werden. Diese Argumentation ist wie die Aussage, dass Sie keine Vorlagen verwenden. Sie können immer Container mit void * verwenden, um einen beliebigen Typ zu speichern. Lesen Sie die Antwort von litb.
David Rodríguez - Dribeas
4
Ich verstehe nicht, wie irreführend dies ist - es gibt Zeiten, in denen Referenzen erforderlich sind, und es gibt Zeiten, in denen Best Practices möglicherweise vorschlagen, sie nicht zu verwenden, selbst wenn Sie könnten. Das Gleiche gilt für alle Merkmale der Sprache - Vererbung, Freunde von Nichtmitgliedern, Überlastung des Bedieners, MI usw.
Michael Burr
Übrigens stimme ich zu, dass die Antwort von litb sehr gut und sicherlich umfassender ist als diese - ich habe mich nur darauf konzentriert, eine Begründung für die Vermeidung der Verwendung von Referenzen als Ausgabeparameter zu diskutieren.
Michael Burr
1
Diese Regel wird in Google C ++ Style Guide verwendet: google-styleguide.googlecode.com/svn/trunk/…
Anton Daneyko
9

Erläuterungen zu den vorhergehenden Beiträgen:


Referenzen sind KEINE Garantie dafür, dass ein Zeiger ungleich Null erhalten wird. (Obwohl wir sie oft als solche behandeln.)

Während schrecklich schlechter Code, wie Sie hinter dem schlechten Code von Woodshed herausholen , wird Folgendes kompiliert und ausgeführt: (Zumindest unter meinem Compiler.)

bool test( int & a)
{
  return (&a) == (int *) NULL;
}

int
main()
{
  int * i = (int *)NULL;
  cout << ( test(*i) ) << endl;
};

Das eigentliche Problem, das ich mit Referenzen habe, liegt bei anderen Programmierern, im Folgenden IDIOTS genannt , die im Konstruktor zuordnen, im Destruktor freigeben und keinen Kopierkonstruktor oder Operator = () angeben .

Plötzlich gibt es einen großen Unterschied zwischen foo (BAR Bar) und foo (BAR & Bar) . (Der automatische bitweise Kopiervorgang wird aufgerufen. Die Freigabe im Destruktor wird zweimal aufgerufen.)

Zum Glück werden moderne Compiler diese doppelte Freigabe desselben Zeigers übernehmen. Vor 15 Jahren haben sie es nicht getan. (Verwenden Sie unter gcc / g ++ setenv MALLOC_CHECK_ 0 , um die alten Methoden erneut aufzurufen .) Unter DEC UNIX wird derselbe Speicher zwei verschiedenen Objekten zugewiesen. Viel Spaß beim Debuggen ...


Praktischer:

  • Referenzen verbergen, dass Sie Daten ändern, die an einem anderen Ort gespeichert sind.
  • Es ist leicht, eine Referenz mit einem kopierten Objekt zu verwechseln.
  • Zeiger machen es deutlich!
Mr.Ree
quelle
16
Das ist nicht das Problem der Funktion oder Referenzen. Sie brechen Sprachregeln. Das Dereferenzieren eines Nullzeigers an sich ist bereits ein undefiniertes Verhalten. "Referenzen sind KEINE Garantie dafür, dass ein Zeiger ungleich Null erhalten wird.": Der Standard selbst sagt, dass dies der Fall ist. andere Wege stellen undefiniertes Verhalten dar.
Johannes Schaub - litb
1
Ich stimme litb zu. Der Code, den Sie uns zeigen, ist zwar mehr Sabotage als alles andere. Es gibt Möglichkeiten, alles zu sabotieren, einschließlich der Notationen "Referenz" und "Zeiger".
Paercebal
1
Ich habe gesagt, es sei "Bring dich hinter den schlechten Code des Holzschuppens"! In diesem Sinne können Sie auch i = new FOO haben; lösche i; Test (* i); Ein weiteres (leider häufiges) baumelndes Zeiger- / Referenzvorkommen.
Mr.Ree
1
Es ist eigentlich nicht die Dereferenzierung von NULL, die das Problem ist, sondern die Verwendung dieses dereferenzierten (null) Objekts. Daher gibt es aus Sicht der Sprachimplementierung keinen Unterschied (außer der Syntax) zwischen Zeigern und Referenzen. Es sind die Benutzer, die unterschiedliche Erwartungen haben.
Mr.Ree
2
Unabhängig davon, was Sie mit der zurückgegebenen Referenz tun *i, hat Ihr Programm in dem Moment, in dem Sie sagen , ein undefiniertes Verhalten. Beispielsweise kann der Compiler diesen Code sehen und annehmen, dass "OK, dieser Code in allen Codepfaden ein undefiniertes Verhalten aufweist, sodass diese gesamte Funktion nicht erreichbar sein muss". Dann wird davon ausgegangen, dass nicht alle Zweige, die zu dieser Funktion führen, belegt sind. Dies ist eine regelmäßig durchgeführte Optimierung.
David Stone
5

Die meisten Antworten hier sprechen nicht die inhärente Mehrdeutigkeit an, einen Rohzeiger in einer Funktionssignatur zu haben, um die Absicht auszudrücken. Die Probleme sind die folgenden:

  • Der Aufrufer weiß nicht, ob der Zeiger auf ein einzelnes Objekt oder auf den Anfang eines "Arrays" von Objekten zeigt.

  • Der Aufrufer weiß nicht, ob der Zeiger den Speicher "besitzt", auf den er zeigt. IE, ob die Funktion den Speicher freigeben soll oder nicht. ( foo(new int)- Ist das ein Speicherverlust?).

  • Der Anrufer weiß nicht, ob er nullptrsicher an die Funktion übergeben werden kann oder nicht .

Alle diese Probleme werden durch Referenzen gelöst:

  • Referenzen beziehen sich immer auf ein einzelnes Objekt.

  • Referenzen besitzen niemals das Gedächtnis, auf das sie sich beziehen, sie sind lediglich ein Blick in das Gedächtnis.

  • Referenzen dürfen nicht null sein.

Dies macht Referenzen zu einem viel besseren Kandidaten für die allgemeine Verwendung. Referenzen sind jedoch nicht perfekt - es sind einige Hauptprobleme zu berücksichtigen.

  • Keine explizite Indirektion. Dies ist bei einem Rohzeiger kein Problem, da wir den &Operator verwenden müssen, um zu zeigen, dass wir tatsächlich einen Zeiger übergeben. Zum Beispiel int a = 5; foo(a);ist hier überhaupt nicht klar, dass a als Referenz übergeben wird und geändert werden könnte.
  • Nullbarkeit. Diese Schwäche von Zeigern kann auch eine Stärke sein, wenn wir tatsächlich wollen, dass unsere Referenzen nullbar sind. Da std::optional<T&>Zeiger (aus guten Gründen) nicht gültig sind, geben sie uns die gewünschte Nullfähigkeit.

Wenn wir also eine nullfähige Referenz mit expliziter Indirektion wollen, sollten wir nach einem T*Recht greifen ? Falsch!

Abstraktionen

In unserer Verzweiflung nach Nullfähigkeit können wir nach T*allen zuvor aufgeführten Mängeln und semantischen Zweideutigkeiten greifen und diese einfach ignorieren. Stattdessen sollten wir nach dem greifen, was C ++ am besten kann: einer Abstraktion. Wenn wir einfach eine Klasse schreiben, die einen Zeiger umschließt, gewinnen wir die Ausdruckskraft sowie die Nullbarkeit und explizite Indirektion.

template <typename T>
struct optional_ref {
  optional_ref() : ptr(nullptr) {}
  optional_ref(T* t) : ptr(t) {}
  optional_ref(std::nullptr_t) : ptr(nullptr) {}

  T& get() const {
    return *ptr;
  }

  explicit operator bool() const {
    return bool(ptr);
  }

private:
  T* ptr;
};

Dies ist die einfachste Oberfläche, die ich mir vorstellen kann, aber sie erledigt den Job effektiv. Sie können die Referenz initialisieren, prüfen, ob ein Wert vorhanden ist, und auf den Wert zugreifen. Wir können es so verwenden:

void foo(optional_ref<int> x) {
  if (x) {
    auto y = x.get();
    // use y here
  }
}

int x = 5;
foo(&x); // explicit indirection here
foo(nullptr); // nullability

Wir haben unsere Ziele erreicht! Lassen Sie uns nun die Vorteile im Vergleich zum Rohzeiger sehen.

  • Die Schnittstelle zeigt deutlich, dass sich die Referenz nur auf ein Objekt beziehen sollte.
  • Offensichtlich besitzt es nicht den Speicher, auf den es sich bezieht, da es keinen benutzerdefinierten Destruktor und keine Methode zum Löschen des Speichers hat.
  • Der Aufrufer weiß, dass übergeben werden nullptrkann, da der Funktionsautor explizit nach einem fragtoptional_ref

Wir könnten die Schnittstelle von hier aus komplexer gestalten, z. B. durch Hinzufügen von Gleichheitsoperatoren, einer Monade get_orund einer mapSchnittstelle, einer Methode, die den Wert abruft oder eine Ausnahme auslöst, constexprUnterstützung. Das können Sie tun.

Anstatt rohe Zeiger zu verwenden, begründen Sie, was diese Zeiger tatsächlich in Ihrem Code bedeuten, und nutzen Sie entweder eine Standard-Bibliotheksabstraktion oder schreiben Sie Ihre eigene. Dadurch wird Ihr Code erheblich verbessert.

Tom VH
quelle
3

Nicht wirklich. Intern erfolgt die Übergabe als Referenz, indem im Wesentlichen die Adresse des referenzierten Objekts übergeben wird. Es gibt also wirklich keine Effizienzgewinne durch Übergeben eines Zeigers.

Das Übergeben von Referenzen hat jedoch einen Vorteil. Es ist garantiert, dass Sie eine Instanz eines beliebigen Objekts / Typs haben, das übergeben wird. Wenn Sie einen Zeiger übergeben, besteht das Risiko, dass Sie einen NULL-Zeiger erhalten. Durch die Verwendung von Pass-by-Reference schieben Sie eine implizite NULL-Prüfung um eine Ebene an den Aufrufer Ihrer Funktion.

Brian
quelle
1
Das ist sowohl ein Vorteil als auch ein Nachteil. Viele APIs verwenden NULL-Zeiger, um etwas Nützliches zu bedeuten (dh NULL-Zeitangaben warten für immer, während der Wert bedeutet, dass Sie so lange warten müssen).
Greg Rogers
1
@Brian: Ich möchte nicht picken, aber: Ich würde nicht sagen, dass man garantiert eine Instanz bekommt, wenn man eine Referenz bekommt. Dangling-Referenzen sind weiterhin möglich, wenn der Aufrufer einer Funktion einen Dangling-Zeiger de-referenziert, den der Angerufene nicht kennen kann.
Foraidt
Manchmal können Sie sogar Leistung erzielen, indem Sie Referenzen verwenden, da diese keinen Speicherplatz benötigen und keine Adressen für sich selbst zugewiesen haben. Keine Indirektion erforderlich.
Johannes Schaub - litb
Programme, die baumelnde Referenzen enthalten, sind in C ++ nicht gültig. Daher kann der Code davon ausgehen, dass alle Referenzen gültig sind.
Konrad Rudolph
2
Ich kann einen Nullzeiger definitiv dereferenzieren und der Compiler kann es nicht sagen ... Wenn der Compiler nicht sagen kann, dass es "ungültiges C ++" ist, ist es wirklich ungültig?
rmeador