C++-Programmierung/ Weitere Grundelemente/ Referenzen

Aus Wikibooks


Grundlagen zu Referenzen[Bearbeiten]

Referenzen sind interne Zeiger auf Variablen. Sie werden also genau so verwendet wie gewöhnliche Variablen, verweisen jedoch auf das Objekt, mit dem sie initialisiert wurden. Die Zeigerverwendung wird vor dem Programmierer verborgen.

int  a = 1;  // eine Variable
int &r = a;  // Referenz auf die Variable a

std::cout << "a: " << a << " r: " << r << std::endl;
++a;
std::cout << "a: " << a << " r: " << r << std::endl;
++r;
std::cout << "a: " << a << " r: " << r << std::endl;
Ausgabe:
a: 1 r: 1
a: 2 r: 2
a: 3 r: 3

Wie Sie im Beispiel sehen, sind a und r identisch. Gleiches können Sie natürlich auch mit einem Zeiger erreichen, auch wenn bei einem Zeiger die Syntax etwas anders ist als bei einer Referenz.

Im Beispiel wurde die Referenz auf int r, mit dem int a initialisiert. Beachten Sie, dass die Initialisierung einer Referenzvariablen nur beim Anlegen erfolgen kann, danach kann ihr Wert nur noch durch eine Zuweisung geändert werden. Daraus folgt, dass eine Referenz immer initialisiert werden muss und es nicht möglich ist, eine Referenzvariable auf ein neues Objekt verweisen zu lassen:

int  a = 10; // eine Variable
int  b = 20; // noch eine Variable
int &r = a;  // Referenz auf die Variable a

std::cout << "a: " << a << " b: " << b << " r: " << r << std::endl;
++a;
std::cout << "a: " << a << " b: " << b << " r: " << r << std::endl;
r = b;       // r zeigt weiterhin auf a, r (und somit a) wird 20 zugewiesen 
std::cout << "a: " << a << " b: " << b << " r: " << r << std::endl;
Ausgabe:
a: 10 b: 20 r: 10
a: 11 b: 20 r: 11
a: 20 b: 20 r: 20

Wie Sie sehen, ist es nicht möglich, r als Alias für b zu definieren, nachdem es einmal mit a initialisiert wurde. Die Zuweisung bewirkt genau das, was auch eine Zuweisung von b an a bewirkt hätte. Dass eine Referenz wirklich nichts weiter ist als ein Aliasname wird umso deutlicher, wenn man sich die Adressen der Variablen aus dem ersten Beispiel ansieht:

int  a = 1;  // eine Variable
int &r = a;  // Referenz auf die Variable a

std::cout << "a: " << &a << " r: " << &r << std::endl;
Ausgabe:
a: 0x7fffbf623c54 r: 0x7fffbf623c54

Die Ausgabe sieht bei Ihnen vermutlich etwas anders aus.

Wie Sie sehen, sind die Adressen identisch.

Anwendung von Referenzen[Bearbeiten]

Vielleicht haben Sie sich bereits gefragt, wofür Referenzen nun eigentlich gut sind, schließlich könnte man ja auch einfach die Originalvariable benutzen.

Selbstdefinierte Referenzen[Bearbeiten]

Referenzen bieten in einigen Anwendungsfällen eine Beschleunigung und bessere Lesbarkeit der Quelltexte. Sie müssen initialisiert werden.

#include <iostream>

int main(){
    unsigned int const x = 2, y = 3, z = 4;
    unsigned int zahlen_array[x][y][z] = {
        { { 0,  1,  2,  3}, { 4,  5,  6,  7}, { 8,  9, 10, 11} },
        { {12, 13, 14, 15}, {16, 17, 18, 19}, {20, 21, 22, 23} }
    };
    for(unsigned int a = 0; a < x; ++a){
        for(unsigned int b = 0; b < y; ++b){
            for(unsigned int c = 0; c < z; ++c){
                // ref wird als Referenz auf zahlen_array[a][b][c] initialisiert
                unsigned int& ref = zahlen_array[a][b][c];
                // entspricht 'zahlen_array[a][b][c] *= 2;'
                 ref *= 2;
                // entspricht 'zahlen_array[a][b][c] += a * b * c;'
                 ref += a * b * c;
                // entspricht 'std::cout << zahlen_array[a][b][c] << std::endl;'
                std::cout << ref << ", ";
            }
            std::cout << std::endl;
        }
        std::cout << std::endl;
    }
}
Ausgabe:
0, 2, 4, 6,
8, 10, 12, 14,
16, 18, 20, 22,

24, 26, 28, 30, 
32, 35, 38, 41, 
40, 44, 48, 52,

Bei mehrfacher Verwendung, kann eine Referenz Ihnen viel Tipparbeit ersparen und vor allem erhöht sie die Übersichtlichkeit des Quellcodes. Außerdem kann diese Vorgehensweise die Performance verbessern, da der Zugriff auf Daten in Klassen oder Feldern durch eine Referenzdefinition vereinfacht wird. Bei obigem Beispiel wird zur Laufzeit, im Arbeitsspeicher, bei jeder Verwendung von zahlen_array[a][b][c] zuerst der Speicherort der einzelnen Zahl berechnet, dabei müssen die Inhalts-, Feldgrößen und Offsets innerhalb des Felds berücksichtigt werden. An anderen Stellen mag dies alles mit STL-Containerklassen und verschachtelten Methoden- und Operatoraufrufen erfolgen.

Dies alles können Sie dem Prozessor nicht ersparen. Sie können aber dafür sorgen, dass es, pro Schritt, nur einmal vorkommt. Die Verwendung einer Referenz ergibt daher Sinn, sobald Sie zahlen_array[a][b][c] mehr als einmal verwenden. Eine Referenz ist intern meist mit einem Zeiger implementiert. Es sei allerdings darauf hingewiesen, dass der Compiler in vielen Fällen diese Optimierung auch selbst vornehmen kann, daher ist ein Performancegewinn nicht zwingend vorhanden.

Call-By-Reference[Bearbeiten]

Möglicherweise erinnern Sie sich aber auch noch, dass im Kapitel „Prozeduren und Funktionen“ die Wertübergabe als Referenz (call-by-reference) vorgestellt wurde. Darauf wird nun genauer eingegangen.

Referenzen bieten genau wie Zeiger die Möglichkeit, den Wert einer Variable außerhalb der Funktion zu ändern. Im Folgenden sehen Sie die im Kapitel über Zeiger vorgestellte Funktion swap() mit Referenzen:

#include <iostream>

void swap(int &wert1, int &wert2) {
    int tmp;
    tmp   = wert1;
    wert1 = wert2;
    wert2 = tmp;
}

int main() {
    int a = 7, b = 9;

    std::cout << "a: " << a << ", b: " << b << "\n";

    swap(a, b);

    std::cout << "a: " << a << ", b: " << b << "\n";
    return (0);
}
Ausgabe:
a: 7, b: 9
a: 9, b: 7

Diese Funktion bietet gegenüber der Zeigervariante zwei Vorteile. Die Syntax ist einfacher und es ist nicht möglich, so etwas wie einen Nullzeiger zu übergeben. Um diese Funktion zum Absturz zu bewegen, ist schon einige Mühe nötig.

const-Referenzen[Bearbeiten]

Referenzen auf konstante Variablen spielen in C++ eine besondere Rolle. Eine Funktion, die eine Variable übernimmt, kann genauso gut auch eine Referenz auf eine konstante Variable übernehmen. Folgendes Beispiel soll dies demonstrieren:

#include <iostream>

void ausgabe1(int wert) {
    std::cout << "wert: " << wert << "\n";
}

void ausgabe2(int const &wert) {
    std::cout << "wert: " << wert << "\n";
}

int main() {
    ausgabe1(5);
    ausgabe2(5);
    return (0);
}
Ausgabe:
ausgabe1 wert: 5
ausgabe2 wert: 5

Die beiden Ausgabefunktionen sind an sich identisch, lediglich die Art der Parameterübergabe unterscheidet sich. ausgabe1() übernimmt einen int, ausgabe2() eine Referenz auf einen konstanten int. Beide Funktionen lassen sich auch vollkommen identisch aufrufen. Würde ausgabe2() eine Referenz auf einen nicht-konstanten int übernehmen, wäre ein Aufruf mit einer Konstanten, wie dem int-Literal 5 nicht möglich.

In Verbindung mit Klassenobjekten ist die Übergabe als Referenz auf ein konstantes Objekt sehr viel schneller, dazu erfahren Sie aber zu gegebener Zeit mehr. Für die Ihnen bereits bekannten Basisdatentypen ist tatsächlich die Übergabe als Wert effizienter.

Referenzen als Rückgabetyp[Bearbeiten]

Referenzen haben als Rückgabewert die gleichen Vorteile wie bei der Wertübergabe. Allerdings sind sie in diesem Zusammenhang wesentlich gefährlicher. Es kann schnell passieren, dass Sie versehentlich eine Referenz auf eine lokale Variable zurückgeben. Diese Variable ist außerhalb der Funktion allerdings nicht mehr gültig, daher ist das Resultat, wenn Sie außerhalb der Funktion darauf zugreifen, undefiniert. Aus diesem Grund sollten Sie Referenzen als Rückgabewert nur verwenden wenn Sie wirklich wissen, was Sie tun.

#include <iostream>

// gibt die Referenz des Parameters x zurück
int &zahl(int &x) {
    return x;
}

int main() {
    int y = 3;
    zahl(y) = 5;
    std::cout << "wert: " << y; 
    return y;
}
Ausgabe:
wert: 5