Ein anderer Name für dieselbe Sache
Auf der Seite zu den Funktionsparametern wurde jedes Argument in die Funktion kopiert. Diese Kopie ist der Grund, warum eine Funktion die Variable des Aufrufers nicht ändern kann – sie sieht stets nur ihr eigenes Duplikat. Eine Referenz durchbricht diese Mauer. Sie ist ein Alias: ein zweiter Name, der an eine bestehende Variable gebunden ist und genau denselben Speicher teilt.
Du erzeugst eine Referenz mit & in der Deklaration. Einmal gebunden, sind Referenz und Original nicht mehr zu unterscheiden:
Zwei Regeln machen Referenzen sicher und vorhersehbar: Eine Referenz muss initialisiert werden, sobald sie deklariert wird (int& r; ist ein Compilerfehler), und sie kann danach nie umgehängt werden, um auf etwas anderes zu zeigen. Einer Referenz etwas zuzuweisen schreibt immer in das, woran sie ursprünglich gebunden wurde.
Übergabe per Referenz: lass eine Funktion zurückgreifen
Der eigentliche Gewinn liegt in den Funktionen. Setze & an einen Parameter, und die Funktion erhält statt einer Kopie einen Alias auf das Argument des Aufrufers. Jetzt sind Änderungen innerhalb der Funktion auch außerhalb sichtbar:
Lass das & weg, und addBonus würde eine Wegwerfkopie erhöhen, sodass total bei 100 bliebe. Dieses eine Zeichen macht den ganzen Unterschied. Das ist die kanonische Art, eine Funktion zu schreiben, die mehr als ein Ergebnis zurückgibt oder ihre Eingabe an Ort und Stelle ändert. Das klassische Beispiel ist das Vertauschen zweier Variablen:
Ohne Referenzen würde swapValues nur lokale Kopien vertauschen und x/y blieben 1 2. (Die Standardbibliothek hat bereits std::swap, aber es selbst zu schreiben zeigt genau, was ein Referenzparameter bringt.)
const-Referenzen: schnell lesen, versprechen nicht anzufassen
Die Übergabe per Referenz vermeidet ebenfalls das Kopieren – und bei einem großen Objekt kann diese Kopie teuer sein. Aber ein schlichter T&-Parameter signalisiert „ich könnte das ändern", was irreführend ist, wenn du nur lesen willst. Die Lösung ist const T&: Du bekommst die kopierfreie Geschwindigkeit einer Referenz und ein vom Compiler erzwungenes Versprechen, dass die Funktion das Argument nicht ändert.
Eine nicht-const-Referenz kann sich nur an eine veränderbare Variable binden, aber eine const-Referenz kann sich auch an Literale und temporäre Objekte binden – deshalb kompiliert greet("literal works too"). Eine praktische Faustregel für die Wahl eines Parametertyps:
void f(int x) // billiger Typ, nur lesen -> einfach kopieren
void f(const string& s) // schwerer Typ, nur lesen -> const-Referenz
void f(string& s) // du willst das Objekt des Aufrufers ändern
Verwende standardmäßig const T& für jeden Klassentyp, den du nur liest (string, vector, deine eigenen Structs), und reserviere eine nicht-const-Referenz für die Fälle, in denen du wirklich zurückschreiben willst.
Eine Referenz zurückgeben
Eine Funktion kann auch eine Referenz zurückgeben und dem Aufrufer einen Alias auf etwas bereits Bestehendes übergeben. Das ist in containerartigem Code üblich – es ist das, was v[i] = 5 ermöglicht und was operator[] unter der Haube tut:
Da at int& zurückgibt, ist der Aufrufausdruck at(data, 1) selbst ein Lvalue, dem du etwas zuweisen kannst. Gib stattdessen ein schlichtes int zurück, und at(data, 1) = 42 würde nicht kompilieren – du würdest einer temporären Kopie etwas zuweisen.
Die große Falle: hängende Referenzen
Eine Referenz besitzt nichts; sie zeigt nur auf Speicher, der anderswo lebt. Stirbt dieser Speicher, während die Referenz noch in Gebrauch ist, hast du eine hängende Referenz, und das Lesen darüber ist undefiniertes Verhalten – es kann Müll ausgeben, abstürzen oder scheinbar funktionieren, bis es dir in der Produktion den Tag ruiniert. Der klassische Fehler ist, eine Referenz auf eine lokale Variable zurückzugeben:
int& broken() {
int local = 42;
return local; // BUG: local wird zerstört, wenn broken() zurückkehrt
} // die zurückgegebene Referenz hängt in der Luft
int main() {
int& r = broken();
cout << r << "\n"; // UNDEFINIERTES VERHALTEN - liest toten Speicher
}
Die Variable local ist in dem Moment weg, in dem broken zurückkehrt, also zeigt die Referenz auf bereits freigegebenen Stack-Speicher. Gib nur eine Referenz auf etwas zurück, das den Aufruf überlebt – einen per Referenz übergebenen Parameter, ein Datenelement oder ein static. Wird der Wert innerhalb der Funktion berechnet, gib stattdessen per Wert zurück und lass den Compiler die Kopie wegoptimieren. Dieselbe Falle trifft bereichsbasierte Schleifen und jede an ein Temporäres gebundene Referenz: Halte eine Referenz niemals über die Lebensdauer des Dings hinaus, das sie benennt.
Als Nächstes: Funktionsüberladung
Referenzen geben dir einen zweiten Regler an jedem Parameter – Kopie vs. Alias, veränderbar vs. const – und dieser Regler wirkt direkt mit dem nächsten Thema zusammen. Als Nächstes erlaubt dir die Funktionsüberladung, mehrere Funktionen mit demselben Namen, aber unterschiedlichen Parameterlisten zu definieren, und der Compiler wählt die richtige aus, indem er die Argumenttypen abgleicht – einschließlich, ob sie per Wert, per Referenz oder per const-Referenz übergeben werden.
Häufig gestellte Fragen
Was ist eine Referenz in C++?
Eine Referenz ist ein Alias für eine bestehende Variable – ein zweiter Name für denselben Speicher. Man erzeugt sie mit & in der Deklaration: int& r = x;. Danach sind r und x austauschbar; eine zu ändern ändert die andere. Referenzen müssen bei der Deklaration initialisiert werden und können nie umgehängt werden, um auf eine andere Variable zu verweisen.
Was ist der Unterschied zwischen Übergabe per Wert und per Referenz in C++?
Die Übergabe per Wert (void f(int x)) kopiert das Argument, also arbeitet die Funktion auf ihrer eigenen Kopie und die Variable des Aufrufers bleibt unberührt. Die Übergabe per Referenz (void f(int& x)) gibt der Funktion direkten Zugriff auf die Variable des Aufrufers, sodass Änderungen nach dem Aufruf sichtbar sind – und es wird keine Kopie erstellt, was bei großen Objekten zählt.
Wann sollte ich in C++ const-Referenzparameter verwenden?
Verwende const T&, wenn eine Funktion einen Parameter nur lesen muss, der Typ aber teuer zu kopieren ist (string, vector, große Structs). Du bekommst die kopierfreie Geschwindigkeit einer Referenz plus eine vom Compiler garantierte Zusicherung, dass die Funktion den Wert des Aufrufers nicht ändert. Für billige Typen wie int oder double ist die schlichte Übergabe per Wert einfacher und genauso schnell.