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,
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.
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.
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.
Verwenden Sie Referenzen, um eine Alias-Adresse auf ein Objekt zu erzeugen. | Versuchen Sie nicht, eine Referenz erneut zuzuweisen. Verwechseln Sie nicht den Adreßoperator mit dem Referenzoperator. |
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.
Die Deklaration einer Referenz besteht aus dem Typ, gefolgt von dem Referenzoperator (
&
) und dem Referenznamen. Referenzen müssen bei ihrer Erzeugung initialisiert werden.int seinAlter;
int &rAlter = seinAlter;CAT boots;
CAT &rCatRef = boots;
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.
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.
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.
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.
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.
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 USHORT
s. 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).
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.
Ü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.
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!
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.
Normalerweise unterscheiden C++-Programmierer nicht zwischen einer »konstanten Referenz auf ein
SimpleCat
-Objekt« und einer »Referenz auf ein konstantesSimpleCat
-Objekt«. Referenzen selbst können nie einem anderen Objekt erneut zugewiesen werden, deshalb sind sie immer konstant. Wird das Schlüsselwortconst
im Zusammenhang mit einer Referenz verwendet, soll damit das Objekt, auf das sich die Referenz bezieht, konstant gemacht werden.
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
.
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 namensrFrisky
ist, deren Typ als »Referenz auf das ObjektCAT
« 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&
nebenCAT
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
undrBoots
beides Referenzen aufCAT
-Objekte sind, was jedoch falsch wäre. Vielmehr istrFrisky
eine Referenz aufCAT
undrBoots
(trotz seines Namens) keine Referenz, sondern eine einfache, schlichteCAT
-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:
&
- und das *
-Zeichen in die Mitte und links und rechts davon je ein Leerzeichen.
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.
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.
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.
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.
Übergeben Sie nicht als Referenz, wenn das referenzierte Objekt seinen Gültigkeitsbereich verlieren kann. |
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.
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.
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.
new
, wenn nicht genug Speicher für Ihr new
-Objekt vorhanden ist?
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.
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.
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: }
© Markt+Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH