In Kapitel 5, »Funktionen«, haben Sie die Grundlagen für die Arbeit mit Funktionen kennengelernt. Nachdem Sie nun wissen, wie Zeiger und Referenzen arbeiten, können Sie tiefer in diese Materie eindringen. Heute lernen Sie,
In Kapitel 5 haben Sie gelernt, wie man Funktionspolymorphie - oder das Überladen von Funktionen - durch Aufsetzen zweier oder mehrerer Funktionen mit demselben Namen aber mit unterschiedlichen Parametern implementiert. Elementfunktionen von Klassen lassen sich ebenfalls überladen.
Die in Listing 10.1 dargestellte Klasse Rectangle
enthält zwei Funktionen namens
DrawShape()
. Eine Funktion hat keine Parameter und zeichnet das Rectangle
-Objekt
auf der Basis der aktuellen Werte in der Klasse. Die andere Funktion übernimmt zwei
Werte, width
(Breite) und height
(Höhe). Diese Funktion zeichnet das Rectangle
-Objekt
mit den übergebenen Werten und ignoriert die aktuellen Klassenwerte.
Listing 10.1: Überladen von Elementfunktionen
1: // Listing 10.1 Ueberladen von Elementfunktionen
2: #include <iostream.h>
3:
4:
5: // Deklaration der Klasse Rectangle
6: class Rectangle
7: {
8: public:
9: // Konstruktoren
10: Rectangle(int width, int height);
11: ~Rectangle(){}
12:
13: // Ueberladene Klassenfunktion DrawShape
14: void DrawShape() const;
15: void DrawShape(int aWidth, int aHeight) const;
16:
17: private:
18: int itsWidth;
19: int itsHeight;
20: };
21:
22: // Implementierung des Konstruktors
23: Rectangle::Rectangle(int width, int height)
24: {
25: itsWidth = width;
26: itsHeight = height;
27: }
28:
29:
30: // Ueberladene Elementfunktion DrawShape - übernimmt
31: // keine Werte. Zeichnet mit aktuellen Datenelementen.
32: void Rectangle::DrawShape() const
33: {
34: DrawShape( itsWidth, itsHeight);
35: }
36:
37:
38: // Ueberladene Elementfunktion DrawShape - uebernimmt
39: // zwei Werte und zeichnet nach diesen Parametern.
40: void Rectangle::DrawShape(int width, int height) const
41: {
42: for (int i = 0; i<height; i++)
43: {
44: for (int j = 0; j< width; j++)
45: {
46: cout << "*";
47: }
48: cout << "\n";
49: }
50: }
51:
52: // Rahmenprogramm zur Demo fuer ueberladene Funktionen
53: int main()
54: {
55: // Ein Rechteck mit 30,5 initialisieren
56: Rectangle theRect(30,5);
57: cout << "DrawShape(): \n";
58: theRect.DrawShape();
59: cout << "\nDrawShape(40,2): \n";
60: theRect.DrawShape(40,2);
61: return 0;
62: }
DrawShape():
******************************
******************************
******************************
******************************
******************************
DrawShape(40,2):
****************************************
****************************************
Listing 10.1 stellt eine abgespeckte Version des Programms aus dem Rückblick zu Woche 1 dar. Der Test auf ungültige Werte und einige der Zugriffsfunktionen wurden aus dem Programm genommen, um Platz zu sparen. Das Hauptprogramm wurde auf ein einfaches Rahmenprogramm ohne Menü reduziert.
Den für uns im Moment interessanten Code finden Sie in den Zeilen 14 und 15, wo
die Funktion DrawShape()
überladen wird. Die Implementierung dieser beiden überladenen
Klassenmethoden ist in den Zeilen 30 bis 50 untergebracht. Beachten Sie, daß
die Version von DrawShape()
, die keine Parameter übernimmt, einfach die Version
aufruft, die zwei Parameter übernimmt, und dabei die aktuellen Elementvariablen
übergibt. Man sollte tunlichst vermeiden, den gleichen Code in zwei Funktionen zu
wiederholen. Es ist immer schwierig und fehlerträchtig, beide Funktionen synchron zu
halten, wenn man Änderungen an der einen oder anderen Funktion vornimmt.
Das Rahmenprogramm in den Zeilen 52 bis 62 erzeugt ein Rectangle
-Objekt und ruft
dann DrawShape()
auf. Beim ersten Mal werden keine Parameter und beim zweiten
Aufruf zwei unsigned short int
s übergeben.
Der Compiler entscheidet anhand der Anzahl und des Typs der eingegebenen Parameter,
welche Methode aufzurufen ist. Man könnte sich eine dritte überladene Funktion
namens DrawShape()
vorstellen, die eine Abmessung und einen Aufzählungstyp
entweder für Breite oder Höhe - je nach Wahl des Benutzers - übernimmt.
Ebenso wie »klassenlose« Funktionen können auch Elementfunktionen einer Klasse einen oder mehrere Standardwerte haben. Für die Deklaration der Standardwerte gelten dabei stets die gleichen Regeln (siehe Kapitel 5, Abschnitt »Standardparameter«).
1: // Listing 10.2 Standardwerte in Elementfunktionen
2: #include <iostream.h>
3:
4: int
5:
6: // Deklaration der Klasse Rectangle
7: class Rectangle
8: {
9: public:
10: // Konstruktoren
11: Rectangle(int width, int height);
12: ~Rectangle(){}
13: void DrawShape(int aWidth, int aHeight, bool UseCurrentVals
= false) const;
14:
15: private:
16: int itsWidth;
17: int itsHeight;
18: };
19:
20: //Implementierung des Konstruktors
21: Rectangle::Rectangle(int width, int height):
22: itsWidth(width), // Initialisierungen
23: itsHeight(height)
24: {} // leerer Rumpf
25:
26:
27: // Standardwerte für dritten Parameter
28: void Rectangle::DrawShape(
29: int width,
30: int height,
31: bool UseCurrentValue
32: ) const
33: {
34: int printWidth;
35: int printHeight;
36:
37: if (UseCurrentValue == true)
38: {
39: printWidth = itsWidth; // aktuelle Klassenwerte verwenden
40: printHeight = itsHeight;
41: }
42: else
43: {
44: printWidth = width; // Parameterwerte verwenden
45: printHeight = height;
46: }
47:
48:
49: for (int i = 0; i<printHeight; i++)
50: {
51: for (int j = 0; j< printWidth; j++)
52: {
53: cout << "*";
54: }
55: cout << "\n";
56: }
57: }
58:
59: // Rahmenprogramm zur Demonstration der ueberladenen Funktionen
60: int main()
61: {
62: // Ein Rechteck mit 30,5 initialisieren
63: Rectangle theRect(30,5);
64: cout << "DrawShape(0,0,true)...\n";
65: theRect.DrawShape(0,0,true);
66: cout <<"DrawShape(40,2)...\n";
67: theRect.DrawShape(40,2);
68: return 0;
69: }
DrawShape(0,0,true)...
******************************
******************************
******************************
******************************
******************************
DrawShape(40,2)...
************************************************************
************************************************************
Listing 10.2 ersetzt die überladene Funktion DrawShape()
durch eine einzelne Funktion
mit Standardparametern. Die Funktion wird in der Zeile 13 deklariert und übernimmt
drei Parameter. Die ersten beiden, aWidth
und aHeight
, sind vom Typ USHORT
,
der dritte, UseCurrentVals
, ist vom Typ BOOL
(true
oder false
) mit dem Standardwert
false
.
Die Implementierung für diese etwas unhandliche Funktion beginnt in Zeile 28. Die
if
-Anweisung in Zeile 38 wertet den dritten Parameter, UseCurrentValue
, aus. Ist er
true
, erhalten die lokalen Variablen printWidth
und printHeight
die Werte der lokalen
Elementvariablen itsWidth
bzw. itsHeight
.
Ist UseCurrentValue
gleich false
, weil es sich entweder um den Standardwert false
handelt oder der Benutzer diesen Wert so festgelegt hat, übernimmt die Funktion für
printWidth
und printHeight
die beiden ersten Parameter.
Wenn UseCurrentValue
gleich true
ist, werden die Werte der beiden anderen Parameter
ignoriert.
Die Programme der Listings 10.1 und 10.2 realisieren die gleiche Aufgabe, wobei aber die überladenen Funktionen in Listing 10.1 einfacher zu verstehen und natürlicher in der Handhabung sind. Wenn man außerdem eine dritte Variation benötigt - wenn vielleicht der Benutzer entweder die Breite oder die Höhe bereitstellen möchte - kann man überladene Funktionen leicht erweitern. Mit Standardwerten ist schnell die Grenze des sinnvoll Machbaren bei neuen Varianten erreicht.
Anhand der folgenden Punkte können Sie entscheiden, ob überladene Funktionen oder Standardwerte im konkreten Fall besser geeignet sind:
Überladene Funktionen verwendet man, wenn
Wie bereits in Kapitel 6, »Klassen«, erwähnt, erzeugt der Compiler, wenn man nicht explizit einen Konstruktor für die Klasse deklariert, einen Standardkonstruktor, der keine Parameter aufweist und keine Aktionen ausführt. Es ist jedoch ohne weiteres möglich, einen eigenen Standardkonstruktor zu erstellen, der ohne Argumente auskommt, aber das Objekt wie gewünscht einrichtet.
Der automatisch bereitgestellte Konstruktor heißt zwar »Standardkonstruktor«, was aber per Konvention auch für alle Konstruktoren gilt, die keine Parameter übernehmen. Normalerweise geht aus dem Kontext hervor, welcher Konstruktor gemeint ist.
Sobald man irgendeinen Konstruktor selbst erstellt, steuert der Compiler keinen Standardkonstruktor mehr bei. Wenn Sie also einen Konstruktor brauchen, der keine Parameter übernimmt, und Sie haben irgendwelche anderen Konstruktoren erstellt, müssen Sie den Standardkonstruktor selbst erzeugen!
Ein Konstruktor dient der Erzeugung eines Objekts. Beispielsweise erzeugt der Rectangle
-Konstruktor ein Rechteck. Vor Ausführen des Konstruktors gibt es kein Rechteck,
lediglich einen Speicherbereich. Nach Abschluß des Konstruktors ist ein vollständiges
und sofort einsetzbares Rectangle
-Objekt vorhanden.
Wie alle Elementfunktionen lassen sich auch Konstruktoren überladen - eine sehr leistungsfähige und flexible Option.
Nehmen wir zum Beispiel ein Rectangle
-Objekt mit zwei Konstruktoren: Der erste
Konstruktor übernimmt eine Länge und eine Breite und erstellt ein Rechteck dieser
Größe. Der zweite Konstruktor übernimmt keine Werte und erzeugt ein Rechteck mit
Standardgröße. Listing 10.3 veranschaulicht dies.
Listing 10.3: Den Konstruktor überladen
1: // Listing 10.3
2: // Konstruktoren ueberladen
3:
4: #include <iostream.h>
5:
6: class Rectangle
7: {
8: public:
9: Rectangle();
10: Rectangle(int width, int length);
11: ~Rectangle() {}
12: int GetWidth() const { return itsWidth; }
13: int GetLength() const { return itsLength; }
14: private:
15: int itsWidth;
16: int itsLength;
17: };
18:
19: Rectangle::Rectangle()
20: {
21: itsWidth = 5;
22: itsLength = 10;
23: }
24:
25: Rectangle::Rectangle (int width, int length)
26: {
27: itsWidth = width;
28: itsLength = length;
29: }
30:
31: int main()
32: {
33: Rectangle Rect1;
34: cout << "Rect1 Breite: " << Rect1.GetWidth() << endl;
35: cout << "Rect1 Länge: " << Rect1.GetLength() << endl;
36:
37: int aWidth, aLength;
38: cout << "Geben Sie eine Breite ein: ";
39: cin >> aWidth;
40: cout << "\nGeben Sie eine Laenge ein: ";
41: cin >> aLength;
42:
43: Rectangle Rect2(aWidth, aLength);
44: cout << "\nRect2 Breite: " << Rect2.GetWidth() << endl;
45: cout << "Rect2 Laenge: " << Rect2.GetLength() << endl;
46: return 0;
47: }
Rect1 Breite: 5
Rect1 Länge: 10
Geben Sie eine Breite ein: 20
Geben Sie eine Länge ein: 50
Rect2 Breite: 20
Rect2 Länge: 50
Die Zeilen 6 bis 17 deklarieren die Klasse Rectangle
. Es werden zwei Konstruktoren
deklariert, der »Standardkonstruktor« in Zeile 9 und ein Konstruktor, der zwei Integer-
Variablen übernimmt.
Zeile 33 erzeugt mit Hilfe des Standardkonstruktors ein Rechteck, dessen Werte in den Zeilen 34 und 35 ausgegeben werden. Die Zeilen 37 bis 41 bitten den Anwender, Werte für die Breite und Länge einzugeben, und der Aufruf des Konstruktors, der zwei Werte übernimmt, erfolgt in Zeile 43. Schließlich werden die Breite und Höhe des Rechtecks in den Zeilen 44 und 45 ausgegeben.
Wie für jede überladene Funktion, wählt der Compiler anhand Anzahl und Typ der Parameter den richtigen Konstruktor aus.
Bis jetzt haben wir die Elementvariablen von Objekten immer im Rumpf des Konstruktors eingerichtet. Konstruktoren werden allerdings in zwei Stufen aufgerufen: zuerst in der Initialisierungsphase und dann bei Ausführung des Rumpfes.
Die meisten Variablen lassen sich in beiden Phasen einrichten, entweder durch Initialisierung im Initialisierungsteil oder durch Zuweisung im Rumpf des Konstruktors. Sauberer und meist auch effizienter ist es, die Elementvariablen im Initialisierungsteil zu initialisieren. Das folgende Beispiel zeigt, wie dies geht:
CAT(): // Name und Parameter des Konstruktors
itsAge(5), // Initialisierungsliste
itsWeight(8)
{ } // Rumpf des Konstruktors
Auf die schließende Klammer der Parameterliste des Konstruktors folgt ein Doppelpunkt. Dann schreiben Sie den Namen der Elementvariablen und ein Klammernpaar. In die Klammern kommt der Ausdruck zur Initialisierung dieser Elementvariablen. Gibt es mehrere Initialisierungen, sind diese jeweils durch Komma zu trennen. Listing 10.4 definiert die gleichen Konstruktoren wie Listing 10.3, nur daß diesmal Elementvariablen in der Initialisierungsliste eingerichtet werden.
Listing 10.4: Codefragment zur Initialisierung von Elementvariablen
1: Rectangle::Rectangle():
2: itsWidth(5),
3: itsLength(10)
4: {
5: }
6:
7: Rectangle::Rectangle (int width, int length):
8: itsWidth(width),
9: itsLength(length)
10: {
11: }
Einige Variablen, zum Beispiel Referenzen und Konstanten, müssen initialisiert werden und erlauben keine Zuweisungen. Sonstige Zuweisungen oder Arbeiten werden im Rumpf des Konstruktors erledigt, denken Sie aber auf jeden Fall daran, so weit es geht Initialisierungen zu verwenden.
Neben der Bereitstellung eines Standardkonstruktors und -destruktors liefert der Compiler auch einen Standardkopierkonstruktor. Der Aufruf des Kopierkonstruktors erfolgt jedesmal, wenn eine Kopie eines Objekts angelegt wird.
Übergibt man ein Objekt als Wert, entweder als Parameter an eine Funktion oder als Rückgabewert einer Funktion, legt die Funktion eine temporäre Kopie des Objekts an. Handelt es sich um ein benutzerdefiniertes Objekt, wird der Kopierkonstruktor der Klasse aufgerufen, wie Sie gestern in Listing 9.6 feststellen konnten.
Alle Kopierkonstruktoren übernehmen einen Parameter: eine Referenz auf ein Objekt derselben Klasse. Es empfiehlt sich, diese Referenz als konstant zu deklarieren, da der Konstruktor das übergebene Objekt nicht ändern muß. Zum Beispiel:
CAT(const CAT & theCat);
Hier übernimmt der CAT
-Konstruktor eine konstante Referenz auf ein existierendes
CAT
-Objekt. Ziel des Kopierkonstruktors ist das Anlegen einer Kopie von theCat
.
Der Standardkopierkonstruktor kopiert einfach jede Elementvariable von dem als Parameter übergebenen Objekt in die Elementvariablen des neuen Objekts. Man spricht hier von einer elementweisen (oder flachen) Kopie. Obwohl das bei den meisten Elementvariablen durchaus funktioniert, klappt das bei Elementvariablen, die Zeiger auf Objekte im Heap sind, schon nicht mehr.
Eine flache oder elementweise Kopie kopiert die Werte der Elementvariablen des einen Objekts in ein anderes Objekt. Zeiger in beiden Objekten verweisen danach auf denselben Speicher. Eine tiefe Kopie überträgt dagegen die auf dem Heap reservierten Werte in neu zugewiesenen Speicher.
Wenn die CAT
-Klasse eine Elementvariable itsAge
enthält, die auf einen Integer im
Heap zeigt, kopiert der Standardkopierkonstruktor die übergebene Elementvariable
itsAge
von CAT
in die neue Elementvariable itsAge
von CAT
. Die beiden Objekte zeigen
dann auf denselben Speicher, wie es Abbildung 10.1 verdeutlicht.
Abbildung 10.1: Arbeitsweise des Standardkopierkonstruktors
Dieses Verfahren führt zur Katastrophe, wenn eines der beiden CAT
-Objekte den Gültigkeitsbereich
verliert. Denn, wie Sie in Kapitel 8, »Zeiger«, gelernt haben, ist es die
Aufgabe des aufgerufenen Destruktors, den zugewiesenen Speicher aufzuräumen.
Nehmen wir im Beispiel an, daß das originale CAT
-Objekt den Gültigkeitsbereich verliert.
Der Destruktor dieses Objekts gibt den zugewiesenen Speicher frei. Die Kopie
zeigt aber weiterhin auf diesen Speicherbereich. Damit hat man einen vagabundierenden
Zeiger erzeugt, der eine reelle Gefahr für das Programm darstellt. Abbildung 10.2
zeigt diesen Problemfall.
Abbildung 10.2: Einen vagabundierenden Zeiger erzeugen
Die Lösung besteht darin, einen eigenen Kopierkonstruktor zu definieren und für die Kopie eigenen Speicher zu allokieren. Anschließend kann man die alten Werte in den neuen Speicher kopieren. Diesen Vorgang bezeichnet man als tiefe Kopie. Listing 10.5 zeigt ein Programm, das nach diesem Verfahren arbeitet.
Listing 10.5: Kopierkonstruktoren
1: // Listing 10.5
2: // Kopierkonstruktoren
3:
4: #include <iostream.h>
5:
6: class CAT
7: {
8: public:
9: CAT(); // Standardkonstruktor
10: CAT (const CAT &); // Kopierkonstruktor
11: ~CAT(); // Destruktor
12: int GetAge() const { return *itsAge; }
13: int GetWeight() const { return *itsWeight; }
14: void SetAge(int age) { *itsAge = age; }
15:
16: private:
17: int *itsAge;
18: int *itsWeight;
19: };
20:
21: CAT::CAT()
22: {
23: itsAge = new int;
24: itsWeight = new int;
25: *itsAge = 5;
26: *itsWeight = 9;
27: }
28:
29: CAT::CAT(const CAT & rhs)
30: {
31: itsAge = new int;
32: itsWeight = new int;
33: *itsAge = rhs.GetAge(); // oeffentlicher Zugriff
34: *itsWeight = *(rhs.itsWeight); // privater Zugriff
35: }
36:
37: CAT::~CAT()
38: {
39: delete itsAge;
40: itsAge = 0;
41: delete itsWeight;
42: itsWeight = 0;
43: }
44:
45: int main()
46: {
47: CAT frisky;
48: cout << "Alter von Frisky: " << frisky.GetAge() << endl;
49: cout << "Alter von Frisky auf 6 setzen...\n";
50: frisky.SetAge(6);
51: cout << "Boots aus Frisky erzeugen\n";
52: CAT boots(frisky);
53: cout << "Alter von Frisky: " << frisky.GetAge() << endl;
54: cout << "Alter von Boots: " << boots.GetAge() << endl;
55: cout << "Alter von Frisky auf 7 setzen...\n";
56: frisky.SetAge(7);
57: cout << "Alter von Frisky: " << frisky.GetAge() << endl;
58: cout << "Alter von Boots: " << boots.GetAge() << endl;
59: return 0;
60: }
Alter von Frisky: 5
Alter von Frisky auf 6 setzen...
Boots aus Frisky erzeugen
Alter von Frisky: 6
Alter von Boots: 6
Alter von Frisky auf 7 setzen...
Alter von Frisky: 7
Alter von Boots: 6
Die Zeilen 6 bis 19 deklarieren die Klasse CAT
. In Zeile 9 steht die Deklaration eines
Standardkonstruktors, in Zeile 10 die Deklaration eines Kopierkonstruktors.
Das Programm deklariert in den Zeilen 17 und 18 zwei Elementvariablen als Zeiger
auf int
-Werte. Normalerweise gibt es kaum einen Grund, daß eine Klasse Elementvariablen
vom Typ int
als Zeiger speichert. Hier aber soll dies verdeutlichen, wie man
Elementvariablen im Heap verwaltet.
Der Standardkonstruktor in den Zeilen 21 bis 27 reserviert im Heap Platz für zwei
int
-Variablen und weist ihnen dann Werte zu.
Der Kopierkonstruktor beginnt in Zeile 29. Der Parameter ist wie bei einem Kopierkonstruktor
üblich mit rhs
benannt, was für right-hand side - zur rechten Seite -
steht. (Bei den Zuweisungen, siehe Zeilen 33 und 34, steht das als Parameter übergebene
Objekt auf der rechten Seite des Gleichheitszeichens.) Der Kopierkonstruktor arbeitet
wie folgt:
In den Zeilen 31 und 32 wird Speicher auf dem Heap reserviert. Dann überträgt der
Kopierkonstruktor die Werte aus dem existierenden CAT
-Objekt in die neuen Speicherstellen
(Zeilen 33 und 34).
Der Parameter rhs
ist ein CAT
-Objekt, dessen Übergabe an den Kopierkonstruktor als
konstante Referenz erfolgt. Als CAT
-Objekt verfügt rhs
über die gleichen Elementvariablen
wie jedes andere CAT
-Objekt auch.
Jedes CAT
-Objekt kann auf die privaten Elementvariablen aller anderen CAT
-Objekte zugreifen.
Dennoch ist es guter Programmierstil, möglichst öffentliche Zugriffsmethoden
zu verwenden. Die Elementfunktion rhs.GetAge()
gibt den Wert aus dem Speicher zurück,
auf den die Elementvariable itsAge
von rhs
zeigt.
Abbildung 10.3 zeigt die Abläufe. Die Werte, auf die das existierende CAT
-Objekt verweist,
werden in den für das neue CAT
-Objekt zugewiesenen Speicher kopiert.
Zeile 47 erzeugt ein CAT
-Objekt namens frisky
. Zeile 48 gibt das Alter von frisky
aus
und setzt es dann in Zeile 50 auf den Wert 6
. Die Anweisung in Zeile 52 erzeugt mit
Hilfe des Kopierkonstruktors das neue CAT
-Objekt boots
und übergibt dabei frisky
als
Parameter. Hätte man frisky
als Parameter an eine Funktion übergeben, würde der
Compiler den gleichen Aufruf des Kopierkonstruktors ausführen.
Die Zeilen 53 und 54 geben das Alter beider CAT
-Objekte aus. Wie erwartet hat boots
das Alter von frisky
(6
) und nicht den Standardwert von 5
. Zeile 56 setzt das Alter
von frisky
auf 7
, und Zeile 57 gibt erneut das Alter aus. Dieses Mal ist das Alter von
frisky
gleich 7
, während das Alter von boots
bei 6
bleibt. Damit ist nachgewiesen, daß
sich die Objekte in separaten Speicherbereichen befinden.
Wenn die CAT
-Objekte ihren Gültigkeitsbereich verlieren, findet automatisch der Aufruf
ihrer Destruktoren statt. Die Implementierung des CAT
-Destruktors ist in den Zeilen
37 bis 43 zu finden. Der Aufruf von delete
für die beiden Zeiger itsAge
und itsWeight
gibt den zugewiesenen Speicher an den Heap zurück. Aus Sicherheitsgründen
wird beiden Zeigern der Wert NULL
zugewiesen.
C++ verfügt über eine Reihe vordefinierter Typen, beispielsweise int
, float
oder
char
. Zu jedem dieser Typen gehören verschiedene vordefinierte Operatoren wie Addition
(+
) und Multiplikation (*
). In C++ können Sie diese Operatoren auch in eigene
Klassen aufnehmen.
Listing 10.6 erzeugt die neue Klasse Counter
, anhand der wir das Überladen von Operatoren
umfassend untersuchen werden. Ein Counter
-Objekt realisiert Zählvorgänge
für Schleifen und andere Konstruktionen, in denen man eine Zahl inkrementieren, dekrementieren
oder in ähnlicher Weise schrittweise verändern muß.
Listing 10.6: Die Klasse Counter
1: // Listing 10.6
2: // Die Klasse Counter
3:
4:
5: #include <iostream.h>
6:
7: class Counter
8: {
9: public:
10: Counter();
11: ~Counter(){}
12: int GetItsVal()const { return itsVal; }
13: void SetItsVal(int x) {itsVal = x; }
14:
15: private:
16: int itsVal;
17:
18: };
19:
20: Counter::Counter():
21: itsVal(0)
22: {};
23:
24: int main()
25: {
26: Counter i;
27: cout << "Wert von i: " << i.GetItsVal() << endl;
28: return 0;
29: }
Wert von i ist 0.
Die in den Zeilen 7 bis 18 definierte Klasse ist eigentlich recht nutzlos. Die einzige Elementvariable
ist vom Typ int
. Der in Zeile 10 deklarierte und in Zeile 20 implementierte
Standardkonstruktor initialisiert die Elementvariable itsVal
mit 0
.
Im Gegensatz zu einem echten, vordefinierten »Vollblut«-int
läßt sich das Counter
-Objekt
nicht inkrementieren, nicht dekrementieren, nicht addieren und weder zuweisen
noch anderweitig manipulieren. Dafür gestaltet sich die Ausgabe seines Wertes wesentlich
schwieriger!
Durch das Überladen von Operatoren kann man einen großen Teil der Standardfunktionalität
wiederherstellen, die benutzerdefinierten Klassen wie Counter
verwehrt
bleibt. Listing 10.7 zeigt, wie man eine Inkrement-Methode schreibt.
Listing 10.7: Einen Inkrement-Operator hinzufügen
1: // Listing 10.7
2: // Die Klasse Counter
3:
4:
5: #include <iostream.h>
6:
7: class Counter
8: {
9: public:
10: Counter();
11: ~Counter(){}
12: int GetItsVal()const { return itsVal; }
13: void SetItsVal(int x) {itsVal = x; }
14: void Increment() { ++itsVal; }
15:
16: private:
17: int itsVal;
18:
19: };
20:
21: Counter::Counter():
22: itsVal(0)
23: {}
24:
25: int main()
26: {
27: Counter i;
28: cout << "Wert von i: " << i.GetItsVal() << endl;
29: i.Increment();
30: cout << "Wert von i: " << i.GetItsVal() << endl;
31: return 0;
32: }
Wert von i ist 0
Wert von i ist 1
Listing 10.7 fügt eine Increment
-Funktion hinzu, die in Zeile 14 definiert ist. Das funktioniert
zwar, ist aber etwas mühsam. Das Programm schreit förmlich nach einem ++-
Operator, der sich im übrigen problemlos realisieren läßt.
Präfix-Operatoren lassen sich überladen, indem man Funktionen der folgenden Form deklariert:
rueckgabetyp operator op (parameter)
In diesem Beispiel ist op
der zu überladende Operator. Demnach kann der ++-Operator
mit folgender Syntax überladen werden:
void operator++ ()
In Listing 10.8 sehen Sie ein Anwendungsbeispiel.
Listing 10.8: operator++ überladen
1: // Listing 10.8
2: // Die Klasse Counter
3:
4:
5: #include <iostream.h>
6:
7: class Counter
8: {
9: public:
10: Counter();
11: ~Counter(){}
12: int GetItsVal()const { return itsVal; }
13: void SetItsVal(int x) {itsVal = x; }
14: void Increment() { ++itsVal; }
15: void operator++ () { ++itsVal; }
16:
17: private:
18: int itsVal;
19:
20: };
21:
22: Counter::Counter():
23: itsVal(0)
24: {}
25:
26: int main()
27: {
28: Counter i;
29: cout << "Wert von i ist " << i.GetItsVal() << endl;
30: i.Increment();
31: cout << "Wert von i ist " << i.GetItsVal() << endl;
32: ++i;
33: cout << "Wert von i ist " << i.GetItsVal() << endl;
34: return 0;
35: }
Wert von i ist 0
Wert von i ist 1
Wert von i ist 2
Zeile 15 überlädt den operator++
, der in Zeile 32 zum Einsatz kommt. Dies entspricht
auch viel eher der Syntax, die man für ein Counter
-Objekt erwarten würde. An dieser
Stelle erwägen Sie vielleicht, die zusätzlichen Aufgaben unterzubringen, für die Counter
überhaupt erst erzeugt wurde - beispielsweise den Überlauf des Counter
s abzufangen.
In unserer Implementierung des Inkrement-Operators gibt es jedoch einen groben
Fehler. Wenn Sie den Counter
auf die rechte Seite der Zuweisung stellen, funktioniert
er nicht. Zum Beispiel
Counter a = ++i;
Dieser Code soll einen neuen Counter
a
erzeugen und ihm dann den Wert in i
nach
seiner Inkrementierung zuweisen. Der vordefinierte Kopierkonstruktor ist für die Zuweisung
zuständig, aber der aktuelle Inkrement-Operator liefert kein Counter
-Objekt,
sondern void
zurück, und Sie können einem Counter
-Objekt kein void
-Objekt zuweisen.
(Es ist nicht möglich, aus nichts etwas zu machen!)
Sie wollen natürlich ein Counter
-Objekt zurückliefern, das dann einem anderen Counter
-Objekt zugewiesen werden kann. Welches Objekt sollte zurückgegeben werden?
Ein Ansatz wäre es, ein temporäres Objekt zu erzeugen und dieses dann zurückzugeben.
Listing 10.9 veranschaulicht diesen Ansatz.
Listing 10.9: Ein temporäres Objekt zurückgeben
1: // Listing 10.9
2: // operator++ gibt ein temporaeres Objekt zurück
3:
4: int
5: #include <iostream.h>
6:
7: class Counter
8: {
9: public:
10: Counter();
11: ~Counter(){}
12: int GetItsVal()const { return itsVal; }
13: void SetItsVal(int x) {itsVal = x; }
14: void Increment() { ++itsVal; }
15: Counter operator++ ();
16:
17: private:
18: int itsVal;
19:
20: };
21:
22: Counter::Counter():
23: itsVal(0)
24: {}
25:
26: Counter Counter::operator++()
27: {
28: ++itsVal;
29: Counter temp;
30: temp.SetItsVal(itsVal);
31: return temp;
32: }
33:
34: int main()
35: {
36: Counter i;
37: cout << "Wert von i ist " << i.GetItsVal() << endl;
38: i.Increment();
39: cout << "Wert von i ist " << i.GetItsVal() << endl;
40: ++i;
41: cout << "Wert von i ist " << i.GetItsVal() << endl;
42: Counter a = ++i;
43: cout << "Wert von a: " << a.GetItsVal();
44: cout << " und von i: " << i.GetItsVal() << endl;
45: return 0;
46: }
Wert von i ist 0
Wert von i ist 1
Wert von i ist 2
Wert von a: 3 und von i: 3
In dieser Version deklariert Zeile 15 einen operator++
, der ein Counter
-Objekt zurückgibt.
Zeile 29 erzeugt eine temporäre Variable temp
, deren Wert auf den Wert des aktuellen
Objekts gesetzt wird. Diese temporäre Variable wird zurückgeliefert und in Zeile
42 a
zugewiesen.
Es besteht absolut kein Grund, einen Namen für das temporäre Objekt in Zeile 29 zu
vergeben. Wenn Counter
einen Konstruktor hätte, der einen Wert übernehmen würde,
könnten Sie einfach das Ergebnis dieses Konstruktors als Rückgabewert des Inkrement-Operators
zurückliefern. Zum besseren Verständnis gebe ich Ihnen ein Programmbeispiel.
Listing 10.10: Ein namenloses temporäres Objekt zurückliefern
1: // Listing 10.10 - operator++ liefert
2: // ein namenloses temporaeres Objekt zurück
3:
4: int
5: #include <iostream.h>
6:
7: class Counter
8: {
9: public:
10: Counter();
11: Counter(int val);
12: ~Counter(){}
13: int GetItsVal()const { return itsVal; }
14: void SetItsVal(int x) {itsVal = x; }
15: void Increment() { ++itsVal; }
16: Counter operator++ ();
17:
18: private:
19: int itsVal;
20:
21: };
22:
23: Counter::Counter():
24: itsVal(0)
25: {}
26:
27: Counter::Counter(int val):
28: itsVal(val)
29: {}
30:
31: Counter Counter::operator++()
32: {
33: ++itsVal;
34: return Counter (itsVal);
35: }
36:
37: int main()
38: {
39: Counter i;
40: cout << "Wert von i ist " << i.GetItsVal() << endl;
41: i.Increment();
42: cout << "Wert von i ist " << i.GetItsVal() << endl;
43: ++i;
44: cout << "Wert von i ist " << i.GetItsVal() << endl;
45: Counter a = ++i;
46: cout << "Wert von a: " << a.GetItsVal();
47: cout << " und von i: " << i.GetItsVal() << endl;
48: return 0;
49: }
Wert von i ist 0
Wert von i ist 1
Wert von i ist 2
Wert von a: 3 und von i: 3
Zeile 11 deklariert einen neuen Konstruktor, der einen int
-Wert übernimmt. Die Zeilen
27 bis 29 enthalten die Implementierung. Sie initialisiert itsVal
mit dem übergebenen
Wert.
Die Implementierung von operator++
wird jetzt vereinfacht. Zeile 33 inkrementiert
itsVal
. Anschließend erzeugt Zeile 34 ein temporäres Counter
-Objekt, initialisiert es
mit dem Wert in itsVal
und liefert das Objekt dann als Ergebnis von operator
++ zurück.
Diese Lösung ist wesentlich eleganter. Doch immer noch stellt sich die Frage »Warum
überhaupt ein temporäres Objekt erzeugen?« Denken Sie daran, daß jedes temporäre
Objekt erst erzeugt und später zerstört werden muß - beides aufwendige Operationen.
Außerdem gibt es das Objekt i
bereits, und den richtigen Wert hat es auch. Warum
nicht das Objekt i
zurückliefern? Wir werden das Problem mit Hilfe des this
-Zeigers
lösen.
Der this
-Zeiger wird, wie gestern beschrieben, der operator++
-Elementfunktion - wie
jeder anderen Elementfunktion auch - intern übergeben. Der this
-Zeiger zeigt auf i
,
und wenn er dereferenziert wird, liefert er das Objekt i
zurück, das bereits in seiner
Elementvariablen itsVal
den korrekten Wert enthält. Listing 10.11 veranschaulicht
die Rückgabe des dereferenzierten this
-Zeigers. Dadurch wird die Erzeugung eines
nicht benötigten temporären Objekts vermieden.
Listing 10.11: Rückgabe des this-Zeigers
1: // Listing 10.11
2: // Rueckgabe des dereferenzierten this-Zeigers
3:
4: int
5: #include <iostream.h>
6:
7: class Counter
8: {
9: public:
10: Counter();
11: ~Counter(){}
12: int GetItsVal()const { return itsVal; }
13: void SetItsVal(int x) {itsVal = x; }
14: void Increment() { ++itsVal; }
15: const Counter& operator++ ();
16:
17: private:
18: int itsVal;
19:
20: };
21:
22: Counter::Counter():
23: itsVal(0)
24: {};
25:
26: const Counter& Counter::operator++()
27: {
28: ++itsVal;
29: return *this;
30: }
31:
32: int main()
33: {
34: Counter i;
35: cout << "Wert von i ist " << i.GetItsVal() << endl;
36: i.Increment();
37: cout << "Wert von i ist " << i.GetItsVal() << endl;
38: ++i;
39: cout << "Wert von i ist " << i.GetItsVal() << endl;
40: Counter a = ++i;
41: cout << " Wert von a: " << a.GetItsVal();
42: cout << " und von i: " << i.GetItsVal() << endl;
43: return 0;
44: }
Wert von i ist 0
Wert von i ist 1
Wert von i ist 2
Wert von a: 3 und von i: 3
Die Implementierung von operator
++ in den Zeilen 26 bis 30 wurde dahingehend geändert,
daß nun der this
-Zeiger dereferenziert und das aktuelle Objekt zurückgegeben
wird. Damit erhält man ein Counter
-Objekt, das man a
zuweisen kann. Wenn die Klasse
Counter
Speicher für ihre Objekte allokieren würde, hätte man noch den Kopierkonstruktor
überschreiben müssen (siehe Erläuterungen weiter oben). Für unser Beispiel
reicht der Standardkopierkonstruktor.
Beachten Sie, daß der zurückgelieferte Wert eine Counter
-Referenz ist. Damit wird die
Erzeugung eines zusätzlichen temporären Objekts vermieden. Wir verwenden eine
konstante Referenz, da der Wert nicht von der Funktion, die Counter
verwendet, geändert
werden soll.
Bis jetzt haben wir nur den Präfix-Operator überladen. Was wäre nun, wenn Sie den
Inkrement-Operator in der Postfix-Version überladen möchten? Hier hat der Compiler
ein Problem. Wie kann er zwischen Präfix und Postfix unterscheiden? Per Konvention
nimmt man eine int
-Variable als Parameter in die Operator-Deklaration auf. Der
Wert des Parameters wird ignoriert - er dient nur als Signal, daß es sich um den Postfix-Operator
handelt.
Bevor man den Postfix-Operator aufsetzen kann, sollte man den Unterschied zum Präfix-Operator kennen. Wir sind im Detail bereits in Kapitel 4, »Ausdrücke und Anweisungen«, darauf eingegangen (siehe Listing 4.3).
Zur Erinnerung, Präfix heißt »inkrementiere und hole dann«, während Postfix »hole und inkrementiere dann« bedeutet.
Demnach kann der Präfix-Operator einfach den Wert inkrementieren und dann das Objekt selbst zurückgeben, während der Postfix-Operator den Wert zurückgeben muß, der vor der Inkrementierung vorhanden war. Dazu ist letztendlich
Schauen wir uns das noch einmal genauer an. Schreibt man
a = x++;
und hatte x
den Wert 5
, enthält a
nach dieser Anweisung den Wert 5
und x
den Wert
6
. Zunächst wird der Wert aus x
geholt und an a
zugewiesen. Daran schließt sich die
Inkrementierung von x
an. Wenn x
ein Objekt ist, muß der Postfix-Operator den Originalwert
(5
) in einem temporären Objekt aufbewahren, den Wert von x
auf 6
inkrementieren
und dann das temporäre Objekt zurückgeben, um dessen Wert an a
zuzuweisen.
Da das temporäre Objekt bei Rückkehr der Funktion den Gültigkeitsbereich verliert, ist es als Wert und nicht als Referenz zurückzugeben.
Listing 10.12 demonstriert die Verwendung der Präfix- und Postfix-Operatoren.
Listing 10.12: Präfix- und Postfix-Operatoren
1: // Listing 10.12
2: // Gibt den dereferenzierten this-Zeiger zurueck
3:
4: int
5: #include <iostream.h>
6:
7: class Counter
8: {
9: public:
10: Counter();
11: ~Counter(){}
12: int GetItsVal()const { return itsVal; }
13: void SetItsVal(int x) {itsVal = x; }
14: const Counter& operator++ (); // Präfix
15: const Counter operator++ (int); // Postfix
16:
17: private:
18: int itsVal;
19: };
20:
21: Counter::Counter():
22: itsVal(0)
23: {}
24:
25: const Counter& Counter::operator++()
26: {
27: ++itsVal;
28: return *this;
29: }
30:
31: const Counter Counter::operator++(int)
32: {
33: Counter temp(*this);
34: ++itsVal;
35: return temp;
36: }
37:
38: int main()
39: {
40: Counter i;
41: cout << "Wert von i ist " << i.GetItsVal() << endl;
42: i++;
43: cout << "Wert von i ist " << i.GetItsVal() << endl;
44: ++i;
45: cout << "Wert von i ist " << i.GetItsVal() << endl;
46: Counter a = ++i;
47: cout << "Wert von a: " << a.GetItsVal();
48: cout << " und von i: " << i.GetItsVal() << endl;
49: a = i++;
50: cout << "Wert von a: " << a.GetItsVal();
51: cout << " und von i: " << i.GetItsVal() << endl;
52: return 0;
53: }
Wert von i ist 0
Wert von i ist 1
Wert von i ist 2
Wert von a: 3 und von i: 3
Wert von a: 4 und von i: 4
Die Deklaration des Postfix-Operators steht in Zeile 15 und die Implementierung in
den Zeilen 31 bis 36. Beachten Sie, daß der Aufruf des Präfix-Operators in Zeile 14
keinen int
-Parameter x enthält, sondern die normale Syntax verwendet. Der Postfix-
Operator zeigt durch seinen int
-Parameter x an, daß er der Postfix- und nicht der Präfix-Operator
ist. Der Wert x wird nicht weiter benötigt.
Die Deklaration eines überladenen Operators unterscheidet sich nicht von der einer Funktion. Erst steht das Schlüsselwort
operator
gefolgt von dem zu überladenden Operator. Unäre Operatoren übernehmen keine Parameter, mit Ausnahme des Postfix-Operators zum Inkrementieren und Dekrementieren, der einen Integer als Flag übernimmt:const Counter& Counter::operator++ ();Counter Counter::operator-(int);
Der Inkrement-Operator ist ein unärer Operator, wirkt also nur auf ein Objekt. Dagegen
ist der Additionsoperator (+
) ein binärer Operator, da er zwei Objekte verknüpft.
Wie überlädt man nun den Additionsoperator für Counter
?
Man muß in der Lage sein, zwei Counter
-Variablen anzugeben und diese zu addieren,
wie es folgendes Beispiel zeigt:
Counter varEins, varZwei, varDrei;
varDrei = varEins + varZwei;
Auch hier beginnen wir damit, eine Funktion Add()
aufzusetzen, die ein Counter
-Objekt
als Argument übernimmt, die Werte addiert und dann ein Counter
-Objekt mit dem
Ergebnis zurückgibt. Listing 10.13 zeigt diese Lösung.
Listing 10.13: Die Funktion Add()
1: // Listing 10.13
2: // Die Funktion Add
3:
4: int
5: #include <iostream.h>
6:
7: class Counter
8: {
9: public:
10: Counter();
11: Counter(int initialValue);
12: ~Counter(){}
13: int GetItsVal()const { return itsVal; }
14: void SetItsVal(int x) {itsVal = x; }
15: Counter Add(const Counter &);
16:
17: private:
18: int itsVal;
19:
20: };
21:
22: Counter::Counter(int initialValue):
23: itsVal(initialValue)
24: {}
25:
26: Counter::Counter():
27: itsVal(0)
28: {}
29:
30: Counter Counter::Add(const Counter & rhs)
31: {
32: return Counter(itsVal+ rhs.GetItsVal());
33: }
34:
35: int main()
36: {
37: Counter varOne(2), varTwo(4), varThree;
38: varThree = varOne.Add(varTwo);
39: cout << "varOne: " << varOne.GetItsVal()<< endl;
40: cout << "varTwo: " << varTwo.GetItsVal() << endl;
41: cout << "varThree: " << varThree.GetItsVal() << endl;
42:
43: return 0;
44: }
varOne: 2
varTwo: 4
varThree: 6
Die Deklaration der Funktion Add()
steht in Zeile 15. Die Funktion übernimmt eine
konstante Counter
-Referenz. Diese stellt den Wert dar, der dem aktuellen Objekt hinzuaddiert
werden soll. Die Funktion gibt ein Counter
-Objekt zurück, das als Ergebnis
auf der linken Seite von Zuweisungen stehen kann (wie in Zeile 38). Das heißt, varOne
ist das Objekt, varTwo
ist der Parameter der Funktion Add()
, und das Ergebnis weist
man an varThree
zu.
Um varThree
ohne Angabe eines anfänglichen Wertes erzeugen zu können, ist ein
Standardkonstruktor erforderlich. Der Standardkonstruktor initialisiert itsVal
mit 0,
wie es aus den Zeilen 26 bis 28 ersichtlich ist. Da varOne
und varTwo
mit einem Wert
ungleich Null zu initialisieren sind, wurde ein weiterer Konstruktor aufgesetzt (Zeilen
22 bis 24). Eine andere Lösung für dieses Problem wäre die Bereitstellung des Standardwertes
0 für den in Zeile 11 deklarierten Konstruktor.
Die Funktion Add()
selbst ist in den Zeilen 30 bis 33 zu sehen. Sie funktioniert zwar,
ihre Verwendung ist aber eher ungewöhnlich. Das Überladen von +-operator
würde
eine natürlichere Verwendung der Counter
-Klasse ermöglichen. Listing 10.14 zeigt
diese Lösung.
1: // Listing 10.14
2: // operator+ ueberladen
3:
4:
5: #include <iostream.h>
6:
7: class Counter
8: {
9: public:
10: Counter();
11: Counter(int initialValue);
12: ~Counter(){}
13: int GetItsVal()const { return itsVal; }
14: void SetItsVal(int x) {itsVal = x; }
15: Counter operator+ (const Counter &);
16: private:
17: int itsVal;
18: };
19:
20: Counter::Counter(int initialValue):
21: itsVal(initialValue)
22: {}
23:
24: Counter::Counter():
25: itsVal(0)
26: {}
27:
28: Counter Counter::operator+ (const Counter & rhs)
29: {
30: return Counter(itsVal + rhs.GetItsVal());
31: }
32:
33: int main()
34: {
35: Counter varOne(2), varTwo(4), varThree;
36: varThree = varOne + varTwo;
37: cout << "varOne: " << varOne.GetItsVal()<< endl;
38: cout << "varTwo: " << varTwo.GetItsVal() << endl;
39: cout << "varThree: " << varThree.GetItsVal() << endl;
40:
41: return 0;
42: }
varOne: 2
varTwo: 4
varThree: 6
Die Deklaration von operator+
finden Sie in Zeile 15 und die Definition in den Zeilen
28 bis 31. Vergleichen Sie das mit der Deklaration und der Definition der Funktion
Add()
im vorherigen Listing - sie sind nahezu identisch. Die Syntax unterscheidet sich
allerdings grundlegend. Gegenüber der Anweisung
varThree = varOne.Add(varTwo);
ist die folgende Formulierung natürlicher:
varThree = varOne + varTwo;
Durch diese kleine Änderung läßt sich das Programm einfacher anwenden und besser verstehen.
Die Techniken zum Überladen von
operator++
kann auch auf andere unäre Operatoren, wie zum Beispieloperator--
übertragen werden.
Binäre Operatoren werden wie unäre Operatoren erzeugt - mit der Ausnahme, daß sie einen Parameter übernehmen. Bei dem Parameter handelt es sich um eine konstante Referenz auf ein Objekt des gleichen Typs.
Counter Counter::operator+ (const Counter & rhs);Counter Counter::operator- (const Counter & rhs);
Überladene Operatoren können, wie in diesem Kapitel beschrieben, in der Form von
Elementfunktionen auftreten, aber auch als Nicht-Elementfunktionen. Auf letzteres
werde ich noch in Kapitel 14, »Spezielle Themen zu Klassen und Funktionen«, im Zusammenhang
mit den friend
-Funktionen näher eingehen.
Die einzigen Operatoren, die nur als Klassenelemente definiert werden können, sind
die Operatoren für Zuweisung (=
), Subskription ([]
), Funktionsaufruf (()
) und Indirektion
(->
).
Der Operator []
wird morgen zusammen mit den Arrays erläutert. Das Überladen des
Operators ->
wird in Kapitel 14 in Verbindung mit den »Intelligenten Zeigern« erklärt.
Operatoren von vordefinierten Typen (wie zum Beispiel int
) lassen sich nicht überladen.
Des weiteren kann man weder die Rangfolge noch die Art - unär oder binär -
des Operators ändern. Es lassen sich auch keine neuen Operatoren definieren. Beispielsweise
ist es nicht möglich, **
als »Potenz«-Operator zu deklarieren.
Mit der »Art« des Operators ist gemeint, wie viele Operanden der Operator aufweist.
Einge C++-Operatoren sind unär und haben nur einen Operanden (meinWert++
). Andere
Operatoren sind binär und verwenden zwei Operanden (a+b
). Es gibt nur einen
ternären Operator, und der benötigt drei Operanden: der ?
-Operator (a > b ? x : y
).
Das Überladen von Operatoren ist eines der Konzepte von C++, das neue Programmierer zu häufig und oft mißbräuchlich anwenden. Es ist zwar verlockend, neue und interessante Einsatzfälle für die ungewöhnlicheren Operatoren auszuprobieren, dies führt aber unweigerlich zu einem Code, der verwirrend und schwer zu lesen ist.
Es kann natürlich lustig sein, den +
-Operator zur Subtraktion und den *
-Operator zur
Addition zu »überreden«. Ein professioneller Programmierer ist aber über derartige
Späße erhaben. Die größere Gefahr liegt in der zwar gutgemeinten, aber unüblichen
Verwendung eines Operators - zum Beispiel +
für die Verkettung einer Zeichenfolge
oder /
für die Teilung eines Strings. Manchmal mag das sinnvoll sein, trotzdem sollte
man hier Vorsicht walten lassen. Rufen wir uns das Ziel beim Überladen von Operatoren
ins Bewußtsein: die Brauchbarkeit und Verständlichkeit zu erhöhen.
Überladen Sie Operatoren nur, wenn es das Programm leichter verständlich macht. Lassen Sie den überladenen Operator ein Objekt der Klasse zurückliefern. |
Als vierte und letzte Standardfunktion stellt der Compiler den Zuweisungsoperator
(operator=
) zur Verfügung, wenn man keinen eigenen spezifiziert.
Der Aufruf dieses Operators erfolgt bei der Zuweisung eines Objekts. Dazu folgendes Beispiel:
CAT ersteKatze(5,7);
CAT zweiteKatze(3,4);
// ... hier steht irgendein Code
zweiteKatze = ersteKatze;
Diese Anweisungen erzeugen ersteKatze
und initialisieren itsAge
mit 5
und itsWeight
mit 7
. Es schließt sich die Erzeugung von zweiteKatze
mit der Zuweisung der Werte 3
und 4
an.
Nach einiger Zeit werden catTwo
die Werte in ersteKatze
zugewiesen. Dabei stellen
sich zwei Fragen: Was passiert, wenn itsAge
ein Zeiger ist und was passiert mit dem
Originalwert in zweiteKatze
?
Wie man mit Elementvariablen verfährt, die ihre Werte auf dem Heap ablegen, wurde bereits bei der Behandlung des Kopierkonstruktors diskutiert. Hier stellen sich die gleichen Probleme wie sie in den Abbildungen 10.1 und 10.2 illustriert sind.
C++-Programmierer unterscheiden zwischen einer flachen - oder elementweisen - Kopie auf der einen Seite und einer tiefen - oder vollständigen - Kopie auf der anderen. Eine flache Kopie kopiert einfach die Elemente, und beide Objekte zeigen schließlich auf denselben Bereich im Heap. Eine tiefe Kopie reserviert einen neuen Speicherbereich. Sehen Sie sich dazu gegebenenfalls noch einmal Abbildung 10.3 an.
Das gleiche Problem wie beim Kopierkonstruktor tritt auch hier bei der Zuweisung zutage.
Hier gibt es allerdings noch eine weitere Komplikation. Das Objekt zweiteKatze
existiert bereits und hat Speicher reserviert. Diesen Speicher muß man löschen, wenn
man Speicherlücken vermeiden möchte. Was passiert aber, wenn man zweiteKatze
an sich selbst wie folgt zuweist:
zweiteKatze = zweiteKatze;
Kaum jemand schreibt so etwas absichtlich, doch das Programm muß diesen Fall behandeln können. Derartige Anweisungen können nämlich auch zufällig entstehen, wenn referenzierte und dereferenzierte Zeiger die Tatsache verdecken, daß die Zuweisung des Objekts auf sich selbst vorliegt.
Wenn man dieses Problem nicht umsichtig behandelt, löscht zweiteKatze
die eigene
Speicherzuweisung. Steht dann das Kopieren von der rechten Seite der Zuweisung an
die linke an, haben wir ein Problem: Der Speicher ist nicht mehr vorhanden.
Zur Absicherung muß der Zuweisungsoperator prüfen, ob auf der rechten Seite das
Objekt selbst steht. Dazu untersucht er den Zeiger this
. Listing 10.15 zeigt eine Klasse
mit einem eigenen Zuweisungsoperator.
Listing 10.15: Ein Zuweisungsoperator
1: // Listing 10.15
2: // Kopierkonstruktoren
3:
4: #include <iostream.h>
5:
6: class CAT
7: {
8: public:
9: CAT(); // Standardkonstruktor
10: // Aus Platzgruenden auf Kopierkonstruktor und Destruktor verzichtet!
11: int GetAge() const { return *itsAge; }
12: int GetWeight() const { return *itsWeight; }
13: void SetAge(int age) { *itsAge = age; }
14: CAT & operator=(const CAT &);
15:
16: private:
17: int *itsAge;
18: int *itsWeight;
19: };
20:
21: CAT::CAT()
22: {
23: itsAge = new int;
24: itsWeight = new int;
25: *itsAge = 5;
26: *itsWeight = 9;
27: }
28:
29:
30: CAT & CAT::operator=(const CAT & rhs)
31: {
32: if (this == &rhs)
33: return *this;
34: *itsAge = rhs.GetAge();
35: *itsWeight = rhs.GetWeight();
36: return *this;
37: }
38:
39:
40: int main()
41: {
42: CAT frisky;
43: cout << "Alter von Frisky: " << frisky.GetAge() << endl;
44: cout << "Alter von Frisky auf 6 setzen...\n";
45: frisky.SetAge(6);
46: CAT whiskers;
47: cout << "Alter von Whiskers: " << whiskers.GetAge() << endl;
48: cout << "Frisky in Whiskers kopieren...\n";
49: whiskers = frisky;
50: cout << "Alter von Whiskers: " << whiskers.GetAge() << endl;
51: return 0;
52: }
Alter von Frisky: 5
Alter von Frisky auf 6 setzen...
Alter von Whiskers: 5
Frisky in Whiskers kopieren...
Alter von Whiskers: 6
Listing 10.15 enthält die bekannte CAT
-Klasse, verzichtet aber aus Platzgründen auf
den Kopierkonstruktor und den Destruktor. In Zeile 14 steht die Deklaration des Zuweisungsoperators,
in den Zeilen 30 bis 37 die Definition.
Der Test in Zeile 32 prüft, ob das aktuelle Objekt (das heißt, das auf der linken Seite der
Zuweisung stehende CAT
-Objekt) dasselbe ist, wie das zuzuweisende CAT
-Objekt. Dazu
vergleicht man die Adresse von rhs
mit der im Zeiger this
gespeicherten Adresse.
Den Gleichheitsoperator (==
) kann man natürlich ebenfalls überladen und damit selbst
festlegen, was Gleichheit bei Objekten zu bedeuten hat.
Was passiert, wenn man eine Variable eines vordefinierten Typs wie etwa int
oder
unsigned short
einem Objekt einer benutzerdefinierten Klasse zuweist? Listing 10.16
bedient sich wieder der Counter
-Klasse und versucht, eine Variable vom Typ int
an
ein Counter
-Objekt zuzuweisen.
Listing 10.16: Versuch, einem Zähler einen int-Wert zuzuweisen
1: // Listing 10.16
2: // Dieser Code laesst sich nicht kompilieren!
3:
4:
5: #include <iostream.h>
6:
7: class Counter
8: {
9: public:
10: Counter();
11: ~Counter(){}
12: int GetItsVal()const { return itsVal; }
13: void SetItsVal(int x) {itsVal = x; }
14: private:
15: int itsVal;
16:
17: };
18:
19: Counter::Counter():
20: itsVal(0)
21: {}
22:
23: int main()
24: {
25: int theShort = 5;
26: Counter theCtr = theShort;
27: cout << "theCtr: " << theCtr.GetItsVal() << endl;
28: return 0;
29: }
Compiler-Fehler: 'int' kann nicht in 'class Counter' konvertiert werden
Die in den Zeilen 7 bis 17 deklarierte Klasse Counter
hat nur einen Standardkonstruktor
und deklariert keine besondere Methode für die Umwandlung eines int
in ein
Counter
-Objekt, so daß Zeile 26 einen Compiler-Fehler produziert. Der Compiler
kann nicht erkennen, daß der Wert einer angegebenen int
-Variablen an die Elementvariable
itsVal
zuzuweisen ist, sofern man das nicht ausdrücklich spezifiziert.
Zu diesem Zweck erzeugt die korrigierte Lösung in Listing 10.17 einen Umwandlungsoperator:
einen Konstruktor, der einen int
übernimmt und ein Counter
-Objekt
produziert.
Listing 10.17: Konvertierung eines int in ein Counter-Objekt
1: // Listing 10.17
2: // Konstruktor als Umwandlungsoperator
3:
4: int
5: #include <iostream.h>
6:
7: class Counter
8: {
9: public:
10: Counter();
11: Counter(int val);
12: ~Counter(){}
13: int GetItsVal()const { return itsVal; }
14: void SetItsVal(int x) {itsVal = x; }
15: private:
16: int itsVal;
17:
18: };
19:
20: Counter::Counter():
21: itsVal(0)
22: {}
23:
24: Counter::Counter(int val):
25: itsVal(val)
26: {}
27:
28:
29: int main()
30: {
31: int theShort = 5;
32: Counter theCtr = theShort;
33: cout << "theCtr: " << theCtr.GetItsVal() << endl;
34: return 0;
35: }
theCtr: 5
Als wesentliche Änderung wird in Zeile 11 der Konstruktor überladen, um einen int
zu übernehmen. Die Implementierung des Konstruktors, der ein Counter
-Objekt aus
einem int
erzeugt, steht in den Zeilen 24 bis 26.
Mit diesen Angaben kann der Compiler den Konstruktor - der einen int
als Argument
übernimmt - aufrufen. Und zwar folgendermaßen:
Schritt 1: Ein Counter-Objekt namens theCtr
erzeugen
Das ist das gleiche, als wenn man sagt int x = 5;
womit man eine Integer-Variable x
erzeugt und mit dem Wert 5
initialisiert. In diesem Fall erzeugen wir ein Counter
-Objekt
theCtr
und initialisieren es mit der Integer-Variable theShort
vom Typ short
.
Schritt 2: theCtr
den Wert von theShort
zuweisen.
Aber theShort
ist vom Typ short
und kein Counter
! Wir müssen es erst in ein Counter
-
Objekt umwandeln. Bestimmte Umwandlungen versucht der Compiler automatisch
vorzunehmen, Sie müssen ihm jedoch zeigen wie. Teilen Sie dem Compiler mit, wie
die Umwandlung zu erfolgen hat, indem Sie einen Konstruktor für Counter
erzeugen,
der einen einzigen Parameter übernimmt - zum Beispiel vom Typ short
:
class Counter
{
Counter (short int x);
//....
};
Dieser Konstruktor erzeugt Counter
-Objekte auf der Grundlage von short-
Werten. Zu
diesem Zweck erzeugt er ein temporäres und namenloses Counter
-Objekt. Stellen Sie
sich zur Veranschaulichung vor, daß das aus short
erzeugte temporäre Counter
-Objekt
den Namen wasShort
trägt.
Schritt 3: wasShort
dem Counter-Objekt theCtr
zuweisen, entsprechend
"theCtr = wasShort";
In diesem Schritt steht wasShort
(das temporäre Objekt, das erzeugt wurde, als der
Konstruktor ausgeführt wurde) für das, was rechts vom Zuweisungsoperator stand.
Das heißt, jetzt, da der Compiler ein temporäres Objekt für Sie erzeugt hat, initialisiert
er theCtr
damit.
Um dies zu verstehen, müssen Sie wissen, daß das Überladen ALLER Operatoren auf
die gleiche Art und Weise erfolgt - Sie deklarieren einen überladenen Operator mit
dem Schlüsselwort operator
. Bei binären Operatoren (wie =
oder +
) wird die Variable
auf der rechten Seite zum Parameter. Dies wird vom Konstruktor erledigt. Demzufolge
wird
a = b;
a.operator= (b);
Was passiert jedoch, wenn Sie versuchen, die Zuweisung mit folgenden Schritten rückgängig zu machen?
1: Counter theCtr(5);
2: USHORT theShort = theCtr;
3: cout << "theShort : " << theShort << endl;
Wieder erhält man einen Compiler-Fehler. Obwohl der Compiler jetzt weiß, wie man
ein Counter
-Objekt aus einem int
erzeugt, bleibt ihm der umgekehrte Vorgang weiterhin
ein Rätsel.
Für diese und ähnliche Probleme erlaubt Ihnen C++, Umwandlungsoperatoren für Ihre Klassen zu definieren. Damit läßt sich in einer Klasse festlegen, wie implizite Konvertierungen in vordefinierte Typen auszuführen sind. Listing 10.18 verdeutlicht dies. Ich möchte Sie aber schon vorab darauf hinweisen, daß Umwandlungsoperatoren keinen Rückgabewert spezifizieren, obwohl sie einen konvertierten Wert zurückliefern.
Listing 10.18: Konvertieren eines Counter-Objekts in einen unsigned short
1: #include <iostream.h>
2:
3: class Counter
4: {
5: public:
6: Counter();
7: Counter(int val);
8: ~Counter(){}
9: int GetItsVal()const { return itsVal; }
10: void SetItsVal(int x) {itsVal = x; }
11: operator unsigned short();
12: private:
13: int itsVal;
14:
15: };
16:
17: Counter::Counter():
18: itsVal(0)
19: {}
20:
21: Counter::Counter(int val):
22: itsVal(val)
23: {}
24:
25: Counter::operator unsigned short ()
26: {
27: return ( int (itsVal) );
28: }
29:
30: int main()
31: {
32: Counter ctr(5);
33: int theShort = ctr;
34: cout << "theShort: " << theShort << endl;
35: return 0;
36: }
theShort: 5
Zeile 11 deklariert den Umwandlungsoperator. Beachten Sie, daß er keinen Rückgabewert
hat. Die Implementierung der Funktion steht in den Zeilen 25 bis 28. Zeile 27
gibt den Wert von itsVal
konvertiert in einen int
zurück.
Der Compiler weiß jetzt, wie int
-Variablen in Counter
-Objekte und umgekehrt umzuwandeln
sind, und man kann sie ohne weiteres einander zuweisen.
In diesem Kapitel haben Sie erfahren, wie man Elementfunktionen von Klassen überlädt. Weiterhin haben Sie gelernt, wie man Standardwerte für Elementfunktionen bereitstellt und wie man entscheidet, ob es günstiger ist, Standardwerte vorzugeben oder Funktionen zu überladen.
Mit dem Überladen von Klassenkonstruktoren lassen sich flexible Klassen erzeugen, die man auch aus anderen Objekten erstellen kann. Die Initialisierung von Objekten findet in der Initialisierungsstufe der Konstruktion statt, was effizienter ist als das Zuweisen von Werten im Rumpf des Konstruktors.
Der Compiler stellt einen Kopierkonstruktor und den Zuweisungsoperator operator=
zur Verfügung, wenn man diese nicht selbst in einer Klasse definiert. Allerdings erstellen
die vom Compiler bereitgestellten Versionen lediglich eine elementweise Kopie
der Klasse. Für Klassen, die Zeiger auf den Heap als Datenelemente enthalten, muß
man diese Methoden überschreiben, damit man selbst Speicher für das Zielobjekt reservieren
kann.
Fast alle C++-Operatoren lassen sich überladen. Es empfiehlt sich jedoch, nur solche Operatoren zu überladen, deren Verwendung auf der Hand liegt. Man kann weder die Art des Operators - unär oder binär - ändern, noch neue Operatoren erfinden.
Der Zeiger this
verweist auf das aktuelle Objekt und ist ein unsichtbarer Parameter
alle Elementfunktionen. Überladene Operatoren geben häufig den dereferenzierten
Zeiger this
zurück.
Mit Umwandlungsoperatoren kann man Klassen erzeugen, die in Ausdrücken verwendet werden können, die einen anderen Objekttyp erwarten. Sie bilden die Ausnahme zur Regel, daß alle Funktionen einen expliziten Wert zurückgeben. Genau wie Konstruktoren und Destruktoren haben Umwandlungsoperatoren keinen Rückgabetyp.
Frage:
Warum verwendet man überhaupt Standardwerte, wenn man eine
Funktion überladen kann?
Antwort:
Es ist einfacher, nur eine statt zwei Funktionen zu verwalten. Oftmals ist eine
Funktion mit Standardparametern auch verständlicher, und man muß sich
nicht mit zwei verschiedenen Funktionsrümpfen auseinandersetzen. Außerdem
passiert es schnell, daß man die eine Funktion aktualisiert und die andere
vergißt.
Frage:
Warum verwendet man angesichts dieser Probleme nicht immer Standardwerte?
Antwort:
Überladene Funktionen eröffnen Möglichkeiten, die sich mit Standardwerten
nicht realisieren lassen, beispielsweise die Variation der Parameterliste nach
dem Typ statt nur nach der Anzahl.
Frage:
Wie entscheidet man beim Schreiben eines Klassenkonstruktors, was
in der Initialisierungsliste und was im Rumpf des Konstruktors stehen
soll?
Antwort:
Als Faustregel sollte man soviel wie möglich in der Initialisierungsphase erledigen
- das heißt, alle Elementvariablen in der Initialisierungsliste initialisieren.
Bestimmte Dinge, wie Berechnungen und Ausgabeanweisungen, muß
man im Rumpf des Konstruktors unterbringen.
Frage:
Kann eine überladene Funktion einen Standardparameter haben?
Antwort:
Ja. Es gibt keinen Grund, auf die Kombination dieser leistungsfähigen Merkmale
zu verzichten. Die überladenen Funktionen (eine oder auch mehrere)
können jeweils eigene Standardwerte haben - unter Berücksichtigung der üblichen
Regeln für Standardwerte in Funktionen.
Frage:
Warum werden einige Elementfunktionen in der Klassendeklaration definiert
und andere nicht?
Antwort:
Die Implementierung einer Elementfunktion innerhalb einer Deklaration
macht die Funktion inline
. In der Regel macht man dies nur bei extrem einfachen
Funktionen. Denken Sie daran, daß Sie eine Elementfunktion auch mit
dem Schlüsselwort inline
als Inline-Funktion deklarieren können, sogar wenn
die Funktion außerhalb der Klassendeklaration deklariert wurde.
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.
=
)?
this
-Zeiger?
+
-Operator für Operanden vom Typ short
überladen?
+
-Operator zu überladen, so daß er einen Wert Ihrer Klasse dekrementiert?
SimpleCircle
mit (nur) einer Elementvariablen: itsRadius
. Sehen Sie einen Standardkonstruktor, einen Destruktor und Zugriffsmethoden für radius
vor.
itsRadius
mit dem Wert 5
.
itsRadius
zu.
SimpleCircle
-Klasse einen Präfix- und einen Postfix-Inkrementoperator, die itsRadius
inkrementieren.
SimpleCircle
so, daß itsRadius
auf dem Heap gespeichert wird, und passen Sie die bestehenden Methoden an.
SimpleCircle
hinzu.
SimpleCircle
hinzu.
SimpleCircle
-Objekte erzeugt. Verwenden Sie den Standardkonstruktor zur Erzeugung des einen Objekts und instantiieren Sie das andere mit dem Wert 9. Wenden Sie den Inkrement-Operator auf beide Objekte an und geben Sie dann die Werte beider Objekte aus. Abschließend weisen Sie dem ersten Objekt das zweite zu und geben Sie nochmals die Werte beider Objekte aus.
SQUARE SQUARE ::operator=(const SQUARE & rhs)
{
itsSide = new int;
*itsSide = rhs.GetSide();
return *this;
}
VeryShort VeryShort::operator+ (const VeryShort& rhs)
{
itsVal += rhs.GetItsVal();
return *this;
}
© Markt&Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH