vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


Woche 2

Tag 9



Referenzen

Gestern haben Sie gelernt, wie man Zeiger verwendet, Objekte im Heap manipuliert und auf Objekte indirekt verweist. Referenzen bieten nahezu die gleichen Möglichkeiten wie Zeiger, aber mit einer wesentlich einfacheren Syntax. Heute lernen Sie,

Was ist eine Referenz?

Eine Referenz ist ein Alias-Name. Wenn man eine Referenz erzeugt, initialisiert man sie mit dem Namen eines anderen Objekts, dem Ziel. Von diesem Moment an ist die Referenz wie ein alternativer Name für das Ziel, und alles, was man mit der Referenz anstellt, bezieht sich tatsächlich auf das Ziel.

Die Deklaration einer Referenz besteht aus dem Typ des Zielobjekts, gefolgt vom Referenzoperator (&) und dem Namen der Referenz. Die Regeln für die Benennung von Referenzen sind die gleichen wie für Variablennamen. Viele Programmierer stellen ihren Referenzen ein vorangestelltes r. Zum Beispiel erzeugt man für eine Integer-Variable einInt eine Referenz mit der folgenden Anweisung:

int &rEineRef = einInt;

Man liest das als »rEineRef ist eine Referenz auf einen int-Wert, die mit einem Verweis auf einInt initialisiert ist«. Listing 9.1 zeigt, wie man Referenzen erzeugt und verwendet.

Beachten Sie, daß C++ für den Referenzoperator (&) dasselbe Symbol verwendet wie für den Adreßoperator. Dabei handelt es sich nicht um ein und denselben Operator, wenn die beiden auch verwandt sind.

Listing 9.1: Referenzen erzeugen und verwenden

1:    // Listing 9.1
2: // Zeigt die Verwendung von Referenzen
3:
4: #include <iostream.h>
5:
6: int main()
7: {
8: int intOne;
9: int &rSomeRef = intOne;
10:
11: intOne = 5;
12: cout << "intOne: " << intOne << endl;
13: cout << "rSomeRef: " << rSomeRef << endl;
14:
15: rSomeRef = 7;
16: cout << "intOne: " << intOne << endl;
17: cout << "rSomeRef: " << rSomeRef << endl;
18: return 0;
19: }

intOne: 5
rSomeRef: 5
intOne: 7
rSomeRef: 7

Zeile 8 deklariert die lokale int-Variable intOne. In Zeile 9 wird rSomeRef als Referenz auf int deklariert und mit intOne initialisiert. Wenn man eine Referenz deklariert, aber nicht initialisiert, erhält man einen Compiler-Fehler. Referenzen müssen initialisiert werden.

Zeile 11 weist intOne den Wert 5 zu. Die Anweisungen in den Zeilen 12 und 13 geben die Werte in intOne und rSomeRef aus. Natürlich sind sie gleich, da rSomeRef lediglich die Referenz auf intOne ist.

In Zeile 15 steht die Zuweisung von 7 an rSomeRef. Da es sich um eine Referenz handelt, eine Alias-Adresse für intOne, bezieht sich die Zuweisung von 7 auf intOne, wie es die Ausgaben in den Zeilen 16 und 17 belegen.

Der Adreßoperator bei Referenzen

Wenn man die Adresse einer Referenz abfragt, erhält man die Adresse des Ziels der Referenz. Genau das ist das Wesen der Referenzen - sie sind Alias-Adressen für das Ziel. Listing 9.2 verdeutlicht diesen Sachverhalt.

Listing 9.2: Die Adresse einer Referenz ermitteln

1:    // Listing 9.2
2: // Zeigt die Verwendung von Referenzen
3:
4: #include <iostream.h>
5:
6: int main()
7: {
8: int intOne;
9: int &rSomeRef = intOne;
10:
11: intOne = 5;
12: cout << "intOne: " << intOne << endl;
13: cout << "rSomeRef: " << rSomeRef << endl;
14:
15: cout << "&intOne: " << &intOne << endl;
16: cout << "&rSomeRef: " << &rSomeRef << endl;
17:
18: return 0;
19: }

intOne: 5
rSomeRef: 5
&intOne: 0x3500
&rSomeRef: 0x3500

(Ihre Ausgabe kann in den letzten beiden Zeilen abweichen.)

Auch hier wird rSomeRef als Referenz auf intOne initialisiert. Die Ausgabe zeigt diesmal die Adressen der beiden Variablen - sie sind identisch. C++ bietet keine Möglichkeit, auf die Adresse der Referenz selbst zuzugreifen, da sie im Gegensatz zu einem Zeiger oder einer anderen Variablen nicht von Bedeutung ist. Referenzen werden bei ihrer Erzeugung initialisiert und agieren immer als Synonyme für ihre Ziele, selbst wenn man den Adreßoperator anwendet.

Für eine Klasse, wie zum Beispiel City, kann man eine Instanz dieser Klasse wie folgt deklarieren:

City  boston;

Danach können Sie eine Referenz auf City deklarieren und mit diesem Objekt initialisieren:

City &beanTown = boston;

Es gibt nur ein City-Objekt; beide Bezeichner beziehen sich auf dasselbe Objekt derselben Klasse. Alle Aktionen, die man auf beanTown ausführt, werden genauso auf boston ausgeführt.

Achten Sie auf den Unterschied zwischen dem Symbol & in Zeile 9 von Listing 9.2, das eine Referenz auf int namens rSomeRef deklariert, und den &-Symbolen in den Zeilen 15 und 16, die die Adressen der Integer-Variablen intOne und der Referenz rSomeRef zurückgeben.

Bei Referenzen arbeitet man normalerweise nicht mit dem Adreßoperator. Man setzt die Referenz einfach so ein, als würde man direkt mit der Zielvariablen arbeiten. Zeile 13 zeigt dazu ein Beispiel.

Referenzen können nicht erneut zugewiesen werden

Selbst erfahrene C++-Programmierer, die die Regel kennen, daß Referenzen nicht erneut zugewiesen werden können und immer Alias-Adressen für ihr Ziel sind, wissen manchmal nicht, was beim erneuten Zuweisen einer Referenz passiert. Was wie eine Neuzuweisung aussieht, stellt sich als Zuweisung eines neuen Wertes an das Ziel heraus. Diese Tatsache belegt Listing 9.3.

Listing 9.3: Zuweisungen an eine Referenz

1:     // Listing 9.3
2: // Neuzuweisung einer Referenz
3:
4: #include <iostream.h>
5:
6: int main()
7: {
8: int intOne;
9: int &rSomeRef = intOne;
10:
11: intOne = 5;
12: cout << "intOne:\t" << intOne << endl;
13: cout << "rSomeRef:\t" << rSomeRef << endl;
14: cout << "&intOne:\t" << &intOne << endl;
15: cout << "&rSomeRef:\t" << &rSomeRef << endl;
16:
17: int intTwo = 8;
18: rSomeRef = intTwo;
19: cout << "\nintOne:\t" << intOne << endl;
20: cout << "intTwo:\t" << intTwo << endl;
21: cout << "rSomeRef:\t" << rSomeRef << endl;
22: cout << "&intOne:\t" << &intOne << endl;
23: cout << "&intTwo:\t" << &intTwo << endl;
24: cout << "&rSomeRef:\t" << &rSomeRef << endl;
25: return 0;
26: }

intOne:                 5
rSomeRef: 5
&intOne: 0x213e
&rSomeRef: 0x213e

intOne: 8
intTwo: 8
rSomeRef: 8
&intOne: 0x213e
&intTwo: 0x2130
&rSomeRef: 0x213e

Die Zeilen 8 und 9 deklarieren auch hier wieder eine Integer-Variable und eine Referenz auf int. In Zeile 11 wird der Integer-Variable der Wert 5 zugewiesen, und die Ausgabe der Werte und ihrer Adressen erfolgt in den Zeilen 12 bis 15.

Zeile 17 erzeugt die neue Variable intTwo und initialisiert sie mit dem Wert 8. In Zeile 18 versucht der Programmierer, rSomeRef erneut als Alias-Adresse für die Variable intTwo zuzuweisen. Allerdings passiert etwas anderes: rSomeRef wirkt nämlich weiterhin als Alias-Adresse für intOne, so daß diese Zuweisung mit der folgenden gleichbedeutend ist:

intOne = intTwo;

Tatsächlich sind die in den Zeilen 19 bis 21 ausgegebenen Werte von intOne und rSomeRef gleich intTwo. Die Ausgabe der Adressen in den Zeilen 22 bis 24 beweist, daß sich rSomeRef weiterhin auf intOne und nicht auf intTwo bezieht.

Was Sie tun sollten

... und was nicht

Verwenden Sie Referenzen, um eine Alias-Adresse auf ein Objekt zu erzeugen.

Initialisieren Sie alle Referenzen.

Versuchen Sie nicht, eine Referenz erneut zuzuweisen.

Verwechseln Sie nicht den Adreßoperator mit dem Referenzoperator.

Was kann man referenzieren?

Alle Objekte, einschließlich der benutzerdefinierten Objekte, lassen sich referenzieren. Beachten Sie, daß man eine Referenz auf ein Objekt und nicht auf eine Klasse erzeugt. Beispielsweise schreibt man nicht:

int & rIntRef = int;    // falsch

Man muß rIntRef mit einem bestimmten Integer-Objekt initialisieren, etwa wie folgt:

int wieGross = 200;
int & rIntRef = wieGross;

Auch die Initialisierung mit einer Klasse CAT funktioniert nicht:

CAT & rCatRef = CAT;   // falsch

rCatRef muß mit einem bestimmten CAT-Objekt initialisieren:

CAT Frisky;
CAT & rCatRef = Frisky;

Referenzen auf Objekte verwendet man genau wie das Objekt selbst. Auf Datenelemente und Methoden greift man mit dem normalen Zugriffsoperator (.) zu, und wie für die vordefinierten Typen wirkt die Referenz als Alias-Adresse für das Objekt (siehe Listing 9.4).

Listing 9.4: Referenzen auf Objekte

1:    // Listing 9.4
2: // Referenzen auf Klassenobjekte
3:
4: #include <iostream.h>
5:
6: class SimpleCat
7: {
8: public:
9: SimpleCat (int age, int weight);
10: ~SimpleCat() {}
11: int GetAge() { return itsAge; }
12: int GetWeight() { return itsWeight; }
13: private:
14: int itsAge;
15: int itsWeight;
16: };
17:
18: SimpleCat::SimpleCat(int age, int weight)
19: {
20: itsAge = age;
21: itsWeight = weight;
22: }
23:
24: int main()
25: {
26: SimpleCat Frisky(5,8);
27: SimpleCat & rCat = Frisky;
28:
29: cout << "Frisky ist: ";
30: cout << Frisky.GetAge() << " Jahre alt. \n";
31: cout << "Frisky wiegt: ";
32: cout << rCat.GetWeight() << " Pfund. \n";
33: return 0;
34: }

Frisky ist: 5 Jahre alt.
Frisky wiegt 8 Pfund.

Zeile 26 deklariert das SimpleCat-Objekt Frisky. In Zeile 27 wird eine Referenz auf SimpleCat, rCat deklariert und mit Frisky initialisiert. Die Zeilen 30 und 32 greifen auf die Zugriffsmethoden von SimpleCat zu, wobei zuerst das SimpleCat-Objekt und dann die SimpleCat-Referenz verwendet wird. Der Zugriff erfolgt absolut identisch. Auch hier gilt, daß die Referenz eine Alias-Adresse für das eigentliche Objekt ist.

Referenzen

Die Deklaration einer Referenz besteht aus dem Typ, gefolgt von dem Referenzoperator (&) und dem Referenznamen. Referenzen müssen bei ihrer Erzeugung initialisiert werden.

Beispiel 1:

     int seinAlter;
int &rAlter = seinAlter;

Beispiel 2:

     CAT boots;
CAT &rCatRef = boots;

Null-Zeiger und Null-Referenzen

Wenn man Zeiger löscht oder nicht initialisiert, sollte man ihnen Null (0) zuweisen. Für Referenzen gilt das nicht. In der Tat darf eine Referenz nicht Null sein, und ein Programm mit einer Referenz auf ein Null-Objekt ist unzulässig. Bei einem unzulässigen Programm kann nahezu alles passieren. Vielleicht läuft das Programm, vielleicht löscht es aber auch alle Dateien auf der Festplatte.

Die meisten Compiler unterstützen Null-Objekte, ohne sich darüber zu beschweren. Erst wenn man das Objekt verwendet, gibt es Ärger. Allerdings sollte man auf diese »Unterstützung« verzichten. Denn wenn man das Programm auf einer anderen Maschine oder mit einem anderen Compiler laufen läßt, können sich bei vorhandenen Null-Objekten merkwürdige Fehler einschleichen.

Funktionsargumente als Referenz übergeben

In Kapitel 5, »Funktionen«, haben Sie gelernt, daß Funktionen zwei Einschränkungen aufweisen: Die Übergabe von Argumenten erfolgt als Wert, und die return-Anweisung kann nur einen einzigen Wert zurückgeben.

Die Übergabe von Werten an eine Funktion als Referenz hebt beide Einschränkungen auf. In C++ realisiert man die Übergabe als Referenz entweder mit Zeigern oder mit Referenzen. Beachten Sie die unterschiedliche Verwendung des Begriffs »Referenz«: Die Übergabe als Referenz erfolgt entweder durch einen Zeiger oder durch eine Referenz.

Die Syntax ist unterschiedlich, die Wirkung gleich: Die Funktion legt in ihrem Gültigkeitsbereich keine Kopie an, sondern greift auf das Originalobjekt zu.

In Kapitel 5 haben Sie gelernt, daß Funktionen Ihre übergebenen Parameter auf dem Stack ablegen. Wird einer Funktion ein Wert als Referenz übergeben (entweder mittels Zeiger oder mittels Referenz), wird die Adresse des Objekts und nicht das Objekt selbst auf dem Stack abgelegt.

Um genau zu sein, wird auf manchen Computern die Adresse vielmehr in einem Register verzeichnet, festgehalten und nicht auf dem Stack abgelegt. Auf jedem Fall weiß der Compiler damit, wie er auf das originale Objekt zugreift, um die Änderungen dort und nicht an der Kopie vorzunehmen.

Die Übergabe eines Objekts als Referenz ermöglicht es der Funktion, das betreffende Objekt zu verändern.

Zur Erinnerung möchte ich auf das Listing 5.5 in Kapitel 5 verweisen, in dem der Aufruf der Funktion swap() keinen Einfluß auf die Werte hatte. Das Listing 5.5 wird mit diesem Listing 9.5 wieder aufgegriffen.

Listing 9.5: Übergabe von Argumenten als Wert

1:     // Listing 9.5 Zeigt die Uebergabe als Wert
2:
3: #include <iostream.h>
4:
5: void swap(int x, int y);
6:
7: int main()
8: {
9: int x = 5, y = 10;
10:
11: cout << "Main. Vor Vertauschung, x: " << x << " y: " << y << "\n";
12: swap(x,y);
13: cout << "Main. Nach Vertauschung, x: " << x << " y: " << y << "\n";
14: return 0;
15: }
16:
17: void swap (int x, int y)
18: {
19: int temp;
20:
21: cout << "Swap. Vor Vertauschung, x: " << x << " y: " << y << "\n";
22:
23: temp = x;
24: x = y;
25: y = temp;
26:
27: cout << "Swap. Nach Vertauschung, x: " << x << " y: " << y << "\n";
28:
29: }

Main. Vor Vertauschung, x: 5 y: 10
Swap. Vor Vertauschung, x: 5 y: 10
Swap. Nach Vertauschung, x: 10 y: 5
Main. Nach Vertauschung, x: 5 y: 10

Dieses Programm initialisiert zwei Variablen in main() und übergibt sie dann an die Funktion swap(), die auf den ersten Blick eine Vertauschung der Werte vornimmt. Inspiziert man aber die Werte erneut in main(), haben sie ihre Plätze nicht gewechselt!

Das Problem besteht hier darin, daß die Übergabe von x und y an die Funktion swap() als Wert erfolgt. Das heißt, die Funktion legt lokale Kopien dieser Variablen an. Man müßte also x und y als Referenz übergeben.

In C++ bieten sich hier zwei Möglichkeiten: Man kann die Parameter der Funktion swap() als Zeiger auf die Originalwerte ausbilden oder Referenzen auf die Originalwerte übergeben.

Parameter mit Zeigern übergeben

Die Übergabe eines Zeigers bedeutet, daß man die Adresse des Objekts übergibt. Daher kann die Funktion den Wert an dieser Adresse manipulieren. Damit swap() die ursprünglichen Werte mit Hilfe von Zeigern vertauscht, deklariert man die Parameter der Funktion swap() als zwei int-Zeiger. Die Funktion dereferenziert diese beiden Zeiger und vertauscht damit wie beabsichtigt die Werte von x und y. Listing 9.6 verdeutlicht dieses Konzept.

Listing 9.6: Übergabe als Referenz mit Zeigern

1:     // Listing 9.6 Demonstriert die Uebergabe als Referenz
2:
3: #include <iostream.h>
4:
5: void swap(int *x, int *y);
6:
7: int main()
8: {
9: int x = 5, y = 10;
10:
11: cout << "Main. Vor Vertauschung, x: " << x << " y: " << y << "\n";
12: swap(&x,&y);
13: cout << "Main. Nach Vertauschung, x: " << x << " y: " << y << "\n";
14: return 0;
15: }
16:
17: void swap (int *px, int *py)
18: {
19: int temp;
20:
21: cout << "Swap. Vor Vertauschung, *px: " << *px << " *py: " << *py
<< "\n";
22:
23: temp = *px;
24: *px = *py;
25: *py = temp;
26:
27: cout << "Swap. Nach Vertauschung, *px: " << *px << " *py: " << *py
<< "\n";
28:
29: }

Main. Vor Vertauschung, x: 5 y: 10
Swap. Vor Vertauschung, *px: 5 *py: 10
Swap. Nach Vertauschung, *px: 10 *py: 5
Main. Nach Vertauschung, x: 10 y: 5

Erfolg gehabt! Der geänderte Prototyp von swap() in Zeile 5 zeigt nun an, daß die beiden Parameter als Zeiger auf int und nicht als int-Variablen spezifiziert sind. Der Aufruf von swap() in Zeile 12 übergibt als Argumente die Adressen von x und y.

Die Funktion swap() deklariert in Zeile 19 die lokale Variable temp. Diese Variable braucht kein Zeiger zu sein, sie nimmt einfach den Wert von *px (das heißt, den Wert von x in der aufrufenden Funktion) während der Lebensdauer der Funktion auf. Nachdem die Funktion zurückgekehrt ist, wird temp nicht mehr benötigt.

Zeile 23 weist temp den Wert von px zu. In Zeile 24 erhält px den Wert von py. In Zeile 25 wird der in temp zwischengespeicherte Wert (das heißt, der Originalwert von px) nach py übertragen.

In der aufrufenden Funktion sind die Werte, die als Adressen an swap() übergeben wurden, nun tatsächlich vertauscht.

Parameter mit Referenzen übergeben

Das obige Programm funktioniert zwar, die Syntax der Funktion swap() ist aber in zweierlei Hinsicht umständlich. Erstens wird der Code der Funktion durch die erforderliche Dereferenzierung der Zeiger komplizierter und damit auch fehleranfälliger. Zweitens läßt die erforderliche Übergabe von Variablenadressen Rückschlüsse auf die inneren Abläufe von swap() zu, was vielleicht nicht gewünscht ist.

Eines der Ziele von C++ ist es, daß sich der Benutzer mit der Arbeitsweise von Funktionen nicht zu belasten braucht. Bei der Übergabe von Zeigern trägt die aufrufende Funktion die Verantwortung, die eigentlich von der aufgerufenen Funktion übernommen werden sollte. Listing 9.7 zeigt eine Neufassung der Funktion swap(), bei der Referenzen übergeben werden.

Listing 9.7: Die Funktion swap mit Referenzen als Parametern

1:     // Listing 9.7 Demonstriert die Parameterübergabe
2: // mit Referenzen
3:
4: #include <iostream.h>
5:
6: void swap(int &x, int &y);
7:
8: int main()
9: {
10: int x = 5, y = 10;
11:
12: cout << "Main. Vor Vertauschung, x: " << x
<< " y: " << y << "\n";
13: swap(x,y);
14: cout << "Main. Nach Vertauschung, x: " << x
<< " y: " << y << "\n";
15: return 0;
16: }
17:
18: void swap (int &rx, int &ry)
19: {
20: int temp;
21:
22: cout << "Swap. Vor Vertauschung, rx: " << rx
<< " ry: " << ry << "\n";
23:
24: temp = rx;
25: rx = ry;
26: ry = temp;
27:
28: cout << "Swap. Nach Vertauschung, rx: " << rx
<< " ry: " << ry << "\n";
29:
30: }

Main. Vor Vertauschung, x:5 y: 10
Swap. Vor Vertauschung, rx:5 ry:10
Swap. Nach Vertauschung, rx:10 ry:5
Main. Nach Vertauschung, x:10 y:5

Genau wie im Zeigerbeispiel deklariert dieses Programm in Zeile 10 zwei Variablen und gibt deren Werte in Zeile 12 aus. Der Aufruf der Funktion swap() erfolgt in Zeile 13. Dieses Mal übergibt aber die aufrufende Funktion einfach die Variablen x und y und nicht deren Adressen.

Beim Aufruf von swap() springt die Programmausführung in Zeile 18, wo die Variablen als Referenzen identifiziert werden. Für die Ausgabe der Werte in Zeile 22 sind keine speziellen Operatoren erforderlich. Es handelt sich um die Alias-Adressen für die Originalwerte, die man unverändert einsetzen kann.

Die Vertauschung der Werte geschieht in den Zeilen 24 bis 26, die Ausgabe in Zeile 28. Dann kehrt die Programmausführung zurück zur aufrufenden Funktion. In Zeile 14 gibt main() die Werte aus. Da die Parameter von swap() als Referenzen deklariert sind, werden die Werte aus main() als Referenz übergeben und sind danach in main() ebenfalls vertauscht.

Referenzen lassen sich genauso komfortabel und einfach wie normale Variablen verwenden, sind aber leistungsfähig als Zeiger und erlauben die Übergabe als Referenz.

Header und Prototypen von Funktionen

In Listing 9.6 verwendet die Funktion swap() Zeiger für die Parameterübergabe, in Listing 9.7 verwendet die Funktion Referenzen. Funktionen mit Referenzen als Parameter lassen sich einfacher handhaben, und der Code ist verständlicher. Woher weiß aber die aufrufende Funktion, ob die Übergabe als Referenz oder als Wert stattfindet? Als Klient (oder Benutzer) von swap() muß der Programmierer erkennen können, ob swap() tatsächlich die Parameter ändert bzw. vertauscht.

Dies ist eine weitere Einsatzmöglichkeit für den Funktionsprototyp. Normalerweise sind die Prototypen in einer Header-Datei zusammengefaßt. Die im Prototyp deklarierten Parameter verraten dem Programmierer, daß die an swap() übergebenen Werte als Referenz übergeben und demnach ordnungsgemäß vertauscht werden.

Handelt es sich bei swap() um eine Elementfunktion einer Klasse, lassen sich diese Informationen aus der - ebenfalls in einer Header-Datei untergebrachten - Klassendeklaration ablesen.

In C++ stützen sich die Klienten von Klassen und Funktionen auf die Header-Datei, um alle erforderlichen Angaben zu erhalten. Die Header-Datei wirkt als Schnittstelle zur Klasse oder Funktion. Die eigentliche Implementierung bleibt dem Klienten verborgen. Damit kann sich der Programmierer auf das unmittelbare Problem konzentrieren und die Klasse oder Funktion einsetzen, ohne sich um deren Arbeitsweise kümmern zu müssen.

Als Colonel John Roebling die Brooklyn-Brücke konstruierte, galt seine Sorge auch dem Gießen des Betons und der Herstellung des Bewehrungsstahls. Er war genauestens über die mechanischen und chemischen Verfahren zur Herstellung seiner Baumaterialien informiert. Heutzutage nutzen Ingenieure ihre Zeit besser und verwenden erprobte Materialien, ohne Gedanken an den Herstellungsprozeß zu verschwenden.

In C++ ist es das Ziel, dem Programmierer erprobte Klassen und Funktionen an die Hand zu geben, deren innere Abläufe nicht bekannt sein müssen. Diese »Bausteine« können zu einem Programm zusammengesetzt werden, wie man auch Seile, Rohre, Klammern und andere Teile zu Brücken und Gebäuden zusammensetzen kann.

Genauso wie ein Ingenieur die Spezifikationen einen Rohrs studiert, um Belastbarkeit, Volumen, Anschlußmaße etc. zu ermitteln, liest ein C++-Programmierer die Schnittstelle einer Funktion oder Klasse, um festzustellen, welche Aufgaben sie ausführt, welche Parameter sie übernimmt und welche Werte sie zurückliefert.

Mehrere Werte zurückgeben

Wie bereits erwähnt, können Funktionen nur einen Wert zurückgeben. Was macht man nun, wenn zwei Werte aus einer Funktion zurückzugeben sind? Eine Möglichkeit zur Lösung dieses Problems besteht in der Übergabe von zwei Objekten an die Funktion, und zwar als Referenz. Die Funktion kann dann die Objekte mit den korrekten Werten füllen. Da eine Funktion bei Übergabe als Referenz die Originalobjekte ändern kann, lassen sich mit der Funktion praktisch zwei Informationsteile zurückgeben. Diese Lösung umgeht den Rückgabewert der Funktion, den man am besten für die Meldung von Fehlern vorsieht.

Dabei kann man sowohl mit Referenzen als auch mit Zeigern arbeiten. Listing 9.8 zeigt eine Funktion, die drei Werte zurückgibt, zwei als Zeigerparameter und einen als Rückgabewert der Funktion.

Listing 9.8: Rückgabe von Werten mit Zeigern

1:     // Listing 9.8
2: // Mehrere Werte aus einer Funktion zurückgeben
3:
4: #include <iostream.h>
5: int
6: short Factor(int n, int* pSquared, int* pCubed);
7:
8: int main()
9: {
10: int number, squared, cubed;
11: short error;
12:
13: cout << "Bitte eine Zahl eingeben (0 - 20): ";
14: cin >> number;
15:
16: error = Factor(number, &squared, &cubed);
17:
18: if (!error)
19: {
20: cout << "Zahl: " << number << "\n";
21: cout << "Quadrat: " << squared << "\n";
22: cout << "Dritte Potenz: " << cubed << "\n";
23: }
24: else
25: cout << "Fehler!!\n";
26: return 0;
27: }
28:
29: short Factor(int n, int *pSquared, int *pCubed)
30: {
31: short Value = 0;
32: if (n > 20)
33: Value = 1;
34: else
35: {
36: *pSquared = n*n;
37: *pCubed = n*n*n;
38: Value = 0;
39: }
40: return Value;
41: }

Bitte eine Zahl eingeben (0-20): 3
Zahl: 3
Quadrat: 9
Dritte Potenz: 27

Zeile 10 definiert number, squared und cubed als USHORTs. Die Variable number nimmt die vom Anwender eingegebene Zahl auf. Diese wird zusammen mit den Adressen von squared und cubed an die Funktion Factor() übergeben.

Factor() testet den ersten - als Wert übergebenen - Parameter. Ist er größer als 20 (der Maximalwert, den diese Funktion behandeln kann), setzt die Funktion die Variable Value auf einen Fehlerwert. Beachten Sie, daß der Rückgabewert aus Factor entweder für diesen Fehlerwert oder den Wert 0 reserviert ist, wobei 0 die ordnungsgemäße Funktionsausführung anzeigt. Die Rückgabe des entsprechenden Wertes findet in Zeile 40 statt.

Die eigentlich benötigten Werte, das Quadrat und die dritte Potenz von number, liefert die Funktion nicht über den normalen Rückgabemechanismus, sondern durch Ändern der an die Funktion übergebenen Zeiger.

In den Zeilen 36 und 37 erfolgt die Zuweisung der Rückgabewerte an die Zeiger. Zeile 38 setzt Value auf den Wert für erfolgreiche Ausführung (0), und Zeile 39 gibt Value zurück.

Als Verbesserung dieses Programms könnte man folgendes deklarieren:

enum ERROR_VALUE { SUCCESS, FAILURE};

Dann gibt man nicht 0 oder 1 zurück, sondern SUCCESS (alles OK) oder FAILURE (fehlerhafte Ausführung).

Werte als Referenz zurückgeben

Das Programm in Listing 9.8 funktioniert zwar, läßt sich aber mit der Übergabe von Referenzen anstelle von Zeigern wartungsfreundlicher und übersichtlicher gestalten. Listing 9.9 zeigt das gleiche Programm, allerdings in der neuen Fassung mit Übergabe von Referenzen und Rückgabe eines Aufzählungstyps für den Fehlerwert (ERROR).

Listing 9.9: Neufassung von Listing 9.8 mit Übergabe von Referenzen

1:      // Listing 9.9
2: // Rueckgabe mehrerer Werte aus einer Funktion
3: // mit Referenzen
4:
5: #include <iostream.h>
6:
7: typedef unsigned short USHORT;
8: enum ERR_CODE { SUCCESS, ERROR };
9:
10: ERR_CODE Factor(USHORT, USHORT&, USHORT&);
11:
12: int main()
13: {
14: USHORT number, squared, cubed;
15: ERR_CODE result;
16:
17: cout << "Bitte eine Zahl eingeben (0 - 20): ";
18: cin >> number;
19:
20: result = Factor(number, squared, cubed);
21:
22: if (result == SUCCESS)
23: {
24: cout << "Zahl: " << number << "\n";
25: cout << "Quadrat: " << squared << "\n";
26: cout << "Dritte Potenz: " << cubed << "\n";
27: }
28: else
29: cout << "Fehler!!\n";
30: return 0;
31: }
32:
33: ERR_CODE Factor(USHORT n, USHORT &rSquared, USHORT &rCubed)
34: {
35: if (n > 20)
36: return ERROR; // Einfacher Fehlercode
37: else
38: {
39: rSquared = n*n;
40: rCubed = n*n*n;
41: return SUCCESS;
42: }
43: }

Bitte eine Zahl eingeben (0-20): 3
Zahl: 3
Quadrat: 9
Dritte Potenz: 27

Listing 9.9 ist mit Listing 9.8 bis auf zwei Ausnahmen identisch. Der Aufzählungstyp ERR_CODE erlaubt es, die Fehlermeldungen in den Zeilen 36 und 41 sowie die Fehlerbehandlung in Zeile 22 komfortabler zu schreiben.

Die größere Änderung besteht allerdings darin, daß Factor nun für die Übernahme von Referenzen statt Zeigern auf squared und cubed ausgelegt ist. Die Arbeit mit diesen Parametern gestaltet sich damit einfacher und ist verständlicher.

Übergabe als Referenz der Effizienz wegen

Übergibt man ein Objekt an eine Funktion als Wert, legt die Funktion eine Kopie des Objekts an. Bei der Rückgabe eines Objekts aus einer Funktion als Wert wird eine weitere Kopie erstellt.

In Kapitel 5 haben Sie gelernt, daß diese Objekte auf den Stack kopiert werden. Das ist jedoch zeit- und speicherintensiv. Für kleine Objekte, wie zum Beispiel Integer-Werte, ist der Aufwand allerdings vernachlässigbar.

Bei größeren, benutzerdefinierten Objekten machen sich die Kopiervogänge deutlich bemerkbar. Die Größe eines benutzerdefinierten Objekts auf dem Stack ergibt sich aus der Summe seiner Elementvariablen. Bei diesen kann es sich wiederum um benutzerdefinierte Objekte handeln, und die Übergabe einer derartig massiven Struktur als Kopie geht sehr zu Lasten der Leistung und des Speichers.

Andere Faktoren kommen noch hinzu. Für die von Ihnen erzeugten Klassen werden diese temporären Kopien durch Aufruf eines speziellen Konstruktors, des Kopierkonstruktors, angelegt. Morgen werden Sie erfahren, wie Kopierkonstruktoren arbeiten und wie man eigene erzeugt. Momentan reicht es uns zu wissen, daß der Kopierkonstruktor jedes Mal aufgerufen wird, wenn eine temporäre Kopie des Objekts auf dem Stack angelegt wird.

Bei Rückkehr der Funktion wird das temporäre Objekt zerstört und der Destruktor des Objekts aufgerufen. Wenn man ein Objekt als Wert zurückgibt, muß eine Kopie dieses Objekts angelegt und auch wieder zerstört werden.

Bei großen Objekten gehen diese Konstruktor- und Destruktor-Aufrufe zu Lasten der Geschwindigkeit und des Speicherverbrauchs. Listing 9.9 verdeutlicht das mit einer vereinfachten Version eines benutzerdefinierten Objekts: SimpleCat. Ein reales Objekt wäre wesentlich größer und umfangreicher. Aber auch an diesem Objekt läßt sich zeigen, wie oft die Aufrufe von Konstruktor und Destruktor stattfinden.

Listing 9.10 erzeugt das Objekt SimpleCat und ruft dann zwei Funktionen auf. Die erste Funktion übernimmt Cat als Wert und gibt das Objekt als Wert zurück. Die zweite Funktion erhält das Objekt als Zeiger (nicht als Objekt selbst) und gibt einen Zeiger auf das Objekt zurück.

Listing 9.10: Objekte als Referenz übergeben

1:   // Listing 9.10
2: // Zeiger auf Objekte übergeben
3:
4: #include <iostream.h>
5:
6: class SimpleCat
7: {
8: public:
9: SimpleCat (); // Konstruktor
10: SimpleCat(SimpleCat&); // Kopierkonstruktor
11: ~SimpleCat(); // Destruktor
12: };
13:
14: SimpleCat::SimpleCat()
15: {
16: cout << "SimpleCat Konstruktor...\n";
17: }
18:
19: SimpleCat::SimpleCat(SimpleCat&)
20: {
21: cout << "SimpleCat Kopierkonstruktor...\n";
22: }
23:
24: SimpleCat::~SimpleCat()
25: {
26: cout << "SimpleCat Destruktor...\n";
27: }
28:
29: SimpleCat FunctionOne (SimpleCat theCat);
30: SimpleCat* FunctionTwo (SimpleCat *theCat);
31:
32: int main()
33: {
34: cout << "Eine Katze erzeugen...\n";
35: SimpleCat Frisky;
36: cout << "FunctionOne aufrufen...\n";
37: FunctionOne(Frisky);
38: cout << "FunctionTwo aufrufen...\n";
39: FunctionTwo(&Frisky);
40: return 0;
41: }
42:
43: // FunctionOne, Uebergabe als Wert
44: SimpleCat FunctionOne(SimpleCat theCat)
45: {
46: cout << "FunctionOne. Rueckkehr...\n";
47: return theCat;
48: }
49:
50: // FunctionTwo, Uebergabe als Referenz
51: SimpleCat* FunctionTwo (SimpleCat *theCat)
52: {
53: cout << "FunctionTwo. Rueckkehr...\n";
54: return theCat;
55: }

1:  Eine Katze erzeugen...
2: SimpleCat Konstruktor...
3: FunctionOne aufrufen...
4: SimpleCat Kopierkonstruktor...
5: FunctionOne. Rueckkehr...
6: SimpleCat Kopierkonstruktor...
7: SimpleCat Destruktor...
8: SimpleCat Destruktor...
9: FunctionTwo aufrufen...
10: FunctionTwo. Rueckkehr...
11: SimpleCat Destruktor...

Die hier angegebenen Zeilennummern erscheinen nicht in der Ausgabe, sondern dienen nur als Hilfsmittel für die Analyse.

Die Zeilen 6 bis 12 deklarieren eine sehr vereinfachte Klasse SimpleCat. Konstruktor, Kopierkonstruktor und Destruktor geben jeweils eine Meldung aus, damit man über die Zeitpunkte der Aufrufe informiert ist.

In Zeile 34 gibt main() eine Meldung aus, die als Ausgabezeile 1 zu sehen ist. In Zeile 35 wird ein SimpleCat-Objekt instantiiert. Das bewirkt den Aufruf des Konstruktors. Die entsprechende Meldung erscheint als Ausgabezeile 2.

In Zeile 36 meldet die Funktion main(), daß sie die Funktion FunctionOne() aufruft (Ausgabezeile 3). Da der Funktion FunctionOne() das Objekt SimpleCat beim Aufruf als Wert übergeben wird, legt die Funktion auf dem Stack ein lokales Objekt als Kopie des Objekts SimpleCat an. Das bewirkt den Aufruf des Kopierkonstruktors, der die Ausgabezeile 4 erzeugt.

Die Programmausführung springt zur Zeile 46 in der aufgerufenen Funktion, die eine Meldung ausgibt (Ausgabezeile 5). Die Funktion kehrt dann zurück und gibt dabei das Objekt SimpleCat als Wert zurück. Dies erzeugt eine weitere Kopie des Objekts, wobei der Kopierkonstruktor aufgerufen und Ausgabezeile 6 produziert wird.

Das Programm weist den Rückgabewert aus der Funktion FunctionOne() keinem Objekt zu, so daß das für die Rückgabe erzeugte Objekt mit Aufruf des Destruktors (Meldung in Ausgabezeile 7) verworfen wird. Da FunctionOne() beendet ist, verliert die lokale Kopie ihren Gültigkeitsbereich und wird mit Aufruf des Destruktors (Meldung in Ausgabezeile 8) zerstört.

Das Programm kehrt nach main() zurück und ruft FunctionTwo() auf, der der Parameter als Referenz übergeben wird. Da die Funktion keine Kopie anlegt, gibt es auch keine Ausgabe. FunctionTwo() produziert die als Ausgabezeile 10 erscheinende Meldung und gibt dann das Objekt SimpleCat - wiederum als Referenz - zurück. Aus diesem Grund finden hier ebenfalls keine Aufrufe von Konstruktor oder Destruktor statt.

Schließlich endet das Programm, und Frisky verliert seinen Gültigkeitsbereich. Das führt zum letzten Aufruf des Destruktors und zur Meldung in Ausgabezeile 11.

Aufgrund der Übergabe als Wert produziert der Aufruf der Funktion FunctionOne() zwei Aufrufe des Kopierkonstruktors und zwei Aufrufe des Destruktors, während der Aufruf von FunctionTwo keinerlei derartige Aufrufe erzeugt.

Einen konstanten Zeiger übergeben

Die Übergabe eines Zeigers an FunctionTwo() ist zwar effizienter, aber auch gefährlicher. FunctionTwo() soll ja eigentlich das übergebene Objekt SimpleCat nicht ändern, wird aber durch die Übergabe der Adresse von SimpleCat dazu prinzipiell in die Lage versetzt. Damit genießt das Objekt nicht mehr den Schutz gegenüber Änderungen wie bei der Übergabe als Wert.

Die Übergabe als Wert verhält sich so, als würde man einem Museum eine Fotografie des eigenen Kunstwerks geben und nicht das Kunstwerk selbst. Schmiert jemand im Museum auf Ihrem Bild herum, bleibt Ihnen in jedem Fall das Original erhalten. Bei der Übergabe als Referenz übermittelt man dem Museum lediglich seine Heimatadresse und lädt die Besucher ein, das echte Meisterwerk in Ihrem eigenen Haus anzusehen.

Die Lösung besteht in der Übergabe eines Zeigers auf ein konstantes Objekt SimpleCat . Damit verhindert man den Aufruf nicht konstanter Methoden auf SimpleCat und schützt demzufolge das Objekt gegen Änderungen. Listing 9.11 verdeutlicht dieses Konzept.

Listing 9.11: Übergabe von konstanten Zeigern

1:   // Listing 9.11 - Zeiger auf Objekte übergeben
2: // Zeiger auf Objekte übergeben
3:
4: #include <iostream.h>
5:
6: class SimpleCat
7: {
8: public:
9: SimpleCat();
10: SimpleCat(SimpleCat&);
11: ~SimpleCat();
12:
13: int GetAge() const { return itsAge; }
14: void SetAge(int age) { itsAge = age; }
15:
16: private:
17: int itsAge;
18: };
19:
20: SimpleCat::SimpleCat()
21: {
22: cout << "SimpleCat Konstruktor...\n";
23: itsAge = 1;
24: }
25:
26: SimpleCat::SimpleCat(SimpleCat&)
27: {
28: cout << "SimpleCat Kopierkonstruktor...\n";
29: }
30:
31: SimpleCat::~SimpleCat()
32: {
33: cout << "SimpleCat Destruktor...\n";
34: }
35:
36: const SimpleCat * const FunctionTwo (const SimpleCat * const theCat);
37:
38: int main()
39: {
40: cout << "Eine Katze erzeugen...\n";
41: SimpleCat Frisky;
42: cout << "Frisky ist " ;
43 cout << Frisky.GetAge();
44: cout << " Jahre alt\n";
45: int age = 5;
46: Frisky.SetAge(age);
47: cout << "Frisky ist " ;
48 cout << Frisky.GetAge();
49: cout << " Jahre alt\n";
50: cout << "FunctionTwo aufrufen...\n";
51: FunctionTwo(&Frisky);
52: cout << "Frisky ist " ;
53: cout << Frisky.GetAge();
54: cout << " Jahre _alt\n";
55: return 0;
56: }
57:
58: // FunctionTwo uebernimmt einen konstanten Zeiger
59: const SimpleCat * const FunctionTwo (const SimpleCat * const theCat)
60: {
61: cout << "FunctionTwo. Rueckkehr...\n";
62: cout << "Frisky ist jetzt " << theCat->GetAge();
63: cout << " Jahre alt \n";
64: // theCat->SetAge(8); const!
65: return theCat;
66: }

Eine Katze erzeugen...
SimpleCat Konstruktor...
Frisky ist 1 Jahr alt.
Frisky ist 5 Jahre alt.
FunctionTwo aufrufen...
FunctionTwo. Rueckkehr...
Frisky ist jetzt 5 Jahre alt.
Frisky ist 5 Jahre alt.
SimpleCat Destruktor...

Die Klasse SimpleCat hat zwei Zugriffsfunktionen erhalten: die konstante Funktion GetAge() in Zeile 13 und die nicht konstante Funktion SetAge() in Zeile 14. Außerdem ist die Elementvariable itsAge in Zeile 17 neu hinzugekommen.

Die Definitionen von Konstruktor, Kopierkonstruktor und Destruktor enthalten weiterhin die Ausgabe von Meldungen. Allerdings findet überhaupt kein Aufruf des Kopierkonstruktors statt, da die Übergabe des Objekts als Referenz erfolgt und damit keine Kopien angelegt werden. Zeile 41 erzeugt ein Objekt, die Zeilen 42 und 43 geben dessen Standardwert für das Alter (Age) aus.

In Zeile 46 wird itsAge mit der Zugriffsfunktion SetAge() gesetzt und das Ergebnis in Zeile 47 ausgegeben. FunctionOne() kommt in diesem Programm nicht zum Einsatz. FunctionTwo() weist leichte Änderungen auf: Jetzt sind in Zeile 36 der Parameter und der Rückgabewert so deklariert, daß sie einen konstanten Zeiger auf ein konstantes Objekt übernehmen und einen konstanten Zeiger auf ein konstantes Objekt zurückgeben.

Da die Übergabe der Parameter und des Rückgabewerts weiterhin als Referenz erfolgt, legt die Funktion keine Kopien an und ruft auch nicht den Kopierkonstruktor auf. Der Zeiger in FunctionTwo() ist jetzt allerdings konstant und kann demnach nicht die nicht-const Methode SetAge() aufrufen. Deshalb ist der Aufruf von SetAge() in Zeile 64 auskommentiert - das Programm ließe sich sonst nicht kompilieren.

Beachten Sie, daß das in main() erzeugte Objekt nicht konstant ist und Frisky die Funktion SetAge() aufrufen kann. Die Adresse dieses nicht konstanten Objekts wird an die Funktion FunctionTwo() übergeben. Da aber in FunctionTwo() der Zeiger als konstanter Zeiger auf ein konstantes Objekt deklariert ist, wird das Objekt wie ein konstantes Objekt behandelt!

Referenzen als Alternative

Listing 9.11 vermeidet die Einrichtung unnötiger Kopien und spart dadurch zeitraubende Aufrufe des Kopierkonstruktors und des Destruktors. Es verwendet konstante Zeiger auf konstante Objekte und löst damit das Problem, das die Funktion die übergebenen Objekte nicht ändern soll. Allerdings ist das ganze etwas umständlich, da die an die Funktion übergebenen Objekte Zeiger sind.

Da das Objekt niemals Null sein darf, verlegt man besser die ganze Arbeit in die Funktion und übergibt eine Referenz statt eines Zeigers. Dieses Vorgehen zeigt Listing 9.12.

Listing 9.12: Referenzen auf Objekte übergeben

1: // Listing 9.12
2: // Referenzen auf Objekte übergeben
3:
4: #include <iostream.h>
5:
6: class SimpleCat
7: {
8: public:
9: SimpleCat();
10: SimpleCat(SimpleCat&);
11: ~SimpleCat();
12:
13: int GetAge() const { return itsAge; }
14: void SetAge(int age) { itsAge = age; }
15:
16: private:
17: int itsAge;
18: };
19:
20: SimpleCat::SimpleCat()
21: {
22: cout << "SimpleCat Konstruktor...\n";
23: itsAge = 1;
24: }
25:
26: SimpleCat::SimpleCat(SimpleCat&)
27: {
28: cout << "SimpleCat Kopierkonstruktor...\n";
29: }
30:
31: SimpleCat::~SimpleCat()
32: {
33: cout << "SimpleCat Destruktor...\n";
34: }
35:
36: const SimpleCat & FunctionTwo (const SimpleCat & theCat);
37:
38: int main()
39: {
40: cout << "Eine Katze erzeugen...\n";
41: SimpleCat Frisky;
42: cout << "Frisky ist " << Frisky.GetAge() << " Jahre alt.\n";
43: int age = 5;
44: Frisky.SetAge(age);
45: cout << "Frisky ist " << Frisky.GetAge() << " Jahre alt.\n";
46: cout << "FunctionTwo aufrufen...\n";
47: FunctionTwo(Frisky);
48: cout << "Frisky ist " << Frisky.GetAge() << " Jahre alt.\n";
49: return 0;
50: }
51:
52: // FunctionTwo uebergibt eine Referenz auf ein konstantes Objekt
53: const SimpleCat & FunctionTwo (const SimpleCat & theCat)
54: {
55: cout << "FunctionTwo. Rueckkehr...\n";
56: cout << "Frisky ist jetzt " << theCat.GetAge();
57: cout << " Jahre alt.\n";
58: // theCat.SetAge(8); const!
59: return theCat;
60: }

Eine Katze erzeugen...
SimpleCat Konstruktor...
Frisky ist 1 Jahr alt.
Frisky ist 5 Jahre alt.
FunctionTwo aufrufen
FunctionTwo. Rueckkehr...
Frisky ist jetzt 5 Jahre alt.
Frisky ist 5 Jahre alt.
SimpleCat Destruktor...

Die Ausgabe ist mit der von Listing 9.11 produzierten Ausgabe identisch. Die einzige signifikante Änderung besteht darin, daß FunctionTwo() jetzt die Übernahme und Rückgabe mit einer Referenz auf ein konstantes Objekt realisiert. Auch hier ist das Arbeiten mit Referenzen einfacher als das Arbeiten mit Zeigern. Man erreicht die gleiche Einsparung und Effizienz sowie die Sicherheit der const-Deklaration.

Konstante Referenzen

Normalerweise unterscheiden C++-Programmierer nicht zwischen einer »konstanten Referenz auf ein SimpleCat-Objekt« und einer »Referenz auf ein konstantes SimpleCat-Objekt«. Referenzen selbst können nie einem anderen Objekt erneut zugewiesen werden, deshalb sind sie immer konstant. Wird das Schlüsselwort const im Zusammenhang mit einer Referenz verwendet, soll damit das Objekt, auf das sich die Referenz bezieht, konstant gemacht werden.

Wann verwendet man Referenzen und wann Zeiger?

C++-Programmierer arbeiten lieber mit Referenzen als mit Zeigern. Referenzen sind sauberer und einfacher zu verwenden und eignen sich besser für das Verbergen von Informationen, wie Sie im vorherigen Beispiel gesehen haben.

Allerdings kann man Referenzen nicht erneut zuweisen. Wenn man zuerst auf ein Objekt und dann auf ein anderes zeigen muß, ist die Verwendung eines Zeigers Pflicht. Referenzen dürfen nicht Null sein. Ist dennoch mit einem Null-Objekt zu rechnen, kann man nicht mit Referenzen arbeiten, man muß auf Zeiger ausweichen.

Letzteres ist beispielsweise bei Verwendung des Operators new der Fall. Wenn new keinen Speicher im Heap reservieren kann, liefert der Operator einen Null-Zeiger zurück. Da Null-Referenzen nicht erlaubt sind, muß man zuerst prüfen, ob die von new zurückgelieferte Speicheradresse ungleich Null ist, bevor man eine Referenz mit dem neu allokierten Speicher initialisiert. Das folgende Beispiel zeigt, wie man das in den Griff bekommt:

int *pInt = new int;
if (pInt != NULL)
int &rInt = *pInt;

Dieses Beispiel deklariert den Zeiger pInt auf int und initialisiert ihn mit dem vom Operator new zurückgegebenen Speicher. Die zweite Anweisung testet die Adresse in pInt. Ist diese nicht NULL, dereferenziert die dritte Anweisung pInt. Das Ergebnis der Dereferenzierung einer int-Variablen ist ein int-Objekt, und rInt wird mit einem Verweis auf dieses Objekt initialisiert. Somit wird rInt zu einer Alias-Adresse auf den vom Operator new zurückgegebenen int.

Was Sie tun sollten

... und was nicht

Übergeben Sie Parameter möglichst immer als Referenz.

Geben Sie Werte möglichst immer als Referenz zurück.

Verwenden Sie, wo es möglich ist, das Schlüsselwort const, um Referenzen und Zeiger zu schützen.

Verwenden Sie keine Zeiger, wenn auch Referenzen möglich sind.

Geben Sie keine Referenz an ein lokales Objekt zurück.

Referenzen und Zeiger mischen

Es ist absolut zulässig, in der Parameterliste einer Funktion sowohl Zeiger als auch Referenzen zu deklarieren und gleichzeitig Objekte als Wert zu übergeben. Sehen Sie dazu folgendes Beispiel:

Katze * EineFunktion (Person &derBesitzer, Haus *dasHaus, int alter);

Diese Deklaration sieht vor, daß EineFunktion() drei Parameter übernimmt. Der erste ist eine Referenz auf das Objekt Person, der zweite ein Zeiger auf das Objekt Haus und der dritte ein Integer. Die Funktion liefert einen Zeiger auf ein Katze-Objekt zurück.

Die Frage, wo der Referenz- (&) oder der Indirektionsoperator (*) bei der Deklaration dieser Variablen zu setzen ist, ist heftig umstritten. Zulässig sind folgende Schreibweisen:

1:  CAT&  rFrisky;
2: CAT & rFrisky;
3: CAT &rFrisky;

Leerzeichen werden gänzlich ignoriert. Aus diesem Grund können Sie an jeder Stelle im Code, an dem ein Leerzeichen steht, beliebig viele weitere Leerzeichen, Tabulatoren oder auch neue Zeilen einfügen.

Welche Schreibweise ist jetzt aber die beste, wenn man mal die Freiheit beim Schreiben von Ausdrücken außer acht läßt? Dazu möchte ich Ihnen Argumente für alle drei Schreibweisen nennen:

Die Begründung für Fall 1 ist, daß rFrisky eine Variable namens rFrisky ist, deren Typ als »Referenz auf das Objekt CAT« verstanden wird. Deshalb sollte in diesem Fall das & beim Typ stehen.

Als Gegenargument könnte man einwenden, daß der Typ CAT lautet. Das & ist Teil des »Deklarators«, der den Variablennamen und das Ampersand-Zeichen (kaufmännisches Und) umfaßt. Was jedoch noch wichtiger ist - das & neben CAT zu stellen, kann folgenden Fehler zur Folge haben:

   CAT&  rFrisky, rBoots;

Eine flüchtige Prüfung dieser Zeile würde Sie zu dem Gedanken veranlassen, daß rFrisky und rBoots beides Referenzen auf CAT-Objekte sind, was jedoch falsch wäre. Vielmehr ist rFrisky eine Referenz auf CAT und rBoots (trotz seines Namens) keine Referenz, sondern eine einfache, schlichte CAT-Variable. Statt dessen hätte man schreiben sollen:

   CAT  &rFrisky, rBoots;

Die Antwort auf diesen Einspruch lautet, daß Deklarationen von Referenzen und Variablen nie auf diese Art kombiniert werden sollten. So sollte die Deklaration aussehen:

   CAT&  rFrisky;
CAT boots;

Letztlich umgehen viele Programmierer das Problem und wählen die mittlere Lösung. Sie setzen das & in die Mitte, wie Fall 2 demonstriert.

Selbstverständlich läßt sich alles, was hier im Zusammenhang mit dem Referenzoperator (&) gesagt wurde, auch auf den Indirektionsoperator (*) übertragen. Wichtig ist jedoch zu erkennen, daß die Menschen in der Wahrnehmung des einzig wahren Wegs unterschiedlicher Meinung sind. Wählen Sie einen Stil, der Ihnen am ehesten liegt, und vor allem wechseln Sie den Stil nicht innerhalb eines Programms. Denn, oberstes Gebot der Programmierung ist und bleibt die Klarheit.

Viele Programmierer folgen den folgenden Konventionen zum Deklarieren von Referenzen und Zeigern:

  1. Setzen Sie das &- und das *-Zeichen in die Mitte und links und rechts davon je ein Leerzeichen.
  2. Deklarieren Sie Referenzen, Zeiger und Variablen nie zusammen in einer Zeile.

Referenzen auf nicht mehr vorhandene Objekte

Nachdem sich C++-Programmierer einmal mit der Übergabe als Referenz angefreundet haben, können sie kaum noch davon lassen. Man kann allerdings auch des Guten zuviel tun. Denken Sie daran, daß eine Referenz immer eine Alias-Adresse für irgendein anderes Objekt ist. Wenn man eine Referenz in oder aus einer Funktion übergibt, sollte man sich die kritische Frage stellen: »Was ist das Objekt, das ich unter einer Alias-Adresse anspreche, und existiert es auch wirklich, wenn ich es verwende?«

Listing 9.13 verdeutlicht die Gefahr bei der Rückgabe einer Referenz auf ein Objekt, das nicht mehr existiert.

Listing 9.13: Rückgabe einer Referenz auf ein nicht existierendes Objekt

1:      // Listing 9.13
2: // Rueckgabe einer Referenz auf ein Objekt,
3: // das nicht mehr existiert
4:
5: #include <iostream.h>
6:
7: class SimpleCat
8: {
9: public:
10: SimpleCat (int age, int weight);
11: ~SimpleCat() {}
12: int GetAge() { return itsAge; }
13: int GetWeight() { return itsWeight; }
14: private:
15: int itsAge;
16: int itsWeight;
17: };
18:
19: SimpleCat::SimpleCat(int age, int weight)
20: {
21: itsAge = age;
22: itsWeight = weight;
23: }
24:
25: SimpleCat &TheFunction();
26:
27: int main()
28: {
29: SimpleCat &rCat = TheFunction();
30: int age = rCat.GetAge();
31: cout << "rCat ist " << age << " Jahre alt!\n";
32: return 0;
33: }
34:
35: SimpleCat &TheFunction()
36: {
37: SimpleCat Frisky(5,9);
38: return Frisky;
39: }

Compile error: Attempting to return a reference to a local object!

Dieses Programm läßt sich nicht mit dem Borland-Compiler kompilieren, sondern nur mit Microsoft-Compilern. Allerdings sei angemerkt, daß es sich nicht gerade um guten Programmierstil handelt.

Die Zeilen 7 bis 17 deklarieren SimpleCat. In Zeile 29 wird eine Referenz auf SimpleCat mit dem Ergebnis des Aufrufs der Funktion TheFunction() initialisiert. Die Funktion TheFunction() ist in Zeile 25 deklariert und gibt eine Referenz auf SimpleCat zurück.

Der Rumpf der Funktion TheFunction() deklariert ein lokales Objekt vom Typ SimpleCat und initialisiert dessen Alter (age) und Gewicht (weight). Dann gibt die Funktion dieses lokale Objekt als Referenz zurück. Manche Compiler sind intelligent genug, um diesen Fehler abzufangen und erlauben gar nicht erst den Start des Programms. Andere lassen die Programmausführung zu, was aber zu unvorhersehbaren Ergebnissen führt.

Bei Rückkehr der Funktion TheFunction() wird das lokale Objekt Frisky zerstört. (Der Autor versichert, daß dies schmerzlos geschieht.) Die von dieser Funktion zurückgegebene Referenz wird eine Alias-Adresse für ein nicht existentes Objekt, und das geht irgendwann schief.

Referenzen auf Objekte im Heap zurückgeben

Man mag versucht sein, das Problem in Listing 9.13 zu lösen, indem man die TheFunction()-Funktion Frisky im Heap erzeugen läßt. Damit existiert Frisky auch, nachdem TheFunction() zurückgekehrt ist.

Das Problem bei dieser Variante ist: Was fängt man mit dem für Frisky zugewiesenen Speicher an, wenn die Arbeit damit beendet ist? Listing 9.14 verdeutlicht dieses Problem.

Listing 9.14: Speicherlücken

1:      // Listing 9.14
2: // Beseitigen von Speicherlücken
3: #include <iostream.h>
4:
5: class SimpleCat
6: {
7: public:
8: SimpleCat (int age, int weight);
9: ~SimpleCat() {}
10: int GetAge() { return itsAge; }
11: int GetWeight() { return itsWeight; }
12:
13 private:
14: int itsAge;
15: int itsWeight;
16: };
17:
18: SimpleCat::SimpleCat(int age, int weight)
19: {
20: itsAge = age;
21: itsWeight = weight;
22: }
23:
24: SimpleCat & TheFunction();
25:
26: int main()
27: {
28: SimpleCat & rCat = TheFunction();
29: int age = rCat.GetAge();
30: cout << "rCat ist " << age << " Jahre alt!\n";
31: cout << "&rCat: " << &rCat << endl;
32: // Wie wird man diesen Speicher wieder los?
33: SimpleCat * pCat = &rCat;
34: delete pCat;
35: // Worauf verweist denn nun rCat?!?
36: return 0;
37: }
38:
39: SimpleCat &TheFunction()
40: {
41: SimpleCat * pFrisky = new SimpleCat(5,9);
42: cout << "pFrisky: " << pFrisky << endl;
43: return *pFrisky;
44: }

pFrisky: 0x00431C60
rCat ist 5 Jahre alt!
&rCat: 0x00431C60

Dieses Programm läßt sich kompilieren und scheint zu arbeiten. Es handelt sich aber um eine Zeitbombe, die nur auf ihre Zündung wartet.

Die Funktion TheFunction() wurde geändert und gibt jetzt nicht länger eine Referenz auf eine lokale Variable zurück. Zeile 41 reserviert Speicher im Heap und weist ihn einem Zeiger zu. Es folgt die Ausgabe der vom Zeiger gespeicherten Adresse. Dann wird der Zeiger dereferenziert und das SimpleCat-Objekt als Referenz zurückgegeben.

In Zeile 28 wird das Ergebnis des Funktionsaufrufs von TheFunction() einer Referenz auf ein SimpleCat-Objekt zugewiesen. Über dieses Objekt wird das Alter der Katze ermittelt und in Zeile 30 ausgegeben.

Um sich davon zu überzeugen, daß die in main() deklarierte Referenz auf das in der Funktion TheFunction() im Heap abgelegte Objekt verweist, wird der Adreßoperator auf rCat angewandt. Tatsächlich erscheint die Adresse des betreffenden Objekts, und diese stimmt mit der Adresse des Objekts im Heap überein.

So weit so gut. Wie aber wird dieser Speicher freigegeben? Auf der Referenz kann man delete nicht ausführen. Eine clevere Lösung ist die Erzeugung eines weiteren Zeigers und dessen Initialisierung mit der Adresse, die man aus rCat erhält. Damit löscht man den Speicher und stopft die Speicherlücke. Trotzdem bleibt ein schlechter Beigeschmack: Worauf bezieht sich rCat nach Zeile 34? Wie bereits erwähnt, muß eine Referenz immer als Alias-Adresse für ein tatsächliches Objekt agieren. Wenn sie auf ein Null-Objekt verweist (wie in diesem Fall), ist das Programm ungültig.

Man kann nicht genug darauf hinweisen, daß sich ein Programm mit einer Referenz auf ein Null-Objekt zwar kompilieren läßt, das Programm aber nicht zulässig und sein Verhalten nicht vorhersehbar ist.

Für dieses Problem gibt es drei Lösungen. Die erste ist die Deklaration eines SimpleCat -Objekts in Zeile 28 und die Rückgabe der Katze aus der Funktion TheFunction() als Wert. Als zweite Lösung kann man das SimpleCat-Objekt auf dem Heap in der Funktion TheFunction() deklarieren, aber die Funktion TheFunction() einen Zeiger auf diesen Speicher zurückgeben zu lassen. Dann kann die aufrufende Funktion den Zeiger nach Abschluß der Arbeiten löschen.

Die dritte und beste Lösung besteht in der Deklaration des Objekts in der aufrufenden Funktion. Dann übergibt man das Objekt an TheFunction() als Referenz.

Wem gehört der Zeiger?

Wenn man Speicher im Heap reserviert, bekommt man einen Zeiger zurück. Es ist zwingend erforderlich, einen Zeiger auf diesen Speicher aufzubewahren, denn wenn der Zeiger verloren ist, läßt sich der Speicher nicht mehr löschen - es entsteht eine Speicherlücke.

Bei der Übergabe dieses Speicherblocks zwischen Funktionen nimmt irgendwer den Zeiger »in Besitz«. In der Regel wird der Block mittels Referenzen übergeben, und die Funktion, die den Speicher erzeugt hat, löscht ihn auch wieder. Das ist aber nur eine allgemeine Regel und kein eisernes Gesetz.

Gefahr ist im Verzug, wenn eine Funktion einen Speicher erzeugt und eine andere ihn freigibt. Unklare Besitzverhältnisse in bezug auf den Zeiger können zu zwei Problemen führen: Man vergißt, den Zeiger zu löschen, oder löscht ihn zweimal. Beides kann ernste Konsequenzen für das Programm zur Folge haben. Funktionen sollte man sicherheitshalber so konzipieren, daß sie den erzeugten Speicher auch selbst wieder löschen.

Wenn Sie eine Funktion schreiben, die einen Speicher erzeugen muß und ihn dann zurück an die aufrufende Funktion übergibt, sollten Sie eine Änderung der Schnittstelle in Betracht ziehen. Lassen Sie die aufrufende Funktion den Speicher reservieren und übergeben Sie ihn an die Funktion als Referenz. Damit nehmen Sie die gesamte Speicherverwaltung aus dem Programm heraus und überlassen sie der Funktion, die auf das Löschen des Speichers vorbereitet ist.

Was Sie tun sollten

... und was nicht

Übergeben Sie Parameter als Wert, wenn es erforderlich ist.

Geben Sie als Wert zurück, wenn es erforderlich ist.

Übergeben Sie nicht als Referenz, wenn das referenzierte Objekt seinen Gültigkeitsbereich verlieren kann.

Verwenden Sie keine Referenzen auf Null-Objekte.

Zusammenfassung

Heute haben Sie gelernt, was Referenzen sind und in welchem Verhältnis sie zu Zeigern stehen. Man muß Referenzen mit einem Verweis auf ein existierendes Objekt initialisieren und darf keine erneute Zuweisung auf ein anderes Objekt vornehmen. Jede Aktion, die man auf einer Referenz ausführt, wird praktisch auf dem Zielobjekt der Referenz ausgeführt. Ruft man die Adresse einer Referenz ab, erhält man die Adresse des Zielobjekts zurück.

Sie haben gesehen, daß die Übergabe von Objekten als Referenz effizienter sein kann als die Übergabe als Wert. Die Übergabe als Referenz gestattet der aufgerufenen Funktion auch, den als Argument übergebenen Wert in geänderter Form an die aufrufende Funktion zurückzugeben.

Es wurde gezeigt, daß Argumente an Funktionen und die von Funktionen zurückgegebenen Werte als Referenz übergeben werden können und daß sich dies sowohl mit Zeigern als auch mit Referenzen realisieren läßt.

Das Kapitel hat dargestellt, wie man Zeiger auf konstante Objekte und konstante Referenzen für die sichere Übergabe von Werten zwischen Funktionen verwendet und dabei die gleiche Effizienz wie bei der Übergabe als Referenz erreicht.

Fragen und Antworten

Frage:
Warum gibt es Referenzen, wenn sich das gleiche auch mit Zeigern realisieren läßt?

Antwort:
Referenzen sind leichter zu verwenden und zu verstehen. Die Indirektion bleibt verborgen, und man muß die Variable nicht wiederholt dereferenzieren.

Frage:
Warum verwendet man Zeiger, wenn Referenzen einfacher zu handhaben sind?

Antwort:
Referenzen dürfen nicht Null sein und lassen sich nach der Initialisierung nicht mehr auf andere Objekte richten. Zeiger bieten mehr Flexibilität, sind aber etwas schwieriger einzusetzen.

Frage:
Warum gestaltet man die Rückgabe aus einer Funktion überhaupt als Rückgabe von Werten?

Antwort:
Wenn das zurückzugebende Objekt lokal ist, muß man es als Wert zurückgeben. Ansonsten gibt man eine Referenz auf ein nicht-existentes Objekt zurück.

Frage:
Warum gibt man nicht immer als Wert zurück, wenn die Rückgabe als Referenz gefährlich ist?

Antwort:
Die Rückgabe als Referenz ist weitaus effizienter. Man spart Speicher, und das Programm läuft schneller.

Workshop

Der Workshop enthält Quizfragen, die Ihnen helfen sollen, Ihr Wissen zu festigen, und Übungen, die Sie anregen sollen, das eben Gelernte umzusetzen und eigene Erfahrungen zu sammeln. Versuchen Sie, das Quiz und die Übungen zu beantworten und zu verstehen, bevor Sie die Lösungen in Anhang D lesen und zur Lektion des nächsten Tages übergehen.

Quiz

  1. Worin besteht der Unterschied zwischen einer Referenz und einem Zeiger?
  2. Wann sollte man statt einer Referenz lieber einen Zeiger verwenden?
  3. Was für einen Rückgabewert hat new, wenn nicht genug Speicher für Ihr new-Objekt vorhanden ist?
  4. Was ist eine konstante Referenz?
  5. Was ist der Unterschied zwischen Übergabe als Referenz und Übergabe einer Referenz?

Übungen

  1. Schreiben Sie ein Programm, das eine Variable vom Typ int, eine Referenz auf int und einen Zeiger auf int deklariert. Verwenden Sie den Zeiger und die Referenz, um den Wert in int zu manipulieren.
  2. Schreiben Sie ein Programm, das einen konstanten Zeiger auf einen konstanten Integer deklariert. Initialisieren Sie den Zeiger mit einer Integer-Variablen varOne. Weisen Sie varOne den Wert 6 zu. Weisen Sie mit Hilfe des Zeigers varOne den Wert 7 zu. Erzeugen Sie eine zweite Integer-Variable varTwo. Richten Sie den Zeiger auf die Variable varTwo. Kompilieren Sie diese Übung noch nicht.
  3. Kompilieren Sie jetzt das Programm aus Übung 2. Welche Zeilen produzieren Fehler und welche Warnungen?
  4. Schreiben Sie ein Programm, das einen vagabundierenden Zeiger erzeugt.
  5. Beheben Sie den Fehler im Programm aus Übung 4.
  6. Schreiben Sie ein Programm, das eine Speicherlücke erzeugt.
  7. Beheben Sie den Fehler im Programm aus Übung 6.
  8. FEHLERSUCHE: Was ist falsch an diesem Programm?
    1:     #include <iostream.h>
    2:
    3: class CAT
    4: {
    5: public:
    6: CAT(int age) { itsAge = age; }
    7: ~CAT(){}
    8: int GetAge() const { return itsAge;}
    9: private:
    10: int itsAge;
    11: };
    12:
    13: CAT & MakeCat(int age);
    14: int main()
    15: {
    16: int age = 7;
    17: CAT Boots = MakeCat(age);
    18: cout << "Boots ist " << Boots.GetAge() << " Jahre alt\n";
    return 0;
    19: }
    20:
    21: CAT & MakeCat(int age)
    22: {
    23: CAT * pCat = new CAT(age);
    24: return *pCat;
    25: }
  9. Beheben Sie den Fehler im Programm aus Übung 8.



vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


© Markt&Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH