vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


Woche 3

Tag 15

Vererbung - weiterführende Themen

Bis jetzt haben Sie mit Einfach- und Mehrfachvererbung gearbeitet, um eine ist-ein- Beziehung herzustellen. Heute lernen Sie,

Einbettung (Containment)

Wie Sie schon in einigen der bisher besprochenen Beispielen sehen konnten, kann man auf dem Weg über die Datenelemente einer Klasse Objekte einer anderen Klasse einbinden. C++-Programmierer sprechen in diesem Zusammenhang davon, daß die äußere Klasse die innere Klasse enthält (englisch: contain - enthalten). Demzufolge kann beispielsweise eine Employee-Klasse sowohl String-Objekte (für den Namen der Mitarbeiter) als auch Integer-Werte (für das Gehalt der Mitarbeiter) enthalten.

Listing 15.1 zeigt eine rudimentäre, aber nützliche String-Klasse. Zu dem Listing gibt es keine Ausgabe, aber den vorgestellten Code werden wir noch in nachfolgenden Listings verwenden.

Listing 15.1: Die Klasse String

1:     #include <iostream.h>
2: #include <string.h>
3:
4: class String
5: {
6: public:
7: // Konstruktoren
8: String();
9: String(const char *const);
10: String(const String &);
11: ~String();
12:
13: // ueberladene Operatoren
14: char & operator[](int offset);
15: char operator[](int offset) const;
16: String operator+(const String&);
17: void operator+=(const String&);
18: String & operator= (const String &);
19:
20: // Allgemeine Zugriffsfunktionen
21: int GetLen()const { return itsLen; }
22: const char * GetString() const { return itsString; }
23: // static int ConstructorCount;
24:
25: private:
26: String (int); // Privater Konstruktor
27: char * itsString;
28: unsigned short itsLen;
29:
30: };
31:
32: // Standardkonstruktor erzeugt String von 0 Bytes Laenge
33: String::String()
34: {
35: itsString = new char[1];
36: itsString[0] = '\0';
37: itsLen=0;
38: // cout << "\tString-Standardkonstruktor\n";
39: // ConstructorCount++;
40: }
41:
42: // Privater (Hilfs-) Konstruktor, der nur von Methoden
43: // der Klasse zum Erzeugen neuer Null-Strings der
44: // erforderlichen Groeße verwendet wird.
45: String::String(int len)
46: {
47: itsString = new char[len+1];
48: for (i = 0; i<=len; i++)
49: itsString[i] = '\0';
50: itsLen=len;
51: // cout << "\tString(int)-Konstruktor \n";
52: // ConstructorCount++;
53: }
54:
55: //Konvertiert ein Zeichen-Array in einen String
56: String::String(const char * const cString)
57: {
58: itsLen = strlen(cString);
59: itsString = new char[itsLen+1];
60: for (i = 0; i<itsLen; i++)
61: itsString[i] = cString[i];
62: itsString[itsLen]='\0';
63: // cout << "\tString(char*)-Konstruktor\n";
64: // ConstructorCount++;
65: }
66:
67: // Kopierkonstruktor
68: String::String (const String & rhs)
69: {
70: itsLen=rhs.GetLen();
71: itsString = new char[itsLen+1];
72: for (int i = 0; i<itsLen;i++)
73: itsString[i] = rhs[i];
74: itsString[itsLen] = '\0';
75: // cout << "\tString(String&)-Konstruktor\n";
76: // ConstructorCount++;
77: }
78:
79: // Destruktor, gibt zugewiesenen Speicher frei
80: String::~String ()
81: {
82: delete [] itsString;
83: itsLen = 0;
84: // cout << "\tString-Destruktor\n";
85: }
86:
87: // Zuweisungsoperator, gibt vorhandenen Speicher frei,
88: // kopiert dann String und Groeße
89: String& String::operator=(const String & rhs)
90: {
91: if (this == &rhs)
92: return *this;
93: delete [] itsString;
94: itsLen=rhs.GetLen();
95: itsString = new char[itsLen+1];
96: for (int i = 0; i<itsLen;i++)
97: itsString[i] = rhs[i];
98: itsString[itsLen] = '\0';
99: return *this;
100: // cout << "\tString-Operator=\n";
101: }
102:
103: // Nicht konstanter Offset-Operator, gibt Referenz
104: // auf Zeichen zurueck, das sich damit aendern
105: // laeßt!
106: char & String::operator[](int offset)
107: {
108: if (offset > itsLen)
109: return itsString[itsLen-1];
110: else
111: return itsString[offset];
112: }
113:
114: // Konstanter Offset-Operator fuer konstante
115: // Objekte (siehe Kopierkonstruktor!)
116: char String::operator[](int offset) const
117: {
118: if (offset > itsLen)
119: return itsString[itsLen-1];
120: else
121: return itsString[offset];
122: }
123:
124: // Erzeugt einen neuen String durch Anfuegen von
125: // rhs an den aktuellen Strings
126: String String::operator+(const String& rhs)
127: {
128: int totalLen = itsLen + rhs.GetLen();
129: String temp(totalLen);
130: int i,j;
131: for (i = 0; i<itsLen; i++)
132: temp[i] = itsString[i];
133: for (j = 0; j<rhs.GetLen(); j++, i++)
134: temp[i] = rhs[j];
135: temp[totalLen]='\0';
136: return temp;
137: }
138:
139: // Aendert aktuellen String, gibt nichts zurueck
140: void String::operator+=(const String& rhs)
141: {
142: unsigned short rhsLen = rhs.GetLen();
143: unsigned short totalLen = itsLen + rhsLen;
144: String temp(totalLen);
145: int i, j;
146: for (i = 0; i<itsLen; i++)
147: temp[i] = itsString[i];
148: for (j = 0; j<rhs.GetLen(); j++, i++)
149: temp[i] = rhs[i-itsLen];
150: temp[totalLen]='\0';
151: *this = temp;
152: }
153:
154: // int String::ConstructorCount = 0;

Dieses Programm hat keine Ausgabe.

Listing 15.1 stellt eine String-Klasse bereit, die stark der Klasse in Listing 12.12 aus Kapitel 12, »Arrays und verkettete Listen«, ähnelt. Der Hauptunterschied liegt darin, daß die Ausgabe-Anweisungen in den Konstruktoren und einigen anderen Funktionen in Listing 15.1 auskommentiert sind. Diese Funktionen kommen in späteren Beispielen noch zur Anwendung.

In Zeile 23 wird die statische Elementvariable ConstructorCount deklariert, die in Zeile 154 initialisiert wird. In jedem String-Konstruktor wird diese Variable inkrementiert. Derzeit ist dieser Code noch auskommentiert, er wird aber in einem späteren Listing benötigt

Listing 15.2 zeigt eine Employee-Klasse, die drei String-Objekte enthält.

Listing 15.2: Die Klasse Employee und das Rahmenprogramm

1:     #include "String.hpp"
2:
3: class Employee
4: {
5:
6: public:
7: Employee();
8: Employee(char *, char *, char *, long);
9: ~Employee();
10: Employee(const Employee&);
11: Employee & operator= (const Employee &);
12:
13: const String & GetFirstName() const
14: { return itsFirstName; }
15: const String & GetLastName() const { return itsLastName; }
16: const String & GetAddress() const { return itsAddress; }
17: long GetSalary() const { return itsSalary; }
18:
19: void SetFirstName(const String & fName)
20: { itsFirstName = fName; }
21: void SetLastName(const String & lName)
22: { itsLastName = lName; }
23: void SetAddress(const String & address)
24: { itsAddress = address; }
25: void SetSalary(long salary) { itsSalary = salary; }
26: private:
27: String itsFirstName;
28: String itsLastName;
29: String itsAddress;
30: long itsSalary;
31: };
32:
33: Employee::Employee():
34: itsFirstName(""),
35: itsLastName(""),
36: itsAddress(""),
37: itsSalary(0)
38: {}
39:
40: Employee::Employee(char * firstName, char * lastName,
41: char * address, long salary):
42: itsFirstName(firstName),
43: itsLastName(lastName),
44: itsAddress(address),
45: itsSalary(salary)
46: {}
47:
48: Employee::Employee(const Employee & rhs):
49: itsFirstName(rhs.GetFirstName()),
50: itsLastName(rhs.GetLastName()),
51: itsAddress(rhs.GetAddress()),
52: itsSalary(rhs.GetSalary())
53: {}
54:
55: Employee::~Employee() {}
56:
57: Employee & Employee::operator= (const Employee & rhs)
58: {
59: if (this == &rhs)
60: return *this;
61:
62: itsFirstName = rhs.GetFirstName();
63: itsLastName = rhs.GetLastName();
64: itsAddress = rhs.GetAddress();
65: itsSalary = rhs.GetSalary();
66:
67: return *this;
68: }
69:
70: int main()
71: {
72: Employee Edie("Jane","Doe","1461 Shore Parkway", 20000);
73: Edie.SetSalary(50000);
74: String LastName("Levine");
75: Edie.SetLastName(LastName);
76: Edie.SetFirstName("Edythe");
77:
78: cout << "Name: ";
79: cout << Edie.GetFirstName().GetString();
80: cout << " " << Edie.GetLastName().GetString();
81: cout << ".\nAdresse: ";
82: cout << Edie.GetAddress().GetString();
83: cout << ".\nGehalt: " ;
84: cout << Edie.GetSalary();
85: return 0;
86: }

Speichern Sie den Code aus Listing 15.1 in einer Datei namens STRING.HPP. Wenn Sie dann die String-Klasse benötigen, können Sie Listing 15.1 mittels der #include-Anweisung einbinden.

Aus praktischen Erwägungen habe ich hier Deklaration und Implementierung der Klasse in einem Listing zusammengenommen. In einem realen Programm würden Sie die Klassendeklaration in String.HPP und die Implementierung in String.CPP abspeichern. Sie würden dann String.CPP (mittels ADD- oder MAKE-Dateien) in ihr Programm aufnehmen und String.HPP mit Hilfe der #include-Direktive in String.CPP einbinden.

Name: Edythe Levine.
Adresse: 1461 Shore Parkway.
Gehalt: 50000

Listing 15.2 zeigt die Klasse Employee (Angestellte), die drei String-Objekte enthält: itsFirstName (Vorname), itsLastName (Nachname) und itsAddress (Adresse).

Zeile 72 erzeugt ein Employee-Objekt und übergibt ihm vier Werte zur Initialisierung. In Zeile 73 wird die Employee-Zugriffsfunktion SetSalary() (Gehalt festlegen) mit dem konstanten Wert 50000 aufgerufen. Beachten Sie, daß man an dieser Stelle in einem realen Programm entweder einen dynamischen Wert (zur Laufzeit berechnet) oder eine Konstante übergeben würde.

Zeile 74 erzeugt einen String und initialisiert ihn mit einer C++-String-Konstanten. Dieses String-Objekt wird dann als Argument an SetLastName()übergeben, um den Nachnamen zu ändern (Zeile 75).

In Zeile 76 wird die Employee-Funktion SetFirstName() (Vorname festlegen) mit einer String-Konstanten als Argument aufgerufen. Sicherlich ist Ihnen bereits ausgefallen, daß Employee gar nicht über eine Funktion SetFirstName() verfügt, die eine Zeichenfolge als Argument übernimmt - SetFirstName() erfordert eine konstante String-Referenz.

Der Compiler kann diese Umwandlung einer konstanten Zeichenfolge in einen String selbständig vornehmen - die Anleitung dazu findet er in Zeile 9 aus Listing 15.1.

Auf Elemente der enthaltenen Klasse zugreifen

Employee-Objekte haben keinen speziellen Zugriff auf die Elementvariablen von String. Wenn das Employee-Objekt Edie versucht, auf die Elementvariable itsLen seiner eigenen Elementvariablen itsFirstName zuzugreifen, erhält man einen Compiler- Fehler. Allerdings ist das kein großes Problem. Die Zugriffsfunktionen der String-Klasse bieten eine passende Schnittstelle, und die Employee-Klasse braucht sich um die Details der Implementierung genauso wenig zu kümmern, wie um die Art der Speicherung von Informationen in der Integer-Variablen itsSalary.

Zugriff auf enthaltene Elemente

Die String-Klasse stellt den Operator operator+ bereit. Der Designer der Employee- Klasse hat aber den Zugriff auf den Operator operator+ für Aufrufe über Employee-Objekte blockiert, weil er alle String-Zugriffsfunktionen wie zum Beispiel GetFirstName() so deklariert hat, daß diese eine konstante Referenz zurückliefern. Da operator+ keine konstante Funktion ist (und auch nicht sein kann, da der Operator das Objekt verändert, für das er aufgerufen wird), führen Versuche wie die folgende Anweisung zu einem Compiler-Fehler:

String buffer = Edie.GetFirstName() + Edie.GetLastName();

GetFirstName() gibt ein konstantes String-Objekt zurück, und operator+ kann man nicht für ein konstantes Objekt aufrufen.

Um dieses Problem zu lösen, überladen Sie GetFirstName() mit einer nicht konstanten Version:

const String & GetFirstName() const { return itsFirstName; }
String & GetFirstName() { return itsFirstName; }

Beachten Sie, daß weder der Rückgabewert noch die Elementfunktion selbst konstant sind. Die Änderung des Rückgabewertes ist nicht ausreichend, um den Funktionsnamen zu überladen, auch die const-Deklaration der Funktion selbst muß aufgehoben werden.

Nachteile der Einbettung

Es sei darauf hingewiesen, daß jedes der in Employee-Klasse enthaltenen String-Objekte seinen Preis hat, und ein Programmierer, der die Employee-Klasse verwendet, zahlt diesen Preis, wenn ein String-Konstruktor aufgerufen oder eine Kopie von Employee angelegt wird.

Entfernt man die Kommentare vor den cout-Anweisungen in Listing 15.1 (Zeilen 38, 51, 63, 75, 84 und 100), zeigt sich, wie oft diese Aufrufe stattfinden.

Zum Kompilieren dieses Listings entfernen Sie einfach die Kommentare in den Zeilen 38, 51, 63, 75, 84 und 100 in Listing 15.1.

Listing 15.3: Konstruktoraufrufe für enthaltene Klassen

1:     #include "String.hpp"
2:
3: class Employee
4: {
5:
6: public:
7: Employee();
8: Employee(char *, char *, char *, long);
9: ~Employee();
10: Employee(const Employee&);
11: Employee & operator= (const Employee &);
12:
13: const String & GetFirstName() const
14: { return itsFirstName; }
15: const String & GetLastName() const { return itsLastName; }
16: const String & GetAddress() const { return itsAddress; }
17: long GetSalary() const { return itsSalary; }
18:
19: void SetFirstName(const String & fName)
20: { itsFirstName = fName; }
21: void SetLastName(const String & lName)
22: { itsLastName = lName; }
23: void SetAddress(const String & address)
24: { itsAddress = address; }
25: void SetSalary(long salary) { itsSalary = salary; }
26: private:
27: String itsFirstName;
28: String itsLastName;
29: String itsAddress;
30: long itsSalary;
31: };
32:
33: Employee::Employee():
34: itsFirstName(""),
35: itsLastName(""),
36: itsAddress(""),
37: itsSalary(0)
38: {}
39:
40: Employee::Employee(char * firstName, char * lastName,
41: char * address, long salary):
42: itsFirstName(firstName),
43: itsLastName(lastName),
44: itsAddress(address),
45: itsSalary(salary)
46: {}
47:
48: Employee::Employee(const Employee & rhs):
49: itsFirstName(rhs.GetFirstName()),
50: itsLastName(rhs.GetLastName()),
51: itsAddress(rhs.GetAddress()),
52: itsSalary(rhs.GetSalary())
53: {}
54:
55: Employee::~Employee() {}
56:
57: Employee & Employee::operator= (const Employee & rhs)
58: {
59: if (this == &rhs)
60: return *this;
61:
62: itsFirstName = rhs.GetFirstName();
63: itsLastName = rhs.GetLastName();
64: itsAddress = rhs.GetAddress();
65: itsSalary = rhs.GetSalary();
66:
67: return *this;
68: }
69:
70: int main()
71: {
72: cout << "Edie erzeugen...\n";
73: Employee Edie("Jane","Doe","1461 Shore Parkway", 20000);
74: Edie.SetSalary(20000);
75: cout << "SetFirstName mit char * aufrufen...\n";
76: Edie.SetFirstName("Edythe");
77: cout << "Temporaeren String LastName erzeugen...\n";
78: String LastName("Levine");
79: Edie.SetLastName(LastName);
80:
81: cout << "Name: ";
82: cout << Edie.GetFirstName().GetString();
83: cout << " " << Edie.GetLastName().GetString();
84: cout << "\nAdresse: ";
85: cout << Edie.GetAddress().GetString();
86: cout << "\nGehalt: " ;
87: cout << Edie.GetSalary();
88: cout << endl;
89: return 0;
90: }

1:    Edie erzeugen...
2: String(char*)-Konstruktor
3: String(char*)-Konstruktor
4: String(char*)-Konstruktor
5: SetFirstName mit char * aufrufen...
6: String(char*)-Konstruktor
7: String-Destruktor
8: Temporaeren String LastName erzeugen...
9: String(char*)-Konstruktor
10: Name: Edythe Levine
11: Adresse: 1461 Shore Parkway
12: Gehalt: 20000
13: String-Destruktor
14: String-Destruktor
15: String-Destruktor
16: String-Destruktor

Listing 15.3 verwendet die gleichen Klassendeklarationen wie Listing 15.1 und 15.2. Die Kommentarzeichen vor den cout-Anweisungen wurden jedoch entfernt. Die Ausgabe von Listing 15.3 wurde numeriert, um die Bezugnahme bei der Analyse zu erleichtern.

In Zeile 72 von Listing 15.3 wird die Anweisung Edie erzeugen... ausgegeben, was sich in der Ausgabezeile 1 widerspiegelt. Zeile 73 erzeugt ein Employee-Objekt Edie mit vier Parametern. Die Ausgabe zeigt, daß der Konstruktor für String wie erwartet drei Mal aufgerufen wird.

Zeile 75 gibt eine rein informative Meldung aus, und in Zeile 76 steht die Anweisung Edie.SetFirstName("Edythe"). Diese Anweisung erzeugt aus dem Zeichen-String "Edythe" einen temporären String, was man an den Ausgabezeilen 6 und 7 ablesen kann. Beachten Sie, daß der temporäre String sofort nach seiner Verwendung in der Zuweisungsanweisung wieder zerstört wird.

Zeile 78 erzeugt ein String-Objekt. Der Programmierer macht damit explizit das, was der Compiler implizit schon in der Anweisung davor gemacht hat. Diesmal sehen Sie den Konstruktoraufruf (Ausgabezeile 9), jedoch keinen Destruktor. Das Objekt wird erst zerstört, wenn es am Ende der Funktion seinen Gültigkeitsbereich verliert.

In den Zeilen 89 und 90 werden die Strings des Employee-Objekt aufgelöst, da dieses seinen Gültigkeitsbereich verliert. Der String LastName, der in Zeile 78 erzeugt wurde, wird ebenfalls zerstört, wenn er seinen Gültigkeitsbereich verliert.

Als Wert kopieren

Listing 15.3 zeigte, daß die Erzeugung eines Employee-Objekts den Aufruf von fünf String-Konstruktoren zur Folge hat. 15.4 enthält nochmals eine Neufassung des Programms, in der die Auskommentierung des Codes für die statische Elementvariable ConstructorCount der String-Klasse aufgehoben wurde.

Listing 15.1 kann man entnehmen, daß ConstructorCount jedes Mal inkrementiert wird, wenn ein String-Konstruktor aufgerufen wird. Das Rahmenprogramm in Listing 15.4 ruft zwei Ausgabefunktionen auf, denen es das Employee-Objekt - zuerst als Referenz und dann als Wert - übergibt. ConstructorCount merkt sich, wie viele String- Objekte bei der Übergabe der Employee-Objekte erzeugt werden.

Um dieses Listing zu kompilieren, sollten Sie die Zeilen in Listing 15.1, von denen Sie in Listing 15.3 die Kommentarzeichen entfernt haben, so belassen und zusätzlich die Auskommentierung der Zeilen 23, 39, 52, 64, 76 und 154 aus Listing 15.1 aufheben.

Listing 15.4: Übergabe als Wert

1:     #include "String.hpp"
2:
3: class Employee
4: {
5:
6: public:
7: Employee();
8: Employee(char *, char *, char *, long);
9: ~Employee();
10: Employee(const Employee&);
11: Employee & operator= (const Employee &);
12:
13: const String & GetFirstName() const
14: { return itsFirstName; }
15: const String & GetLastName() const { return itsLastName; }
16: const String & GetAddress() const { return itsAddress; }
17: long GetSalary() const { return itsSalary; }
18:
19: void SetFirstName(const String & fName)
20: { itsFirstName = fName; }
21: void SetLastName(const String & lName)
22: { itsLastName = lName; }
23: void SetAddress(const String & address)
24: { itsAddress = address; }
25: void SetSalary(long salary) { itsSalary = salary; }
26: private:
27: String itsFirstName;
28: String itsLastName;
29: String itsAddress;
30: long itsSalary;
31: };
32:
33: Employee::Employee():
34: itsFirstName(""),
35: itsLastName(""),
36: itsAddress(""),
37: itsSalary(0)
38: {}
39:
40: Employee::Employee(char * firstName, char * lastName,
41: char * address, long salary):
42: itsFirstName(firstName),
43: itsLastName(lastName),
44: itsAddress(address),
45: itsSalary(salary)
46: {}
47:
48: Employee::Employee(const Employee & rhs):
49: itsFirstName(rhs.GetFirstName()),
50: itsLastName(rhs.GetLastName()),
51: itsAddress(rhs.GetAddress()),
52: itsSalary(rhs.GetSalary())
53: {}
54:
55: Employee::~Employee() {}
56:
57: Employee & Employee::operator= (const Employee & rhs)
58: {
59: if (this == &rhs)
60: return *this;
61:
62: itsFirstName = rhs.GetFirstName();
63: itsLastName = rhs.GetLastName();
64: itsAddress = rhs.GetAddress();
65: itsSalary = rhs.GetSalary();
66:
67: return *this;
68: }
69:
70: void PrintFunc(Employee);
71: void rPrintFunc(const Employee&);
72:
73: int main()
74: {
75: Employee Edie("Jane","Doe","1461 Shore Parkway", 20000);
76: Edie.SetSalary(20000);
77: Edie.SetFirstName("Edythe");
78: String LastName("Levine");
79: Edie.SetLastName(LastName);
80:
81: cout << "Konstruktorzaehlung: " ;
82: cout << String::ConstructorCount << endl;
83: rPrintFunc(Edie);
84: cout << "Konstruktorzaehlung: ";
85: cout << String::ConstructorCount << endl;
86: PrintFunc(Edie);
87: cout << "Konstruktorzaehlung: ";
88: cout << String::ConstructorCount << endl;
89: return 0;
90: }
91: void PrintFunc (Employee Edie)
92: {
93:
94: cout << "Name: ";
95: cout << Edie.GetFirstName().GetString();
96: cout << " " << Edie.GetLastName().GetString();
97: cout << ".\nAdresse: ";
98: cout << Edie.GetAddress().GetString();
99: cout << ".\nGehalt: " ;
100: cout << Edie.GetSalary();
101: cout << endl;
102:
103: }
104:
105: void rPrintFunc (const Employee& Edie)
106: {
107: cout << "Name: ";
108: cout << Edie.GetFirstName().GetString();
109: cout << " " << Edie.GetLastName().GetString();
110: cout << "\nAdresse: ";
111: cout << Edie.GetAddress().GetString();
112: cout << "\nGehalt: " ;
113: cout << Edie.GetSalary();
114: cout << endl;
115: }

String(char*) constructor
String(char*)-Konstruktor
String(char*)-Konstruktor
String(char*)-Konstruktor
String-Destruktor
String(char*)-Konstruktor
Konstruktorzaehlung: 5
Name: Edythe Levine
Adresse: 1461 Shore Parkway
Gehalt: 20000
Konstruktorzaehlung: 5
String(String&)-Konstruktor
String(String&)-Konstruktor
String(String&)-Konstruktor
Name: Edythe Levine.
Adresse: 1461 Shore Parkway.
Gehalt: 20000
String-Destruktor
String-Destruktor
String-Destruktor
Konstruktorzaehlung: 8
String-Destruktor
String-Destruktor
String-Destruktor
String-Destruktor

Die Ausgabe zeigt, daß im Zuge der Erzeugung eines Employee-Objekts fünf String-Objekte erzeugt werden. Wenn ein Employee-Objekt als Referenz an rPrintFunc()übergeben wird, werden keine weiteren Employee-Objekte erzeugt und demzufolge auch keine weiteren String-Objekte. (Diese werden auch als Referenz übergeben.)

Wenn das Employee-Objekt als Wert an PrintFunc() übergeben wird (Zeile 86), wird eine Kopie von Employee erstellt und drei weitere String-Objekte werden erzeugt (durch Aufruf des Kopierkonstruktors).

Vererbung, Einbettung und Delegierung

Gelegentlich kommt es vor, daß man für die Implementierung einer Klasse gerne auf die Attribute einer anderen Klasse zurückgreifen würde. Angenommen Sie müssen eine Klasse PartsCatalog erzeugen. Ihrer Spezifikation zufolge ist PartsCatalog eine Sammlung von Teilen (Parts), die alle über eine eindeutige Teilenummer verfügen. Die Klasse PartsCatalog erlaubt den Zugriff auf einzelne Teile über die Teilenummer und verhindert, daß Teile doppelte eingetragen werden.

Im Listing aus der Wochenrückschau zur zweiten Woche finden Sie die Klasse PartsList . Diese Klasse ist erprobt und von Ihnen verstanden, und Sie würden für Ihre eigene Klasse PartsCatalog gerne auf dieser Basis aufbauen, statt bei Entwurf und Implementierung Ihrer Klasse ganz von vorne anfangen zu müssen.

Sie könnten eine neue PartsCatalog-Klasse erzeugen, die ein PartsList-Objekt enthält. PartsCatalog könnte dann die Verwaltung der verketteten Teileliste an das in ihr eingebettete PartsList-Objekt delegieren.

Eine andere Alternative wäre, die Klasse PartsCatalog von PartsList abzuleiten und damit deren Eigenschaften zu erben. Denken Sie jedoch daran, daß öffentliche Vererbung eine ist-eine-Beziehung herstellt. Deshalb sollten Sie sich fragen, ob PartsCatalog wirklich eine Art von PartsList ist.

Eine Möglichkeit zu entscheiden, ob PartsCatalog eine Art PartsList ist, besteht darin anzunehmen, daß PartsList die Basisklasse und PartsCatalog die abgeleitete Klasse sei, und sich dann die folgenden Fragen zu stellen:

Gibt es irgend etwas in der Basisklasse, was nicht in die abgeleitete Klasse gehört? Weist zum Beispiel die Basisklasse PartsList Funktionen auf, die für die PartsCatalog -Klasse nicht geeignet sind? Wenn ja, wollen Sie sicherlich keine öffentliche Vererbung.

Benötigt die Klasse, die Sie erzeugen wollen, mehr als ein Objekt der Basisklasse? Benötigt zum Beispiel PartsCatalog zwei PartsList-Listen in jedem Objekt? Wenn ja, werden Sie mit großer Sicherheit auf Einbettung zurückgreifen.

Müssen Sie von der Basisklasse erben, so daß Sie virtuelle Funktionen nutzen oder auf geschützte Elemente zugreifen können? Wenn ja, sollten Sie mit öffentlicher oder privater Vererbung arbeiten.

Je nachdem zu welcher Antwort Sie kommen, müssen Sie zwischen öffentlicher Vererbung (die ist-ein-Beziehung) und entweder privater Vererbung (wird weiter hinter erklärt) oder Einbettung wählen.

Delegierung

Warum sollte man PartsCatalog nicht von PartsList ableiten? PartsCatalog ist keine PartsList, da es sich bei PartsList-Objekten um geordnete Sammlungen handelt, in denen jedes Element der Sammlung mehrfach vorliegen kann. Ein PartsCatalog-Objekt enthält nur eindeutige Einträge, die nicht geordnet sind. Das fünfte Element in PartsCatalog hat nicht automatisch die Teilenummer 5.

Sicher wäre es auch möglich gewesen, öffentlich von PartsList zu erben und dann Insert() und den Offset-Operator ([]) zu überschreiben, aber dann hätten Sie das Wesen der Klasse PartsList geändert. Statt dessen wollen Sie eine PartsCatalog- Klasse schaffen, die keinen Offset-Operator hat, doppelte Einträge verbietet und den operator+ definiert, um zwei Sammlungen zu kombinieren.

Die erste Möglichkeit, dies zu erreichen, setzt auf Einbettung. PartsCatalog delegiert die Listenverwaltung an das enthaltene PartsList-Objekt. Listing 15.5 setzt diesen Ansatz um.

Listing 15.5: Delegierung an ein eingebettetes PartsList-Objekt

1:      #include <iostream.h>
2:
3: // **************** Teile ************
4:
5: // Abstrakte Basisklasse fuer die Teile
6: class Part
7: {
8: public:
9: Part():itsPartNumber(1) {}
10: Part(int PartNumber):
11: itsPartNumber(PartNumber){}
12: virtual ~Part(){}
13: int GetPartNumber() const
14: { return itsPartNumber; }
15: virtual void Display() const =0;
16: private:
17: int itsPartNumber;
18: };
19:
20: // Implementierung einer abstrakten Funktion, damit
21: // abgeleitete Klassen die Funktion ueberschreiben
22: void Part::Display() const
23: {
24: cout << "\nTeilenummer: " << itsPartNumber << endl;
25: }
26:
27: // **************** Autoteile ************
28:
29: class CarPart : public Part
30: {
31: public:
32: CarPart():itsModelYear(94){}
33: CarPart(int year, int partNumber);
34: virtual void Display() const
35: {
36: Part::Display();
37: cout << "Baujahr: ";
38: cout << itsModelYear << endl;
39: }
40: private:
41: int itsModelYear;
42: };
43:
44: CarPart::CarPart(int year, int partNumber):
45: itsModelYear(year),
46: Part(partNumber)
47: {}
48:
49:
50: // **************** Flugzeugteile ************
51:
52: class AirPlanePart : public Part
53: {
54: public:
55: AirPlanePart():itsEngineNumber(1){};
56: AirPlanePart
57: (int EngineNumber, int PartNumber);
58: virtual void Display() const
59: {
60: Part::Display();
61: cout << "Motor-Nr.: ";
62: cout << itsEngineNumber << endl;
63: }
64: private:
65: int itsEngineNumber;
66: };
67:
68: AirPlanePart::AirPlanePart
69: (int EngineNumber, int PartNumber):
70: itsEngineNumber(EngineNumber),
71: Part(PartNumber)
72: {}
73:
74: // **************** Teile-Knoten ************
75: class PartNode
76: {
77: public:
78: PartNode (Part*);
79: ~PartNode();
80: void SetNext(PartNode * node)
81: { itsNext = node; }
82: PartNode * GetNext() const;
83: Part * GetPart() const;
84: private:
85: Part *itsPart;
86: PartNode * itsNext;
87: };
88: // PartNode Implementierungen...
89:
90: PartNode::PartNode(Part* pPart):
91: itsPart(pPart),
92: itsNext(0)
93: {}
94:
95: PartNode::~PartNode()
96: {
97: delete itsPart;
98: itsPart = 0;
99: delete itsNext;
100: itsNext = 0;
101: }
102:
103: // Liefert NULL zurueck, falls kein naechster PartNode vorhanden
104: PartNode * PartNode::GetNext() const
105: {
106: return itsNext;
107: }
108:
109: Part * PartNode::GetPart() const
110: {
111: if (itsPart)
112: return itsPart;
113: else
114: return NULL; //Fehler
115: }
116:
117:
118:
119: // **************** Teileliste ************
120: class PartsList
121: {
122: public:
123: PartsList();
124: ~PartsList();
125: // benoetigt Kopierkonstruktor und Zuweisungsoperator!
126: void Iterate(void (Part::*f)()const) const;
127: Part* Find(int & position, int PartNumber) const;
128: Part* GetFirst() const;
129: void Insert(Part *);
130: Part* operator[](int) const;
131: int GetCount() const { return itsCount; }
132: static PartsList& GetGlobalPartsList()
133: {
134: return GlobalPartsList;
135: }
136: private:
137: PartNode * pHead;
138: int itsCount;
139: static PartsList GlobalPartsList;
140: };
141:
142: PartsList PartsList::GlobalPartsList;
143:
144:
145: PartsList::PartsList():
146: pHead(0),
147: itsCount(0)
148: {}
149:
150: PartsList::~PartsList()
151: {
152: delete pHead;
153: }
154:
155: Part* PartsList::GetFirst() const
156: {
157: if (pHead)
158: return pHead->GetPart();
159: else
160: return NULL; // Fehler auffangen
161: }
162:
163: Part * PartsList::operator[](int offSet) const
164: {
165: PartNode* pNode = pHead;
166:
167: if (!pHead)
168: return NULL; // Fehler auffangen
169:
170: if (offSet > itsCount)
171: return NULL; // Fehler
172:
173: for (int i=0;i<offSet; i++)
174: pNode = pNode->GetNext();
175:
176: return pNode->GetPart();
177: }
178:
179: Part* PartsList::Find(
180: int & position,
181: int PartNumber) const
182: {
183: PartNode * pNode = 0;
184: for (pNode = pHead, position = 0;
185: pNode!=NULL;
186: pNode = pNode->GetNext(), position++)
187: {
188: if (pNode->GetPart()->GetPartNumber() == PartNumber)
189: break;
190: }
191: if (pNode == NULL)
192: return NULL;
193: else
194: return pNode->GetPart();
195: }
196:
197: void PartsList::Iterate(void (Part::*func)()const) const
198: {
199: if (!pHead)
200: return;
201: PartNode* pNode = pHead;
202: do
203: (pNode->GetPart()->*func)();
204: while (pNode = pNode->GetNext());
205: }
206:
207: void PartsList::Insert(Part* pPart)
208: {
209: PartNode * pNode = new PartNode(pPart);
210: PartNode * pCurrent = pHead;
211: PartNode * pNext = 0;
212:
213: int New = pPart->GetPartNumber();
214: int Next = 0;
215: itsCount++;
216:
217: if (!pHead)
218: {
219: pHead = pNode;
220: return;
221: }
222:
223: // Ist dieser kleiner als head
224: // dann ist dies der neue head
225: if (pHead->GetPart()->GetPartNumber() > New)
226: {
227: pNode->SetNext(pHead);
228: pHead = pNode;
229: return;
230: }
231:
232: for (;;)
233: {
234: // gibt es keinen naechsten Knoten, den neuen anhaengen
235: if (!pCurrent->GetNext())
236: {
237: pCurrent->SetNext(pNode);
238: return;
239: }
240:
241: // gehoert der Knoten zwischen diesen und den naechsten,
242: // dann hier einfuegen, ansonsten zu naechstem Knoten gehen
243: pNext = pCurrent->GetNext();
244: Next = pNext->GetPart()->GetPartNumber();
245: if (Next > New)
246: {
247: pCurrent->SetNext(pNode);
248: pNode->SetNext(pNext);
249: return;
250: }
251: pCurrent = pNext;
252: }
253: }
254:
255:
256:
257: class PartsCatalog
258: {
259: public:
260: void Insert(Part *);
261: int Exists(int PartNumber);
262: Part * Get(int PartNumber);
263: operator+(const PartsCatalog &);
264: void ShowAll() { thePartsList.Iterate(Part::Display); }
265: private:
266: PartsList thePartsList;
267: };
268:
269: void PartsCatalog::Insert(Part * newPart)
270: {
271: int partNumber = newPart->GetPartNumber();
272: int offset;
273:
274: if (!thePartsList.Find(offset, partNumber))
275:
276: thePartsList.Insert(newPart);
277: else
278: {
279: cout << partNumber << " war der ";
280: switch (offset)
281: {
282: case 0: cout << "erste "; break;
283: case 1: cout << "zweite "; break;
284: case 2: cout << "dritte "; break;
285: default: cout << offset+1 << "-te ";
286: }
287: cout << "Eintrag. Abgelehnt!\n";
288: }
289: }
290:
291: int PartsCatalog::Exists(int PartNumber)
292: {
293: int offset;
294: thePartsList.Find(offset,PartNumber);
295: return offset;
296: }
297:
298: Part * PartsCatalog::Get(int PartNumber)
299: {
300: int offset;
301: Part * thePart = thePartsList.Find(offset, PartNumber);
302: return thePart;
303: }
304:
305:
306: int main()
307: {
308: PartsCatalog pc;
309: Part * pPart = 0;
310: int PartNumber;
311: int value;
312: int choice;
313:
314: while (1)
315: {
316: cout << "(0)Beenden (1)Auto (2)Flugzeug: ";
317: cin >> choice;
318:
319: if (!choice)
320: break;
321:
322: cout << "Neue Teilenummer?: ";
323: cin >> PartNumber;
324:
325: if (choice == 1)
326: {
327: cout << "Baujahr?: ";
328: cin >> value;
329: pPart = new CarPart(value,PartNumber);
330: }
331: else
332: {
333: cout << "Motor-Nummer?: ";
334: cin >> value;
335: pPart = new AirPlanePart(value,PartNumber);
336: }
337: pc.Insert(pPart);
338: }
339: pc.ShowAll();
340: return 0;
341: }

(0)Beenden (1)Auto (2)Flugzeug:  1
Neue Teilenummer?: 1234
Baujahr?: 94
(0)Beenden (1)Auto (2)Flugzeug: 1
Neue Teilenummer?: 4434
Baujahr?: 93
(0)Beenden (1)Auto (2)Flugzeug: 1
Neue Teilenummer?: 1234
Baujahr?: 94
1234 war der erste Eintrag. Abgelehnt!
(0)Beenden (1)Auto (2)Flugzeug: 1
Neue Teilenummer?: 2345
Baujahr?: 93
(0)Beenden (1)Auto (2)Flugzeug: 0

Neue Teilenummer: 1234
Baujahr: 94

Neue Teilenummer: 2345
Baujahr: 93

Neue Teilenummer: 4434
Baujahr: 93

Einige Compiler haben Probleme mit der Zeile 264, obwohl sie den Regeln von C++ entspricht. Macht auch Ihr Compiler Schwierigkeiten, ändern Sie die Zeile in

264: void ShowAll() { thePartsList.Iterate(&Part::Display); }

(Beachten Sie das kaufmännische Und vor Part::Display.) Läßt sich dieser Code ausführen, rufen Sie umgehend Ihren Compiler-Händler an und beschweren Sie sich.

Listing 15.5 reproduziert die Klassen Part, PartNode und PartsList aus der Rückschau zur zweiten Woche.

Die Zeilen 257 bis 267 deklarieren eine neue Klasse namens PartsCatalog. PartsCatalog enthält PartsList als Datenelement, an das es die Listenverwaltung delegiert. Man kann auch sagen, daß PartsCatalog mit Hilfe von PartsList implementiert wird.

Beachten Sie, daß die Klienten von PartsCatalog keinen direkten Zugriff auf PartsList haben. Der Zugriff erfolgt immer über PartsCatalog, die das Verhalten von PartsList auf diesem Weg dramatisch ändert. So erlaubt zum Beispiel die Methode PartsCatalog::Insert() keine doppelten Einträge in PartsList.

Die Implementierung von PartsCatalog::Insert() beginnt in Zeile 269. Für das Part- Objekt, das als Parameter übergeben wird, wird der Wert der Elementvariablen itsPartNumber abgefragt. Dieser Wert wird an die Find()-Methode von PartsList weitergereicht, und wenn keine Übereinstimmung gefunden wird, wird die Nummer eingefügt. Im anderen Fall wird eine Fehlermeldung ausgegeben.

Beachten Sie, daß PartsCatalog zum eigentlichen Einfügen die Methode Insert() seiner Elementvariable p1 (also seines PartsList-Objekts) aufruft. Für das Einfügen, die Wartung der verketteten Liste sowie Such- und Ausleseoperationen ist das in PartsCatalog eingebettete PartsList-Element verantwortlich. Es besteht kein Grund für PartsCatalog, diesen Code zu reproduzieren. Die Klasse kann vielmehr die Vorteile einer gut definierten Schnittstelle ausschöpfen.

Das ist der Kern der Wiederverwertbarkeit in C++. PartsCatalog kann den PartsList- Code wiederverwerten und der Entwickler von PartsCatalog muß sich nicht um die Implementierungsdetails von PartsList kümmern. Die Schnittstelle von PartsList (das ist die Klassendeklaration) stellt alle Informationen, die vom Entwickler der PartsCatalog -Klasse benötigt werden, bereit.

Private Vererbung

Würde die Klasse PartsCatalog Zugriff auf die geschützten Elemente von PartsList (zur Zeit gibt es keine) benötigen oder müßte sie eine der PartsList-Methoden überschreiben, dann müßte PartsCatalog von PartsList abgeleitet werden.

Da PartsCatalog kein PartsList-Objekt ist und Sie nicht das ganze Spektrum der Funktionalität von PartsList den Klienten von PartsCatalog zur Verfügung stellen wollen, würden Sie PartsList als private Basisklasse von PartsCatalog deklarieren (private Vererbung).

Zuerst sollten Sie sich darüber im klaren sein, daß bei der privaten Vererbung alle Elementvariablen und -funktionen der Basisklasse so behandelt werden, als seien sie privat deklariert - unabhängig von den eigentlichen in der Basisklasse festgelegten Zugriffsrechten. Damit kann keine Funktion, die nicht Elementfunktion von PartsCatalog ist, auf die von PartsList geerbten Funktionen zugreifen. Wichtig zu merken ist: Bei der privaten Vererbung wird nicht die Schnittstelle, sondern nur die Implementierung vererbt.

Für die Klienten der PartsCatalog-Klasse ist die PartsList-Klasse unsichtbar. Keine ihrer Schnittstellen ist für die Klienten verfügbar, sie können keine ihrer Methoden aufrufen. Sie können jedoch die Methoden von PartsCatalog aufrufen und die wiederum können auf die gesamte Klasse PartsList zugreifen, da PartsCatalog von PartsList abgeleitet ist. Wichtig dabei ist, daß PartsCatalog keine PartsList ist, wie das bei öffentlicher Vererbung der Fall wäre. PartsCatalog ist mit Hilfe von PartsList implementiert - wie bei der Einbettung. Private Vererbung stellt nur eine bequeme Alternative dar.

Listing 15.6 zeigt ein Beispiel für private Vererbung. Die Klasse PartsCatalog wird als private Ableitung von PartsList deklariert.

Listing 15.6: Private Vererbung

1:      //listing 15.6 Private Vererbung
2: #include <iostream.h>
3:
4: // **************** Teile ************
5:
6: // Abstrakte Basisklasse fuer die Teile
7: class Part
8: {
9: public:
10: Part():itsPartNumber(1) {}
11: Part(int PartNumber):
12: itsPartNumber(PartNumber){}
13: virtual ~Part(){}
14: int GetPartNumber() const
15: { return itsPartNumber; }
16: virtual void Display() const =0;
17: private:
18: int itsPartNumber;
19: };
20:
21: // Implementierung einer abstrakten Funktion, damit
22: // abgeleitete Klassen die Funktion ueberschreiben
23: void Part::Display() const
24: {
25: cout << "\nTeilenummer: " << itsPartNumber << endl;
26: }
27:
28: // **************** Autoteile ************
29:
30: class CarPart : public Part
31: {
32: public:
33: CarPart():itsModelYear(94){}
34: CarPart(int year, int partNumber);
35: virtual void Display() const
36: {
37: Part::Display();
38: cout << "Baujahr: ";
39: cout << itsModelYear << endl;
40: }
41: private:
42: int itsModelYear;
43: };
44:
45: CarPart::CarPart(int year, int partNumber):
46: itsModelYear(year),
47: Part(partNumber)
48: {}
49:
50:
51: // **************** Flugzeugteile ************
52:
53: class AirPlanePart : public Part
54: {
55: public:
56: AirPlanePart():itsEngineNumber(1){};
57: AirPlanePart
58: (int EngineNumber, int PartNumber);
59: virtual void Display() const
60: {
61: Part::Display();
62: cout << "Motor-Nr.: ";
63: cout << itsEngineNumber << endl;
64: }
65: private:
66: int itsEngineNumber;
67: };
68:
69: AirPlanePart::AirPlanePart
70: (int EngineNumber, int PartNumber):
71: itsEngineNumber(EngineNumber),
72: Part(PartNumber)
73: {}
74:
75: // **************** Teile-Knoten ************
76: class PartNode
77: {
78: public:
79: PartNode (Part*);
80: ~PartNode();
81: void SetNext(PartNode * node)
82: { itsNext = node; }
83: PartNode * GetNext() const;
84: Part * GetPart() const;
85: private:
86: Part *itsPart;
87: PartNode * itsNext;
88: };
89: // PartNode Implementierungen...
90:
91: PartNode::PartNode(Part* pPart):
92: itsPart(pPart),
93: itsNext(0)
94: {}
95:
96: PartNode::~PartNode()
97: {
98: delete itsPart;
99: itsPart = 0;
100: delete itsNext;
101: itsNext = 0;
102: }
103:
104: // Liefert NULL zurueck, wenn kein naechster PartNode vorhanden
105: PartNode * PartNode::GetNext() const
106: {
107: return itsNext;
108: }
109:
110: Part * PartNode::GetPart() const
111: {
112: if (itsPart)
113: return itsPart;
114: else
115: return NULL; //Fehler
116: }
117:
118:
119:
120: // **************** Teile-Liste ************
121: class PartsList
122: {
123: public:
124: PartsList();
125: ~PartsList();
126: // benoetigt Kopierkonstruktor und Zuweisungsoperator!
127: void Iterate(void (Part::*f)()const) const;
128: Part* Find(int & position, int PartNumber) const;
129: Part* GetFirst() const;
130: void Insert(Part *);
131: Part* operator[](int) const;
132: int GetCount() const { return itsCount; }
133: static PartsList& GetGlobalPartsList()
134: {
135: return GlobalPartsList;
136: }
137: private:
138: PartNode * pHead;
139: int itsCount;
140: static PartsList GlobalPartsList;
141: };
142:
143: PartsList PartsList::GlobalPartsList;
144:
145:
146: PartsList::PartsList():
147: pHead(0),
148: itsCount(0)
149: {}
150:
151: PartsList::~PartsList()
152: {
153: delete pHead;
154: }
155:
156: Part* PartsList::GetFirst() const
157: {
158: if (pHead)
159: return pHead->GetPart();
160: else
161: return NULL; // Fehler auffangen
162: }
163:
164: Part * PartsList::operator[](int offSet) const
165: {
166: PartNode* pNode = pHead;
167:
168: if (!pHead)
169: return NULL; // Fehler auffangen
170:
171: if (offSet > itsCount)
172: return NULL; // Fehler
173:
174: for (int i=0;i<offSet; i++)
175: pNode = pNode->GetNext();
176:
177: return pNode->GetPart();
178: }
179:
180: Part* PartsList::Find(
181: int & position,
182: int PartNumber) const
183: {
184: PartNode * pNode = 0;
185: for (pNode = pHead, position = 0;
186: pNode!=NULL;
187: pNode = pNode->GetNext(), position++)
188: {
189: if (pNode->GetPart()->GetPartNumber() == PartNumber)
190: break;
191: }
192: if (pNode == NULL)
193: return NULL;
194: else
195: return pNode->GetPart();
196: }
197:
198: void PartsList::Iterate(void (Part::*func)()const) const
199: {
200: if (!pHead)
201: return;
202: PartNode* pNode = pHead;
203: do
204: (pNode->GetPart()->*func)();
205: while (pNode = pNode->GetNext());
206: }
207:
208: void PartsList::Insert(Part* pPart)
209: {
210: PartNode * pNode = new PartNode(pPart);
211: PartNode * pCurrent = pHead;
212: PartNode * pNext = 0;
213:
214: int New = pPart->GetPartNumber();
215: int Next = 0;
216: itsCount++;
217:
218: if (!pHead)
219: {
220: pHead = pNode;
221: return;
222: }
223:
224: // Ist dieser kleiner als head
225: // dann ist dies der neue head
226: if (pHead->GetPart()->GetPartNumber() > New)
227: {
228: pNode->SetNext(pHead);
229: pHead = pNode;
230: return;
231: }
232:
233: for (;;)
234: {
235: // gibt es keinen naechsten, den neuen anhaengen
236: if (!pCurrent->GetNext())
237: {
238: pCurrent->SetNext(pNode);
239: return;
240: }
241:
242: // gehoert der Knoten zwischen diesen und den naechsten,
243: // dann hier einfuegen, ansonsten zu naechstem Knoten gehen
244: pNext = pCurrent->GetNext();
245: Next = pNext->GetPart()->GetPartNumber();
246: if (Next > New)
247: {
248: pCurrent->SetNext(pNode);
249: pNode->SetNext(pNext);
250: return;
251: }
252: pCurrent = pNext;
253: }
254: }
255:
256:
257:
258: class PartsCatalog : private PartsList
259: {
260: public:
261: void Insert(Part *);
262: int Exists(int PartNumber);
263: Part * Get(int PartNumber);
264: operator+(const PartsCatalog &);
265: void ShowAll() { Iterate(Part::Display); }
266: private:
267: };
268:
269: void PartsCatalog::Insert(Part * newPart)
270: {
271: int partNumber = newPart->GetPartNumber();
272: int offset;
273:
274: if (!Find(offset, partNumber))
275: PartsList::Insert(newPart);
276: else
277: {
278: cout << partNumber << " war der ";
279: switch (offset)
280: {
281: case 0: cout << "erste "; break;
282: case 1: cout << "zweite "; break;
283: case 2: cout << "dritte "; break;
284: default: cout << offset+1 << "th ";
285: }
286: cout << "Eintrag. Abgelehnt!\n";
287: }
288: }
289:
290: int PartsCatalog::Exists(int PartNumber)
291: {
292: int offset;
293: Find(offset,PartNumber);
294: return offset;
295: }
296:
297: Part * PartsCatalog::Get(int PartNumber)
298: {
299: int offset;
300: return (Find(offset, PartNumber));
301:
302: }
303:
304: int main()
305: {
306: PartsCatalog pc;
307: Part * pPart = 0;
308: int PartNumber;
309: int value;
310: int choice;
311:
312: while (1)
313: {
314: cout << "(0)Beenden (1)Auto (2)Flugzeug: ";
315: cin >> choice;
316:
317: if (!choice)
318: break;
319:
320: cout << "Neue Teilenummer?: ";
321: cin >> PartNumber;
322:
323: if (choice == 1)
324: {
325: cout << "Baujahr?: ";
326: cin >> value;
327: pPart = new CarPart(value,PartNumber);
328: }
329: else
330: {
331: cout << "Motor-Nummer?: ";
332: cin >> value;
333: pPart = new AirPlanePart(value,PartNumber);
334: }
335: pc.Insert(pPart);
336: }
337: pc.ShowAll();
338: return 0;
339: }

(0)Beenden (1)Auto (2)Flugzeug:  1
Neue Teilenummer?: 1234
Baujahr?: 94
(0)Beenden (1)Auto (2)Flugzeug: 1
Neue Teilenummer?: 4434
Baujahr?: 93
(0)Beenden (1)Auto (2)Flugzeug: 1
Neue Teilenummer?: 1234
Baujahr?: 94
1234 war der erste Eintrag. Abgelehnt!
(0)Beenden (1)Auto (2)Flugzeug: 1
Neue Teilenummer?: 2345
Baujahr?: 93
(0)Beenden (1)Auto (2)Flugzeug: 0

Teilenummer: 1234
Baujahr: 94

Teilenummer: 2345
Baujahr: 93

Teilenummer: 4434
Baujahr: 93

Listing 15.6 enthält eine geänderte Schnittstelle zu PartsCatalog und ein umgeschriebenes Rahmenprogramm. Die Schnittstellen zu den anderen Klassen sind unverändert aus Listing 15.5 übernommen.

Zeile 258 deklariert PartsCatalog als private Ableitung von PartsList. Die Schnittstelle zu PartsCatalog ist die gleiche wie in Listing 15.5, auch wenn sie ab jetzt natürlich kein Objekt vom Typ PartsList als Datenelement benötigt.

Die PartsCatalog-Funktion ShowAll() ruft die PartsList-Funktion Iterate() mit einem Zeiger auf die verantwortliche Elementfunktion der Klasse Part als Argument auf. ShowAll() fungiert als öffentliche Schnittstelle zu Iterate(). Sie sorgt für einen korrekten Aufruf und verhindert, daß Klient-Klassen Iterate() direkt aufrufen. Mit PartsList mag es möglich sein, daß andere Funktionen an Iterate() übergeben werden, mit PartsCatalog nicht.

Die Funktion Insert() hat sich ebenfalls geändert. Beachten Sie, daß Find() wegen der Ableitungsbeziehung nun direkt aufgerufen werden kann (Zeile 274). Der Aufruf von Insert() in Zeile 275 muß vollständig qualifiziert sein, da er ansonsten endlos wiederholt wird.

Kurz gesagt, wenn Methoden von PartsCatalog Methoden von PartsList aufrufen wollen, kann dies jetzt auf direktem Wege erfolgen. Lediglich wenn PartsCatalog die Methode überschrieben hat und die Version von PartsList benötigt wird, muß der Funktionsname vollständig qualifiziert sein.

Dank der privaten Vererbung kann die PartsCatalog-Klasse erben, was sie benötigt, während der Zugriff auf Insert() und andere Methoden, zu denen Klient-Klassen keinen direkten Zugriff haben sollen, eingeschränkt und von PartsCatalog kontrolliert werden kann.

Was Sie tun sollten

... und was nicht

Arbeiten Sie mit öffentlicher Vererbung, wenn das abgeleitete Objekt auch als Objekt der Basisklasse angesehen werden kann.

Entscheiden Sie sich für Einbettung, wenn Sie Funktionalität an eine andere Klasse delegieren wollen, Sie aber keinen Zugriff auf ihre geschützten Elemente benötigen.

Arbeiten Sie mit privater Vererbung, wenn Sie eine Klasse mit Hilfe einer anderen implementieren möchten und gleichzeitig Zugriff auf die geschützten Elemente der Basisklasse benötigen.

Verzichten Sie auf private Vererbung, wenn Sie mehr als ein Basisklassenobjekt benötigen. Entscheiden Sie sich dann lieber für Einbettung. Wenn zum Beispiel PartsCatalog zwei PartsList-Objekte benötigt, können Sie nicht mit privater Vererbung arbeiten.

Verwenden Sie keine öffentliche Vererbung, wenn Elemente der Basisklasse den Klienten der abgeleiteten Klasse nicht zur Verfügung stehen sollen.

Friend-Klassen

Manchmal erzeugt man Klassen in Paaren oder Gruppen, weil die Klassen in bestimmter Weise zusammengehören und zusammenarbeiten. So stellen zum Beispiel PartNode und PartsList ein Paar dar und es wäre äußerst praktisch gewesen, wenn PartsList, den Part-Zeiger itsPart von PartNode direkt hätte lesen können.

Dabei liegt es gar nicht in Ihrem Interesse, itsPart öffentlich (public) oder geschützt (protected) zu machen, schließlich handelt es sich um ein Implementierungsdetail von PartNode, daß man weiterhin private belassen sollte. Sie würden jedoch gerne PartsList den direkten Zugriff gestatten.

Um private Datenelemente oder Elementfunktionen für eine andere Klasse freizugeben, muß man diese Klasse als Friend (Freund) deklarieren. Dadurch erweitert man die Schnittstelle einer Klasse um die Deklaration der Friend-Klasse.

Nachdem PartNode die Klasse PartsList als Friend deklariert, sind alle Datenelemente und Elementfunktionen aus Sicht von PartsList öffentlich.

Es ist wichtig herauszustreichen, daß Freundschaft nicht übertragbar ist. Nur weil du mein Freund bist und Joe dein Freund ist, bedeutet das nicht, daß Joe mein Freund ist. Freundschaft wird darüber hinaus auch nicht vererbt. Auch hier eine Analogie: Nur weil du mein Freund bist und ich mit dir meine Geheimnisse teile, heißt das noch nicht, daß ich meine Geheimnisse auch deinen Kindern anvertrauen möchte.

Schließlich ist Freundschaft nicht kommutativ. Die Zuweisung von KlasseEins als Friend von KlasseZwei macht KlasseZwei nicht zu einem Friend von KlasseEins. Nur weil du mir deine Geheimnisse mitteilen willst, bedeutet das nicht, daß ich dir auch meine Geheimnisse erzählen will.

Listing 15.7 veranschaulicht Freundschaft anhand einer Neufassung des Beispiels aus Listing 15.6. In diesem Beispiel wird PartsList ein Friend von PartNode. Beachten Sie, daß dadurch nicht automatisch PartNode ein Friend von PartsList wird.

Listing 15.7: Beispiel für eine Friend-Klasse

1:      #include <iostream.h>
2:
3:
4:
5:
6: // **************** Teile ************
7:
8: // Abstrakte Basisklasse fuer die Teile
9: class Part
10: {
11: public:
12: Part():itsPartNumber(1) {}
13: Part(int PartNumber):
14: itsPartNumber(PartNumber){}
15: virtual ~Part(){}
16: int GetPartNumber() const
17: { return itsPartNumber; }
18: virtual void Display() const =0;
19: private:
20: int itsPartNumber;
21: };
22:
23: // Implementierung einer abstrakten Funktion, damit
24: // abgeleitete Klassen die Funktion ueberschreiben
25: void Part::Display() const
26: {
27: cout << "\nTeilenummer: ";
28: cout << itsPartNumber << endl;
29: }
30:
31: // **************** Autoteile ************
32:
33: class CarPart : public Part
34: {
35: public:
36: CarPart():itsModelYear(94){}
37: CarPart(int year, int partNumber);
38: virtual void Display() const
39: {
40: Part::Display();
41: cout << "Baujahr: ";
42: cout << itsModelYear << endl;
43: }
44: private:
45: int itsModelYear;
46: };
47:
48: CarPart::CarPart(int year, int partNumber):
49: itsModelYear(year),
50: Part(partNumber)
51: {}
52:
53:
54: // **************** Flugzeugteile ************
55:
56: class AirPlanePart : public Part
57: {
58: public:
59: AirPlanePart():itsEngineNumber(1){};
60: AirPlanePart
61: (int EngineNumber, int PartNumber);
62: virtual void Display() const
63: {
64: Part::Display();
65: cout << "Motor-Nr.: ";
66: cout << itsEngineNumber << endl;
67: }
68: private:
69: int itsEngineNumber;
70: };
71:
72: AirPlanePart::AirPlanePart
73: (int EngineNumber, int PartNumber):
74: itsEngineNumber(EngineNumber),
75: Part(PartNumber)
76: {}
77:
78: // **************** Teile-Knoten ************
79: class PartNode
80: {
81: public:
82: friend class PartsList;
83: PartNode (Part*);
84: ~PartNode();
85: void SetNext(PartNode * node)
86: { itsNext = node; }
87: PartNode * GetNext() const;
88: Part * GetPart() const;
89: private:
90: Part *itsPart;
91: PartNode * itsNext;
92: };
93:
94:
95: PartNode::PartNode(Part* pPart):
96: itsPart(pPart),
97: itsNext(0)
98: {}
99:
100: PartNode::~PartNode()
101: {
102: delete itsPart;
103: itsPart = 0;
104: delete itsNext;
105: itsNext = 0;
106: }
107:
108: // Liefert NULL zurueck, wenn kein naechster PartNode vorhanden
109: PartNode * PartNode::GetNext() const
110: {
111: return itsNext;
112: }
113:
114: Part * PartNode::GetPart() const
115: {
116: if (itsPart)
117: return itsPart;
118: else
119: return NULL; //Fehler
120: }
121:
122:
123: // **************** Teile-Liste ************
124: class PartsList
125: {
126: public:
127: PartsList();
128: ~PartsList();
129: // benoetigt Kopierkonstruktor und Zuweisungsoperator!
130: void Iterate(void (Part::*f)()const) const;
131: Part* Find(int & position, int PartNumber) const;
132: Part* GetFirst() const;
133: void Insert(Part *);
134: Part* operator[](int) const;
135: int GetCount() const { return itsCount; }
136: static PartsList& GetGlobalPartsList()
137: {
138: return GlobalPartsList;
139: }
140: private:
141: PartNode * pHead;
142: int itsCount;
143: static PartsList GlobalPartsList;
144: };
145:
146: PartsList PartsList::GlobalPartsList;
147:
148: // Implementierungen fuer Lists...
149:
150: PartsList::PartsList():
151: pHead(0),
152: itsCount(0)
153: {}
154:
155: PartsList::~PartsList()
156: {
157: delete pHead;
158: }
159:
160: Part* PartsList::GetFirst() const
161: {
162: if (pHead)
163: return pHead->itsPart;
164: else
165: return NULL; // Fehler auffangen
166: }
167:
168: Part * PartsList::operator[](int offSet) const
169: {
170: PartNode* pNode = pHead;
171:
172: if (!pHead)
173: return NULL; // Fehler auffangen
174:
175: if (offSet > itsCount)
176: return NULL; // Fehler
177:
178: for (int i=0;i<offSet; i++)
179: pNode = pNode->itsNext;
180:
181: return pNode->itsPart;
182: }
183:
184: Part* PartsList::Find(int & position, int PartNumber) const
185: {
186: PartNode * pNode = 0;
187: for (pNode = pHead, position = 0;
188: pNode!=NULL;
189: pNode = pNode->itsNext, position++)
190: {
191: if (pNode->itsPart->GetPartNumber() == PartNumber)
192: break;
193: }
194: if (pNode == NULL)
195: return NULL;
196: else
197: return pNode->itsPart;
198: }
199:
200: void PartsList::Iterate(void (Part::*func)()const) const
201: {
202: if (!pHead)
203: return;
204: PartNode* pNode = pHead;
205: do
206: (pNode->itsPart->*func)();
207: while (pNode = pNode->itsNext);
208: }
209:
210: void PartsList::Insert(Part* pPart)
211: {
212: PartNode * pNode = new PartNode(pPart);
213: PartNode * pCurrent = pHead;
214: PartNode * pNext = 0;
215:
216: int New = pPart->GetPartNumber();
217: int Next = 0;
218: itsCount++;
219:
220: if (!pHead)
221: {
222: pHead = pNode;
223: return;
224: }
225:
226: // Ist dieser kleiner als head
227: // dann ist dies der neue head
228: if (pHead->itsPart->GetPartNumber() > New)
229: {
230: pNode->itsNext = pHead;
231: pHead = pNode;
232: return;
233: }
234:
235: for (;;)
236: {
237: // gibt es keinen next, den neuen anhaengen
238: if (!pCurrent->itsNext)
239: {
240: pCurrent->itsNext = pNode;
241: return;
242: }
243:
244: // gehoert der Knoten zwischen diesen und den naechsten,
245: // dann hier einfuegen, ansonsten zu naechstem Knoten gehen
246: pNext = pCurrent->itsNext;
247: Next = pNext->itsPart->GetPartNumber();
248: if (Next > New)
249: {
250: pCurrent->itsNext = pNode;
251: pNode->itsNext = pNext;
252: return;
253: }
254: pCurrent = pNext;
255: }
256: }
257:
258: class PartsCatalog : private PartsList
259: {
260: public:
261: void Insert(Part *);
262: int Exists(int PartNumber);
263: Part * Get(int PartNumber);
264: operator+(const PartsCatalog &);
265: void ShowAll() { Iterate(Part::Display); }
266: private:
267: };
268:
269: void PartsCatalog::Insert(Part * newPart)
270: {
271: int partNumber = newPart->GetPartNumber();
272: int offset;
273:
274: if (!Find(offset, partNumber))
275: PartsList::Insert(newPart);
276: else
277: {
278: cout << partNumber << " war der ";
279: switch (offset)
280: {
281: case 0: cout << "erste "; break;
282: case 1: cout << "zweite "; break;
283: case 2: cout << "dritte "; break;
284: default: cout << offset+1 << "th ";
285: }
286: cout << "Eintrag. Abgelehnt!\n";
287: }
288: }
289:
290: int PartsCatalog::Exists(int PartNumber)
291: {
292: int offset;
293: Find(offset,PartNumber);
294: return offset;
295: }
296:
297: Part * PartsCatalog::Get(int PartNumber)
298: {
299: int offset;
300: return (Find(offset, PartNumber));
301:
302: }
303:
304: int main()
305: {
306: PartsCatalog pc;
307: Part * pPart = 0;
308: int PartNumber;
309: int value;
310: int choice;
311:
312: while (1)
313: {
314: cout << "(0)Beenden (1)Auto (2)Flugzeug: ";
315: cin >> choice;
316:
317: if (!choice)
318: break;
319:
320: cout << "Neue Teilenummer?: ";
321: cin >> PartNumber;
322:
323: if (choice == 1)
324: {
325: cout << "Baujahr?: ";
326: cin >> value;
327: pPart = new CarPart(value,PartNumber);
328: }
329: else
330: {
331: cout << "Motor-Nummer?: ";
332: cin >> value;
333: pPart = new AirPlanePart(value,PartNumber);
334: }
335: pc.Insert(pPart);
336: }
337: pc.ShowAll();
338: return 0;
339: }

 (0)Beenden (1)Auto (2)Flugzeug:  1
Neue Teilenummer?: 1234
Baujahr?: 94
(0)Beenden (1)Auto (2)Flugzeug: 1
Neue Teilenummer?: 4434
Baujahr?: 93
(0)Beenden (1)Auto (2)Flugzeug: 1
Neue Teilenummer?: 1234
Baujahr?: 94
1234 war der erste Eintrag. Abgelehnt!
(0)Beenden (1)Auto (2)Flugzeug: 1
Neue Teilenummer?: 2345
Baujahr?: 93
(0)Beenden (1)Auto (2)Flugzeug: 0

Teilenummer: 1234
Baujahr: 94

Teilenummer: 2345
Baujahr: 93

Teilenummer: 4434
Baujahr: 93

Zeile 82 deklariert die Klasse PartsList als Friend von PartNode.

Im Beispiel befindet sich die Friend-Deklaration in dem öffentlichen Abschnitt. Dies ist aber nicht erforderlich, Sie können sie auch an einer beliebigen anderen Stelle in der Klassendeklaration einfügen, ohne daß die Bedeutung der Anweisung sich dadurch ändert. Aufgrund der Friend-Deklaration sind alle privaten Datenelemente und -funktionen für die Elementfunktionen der Klasse PartsList verfügbar.

In Zeile 160 spiegelt die Implementierung der Elementfunktion GetFirst() diese Änderung wider. Anstatt pHead->GetPart() zurückzuliefern, kann diese Funktion jetzt die ansonsten privaten Datenelemente mit der Anweisung pHead->itsPart zurückgeben. Entsprechend kann Insert() statt pNode->SetNext(pHead) jetzt pNode->itsNext = pHead schreiben.

Dies sind zugegebenermaßen unwesentliche Änderungen und es gibt auch keinen wirklich guten Grund, um PartsList zum Friend von PartNode zu machen, aber sie veranschaulichen doch ausreichend, wie man das Schlüsselwort friend verwendet.

Deklarationen von Friend-Klassen sollte man mit äußerster Vorsicht verwenden. Wenn zwei Klassen durch und durch verflochten sind und man häufig auf Daten in der anderen zugreifen muß, gibt es gute Gründe, diese Deklaration zu verwenden. Trotzdem sollte man sparsam damit umgehen. Es ist oftmals genauso einfach, die öffentlichen Zugriffsmethoden zu nutzen. Letzterer Ansatz hat außerdem den Vorteil, daß man eine Klasse ändern kann, ohne die andere neu kompilieren zu müssen.

Oftmals beklagen sich Neueinsteiger in die C++-Programmierung darüber, daß Friend-Deklarationen die für die objektorientierte Programmierung so bedeutsame Kapselung untergraben. Offen gesagt ist das blanker Unsinn. Die Friend-Deklaration macht den deklarierten Freund zum Teil der Klassenschnittstelle und ist genausowenig ein Unterwandern der Kapselung wie eine öffentliche Ableitung.

Friend-Klasse

Um eine Klasse als Friend einer anderen zu deklarieren, führen Sie die Klasse zusammen mit dem Schlüsselwort friend in der Klasse, die die Zugriffsrechte einräumt, auf. Ich kann sozusagen Sie als meinen Freund deklarieren, Sie aber können sich nicht selbst als mein Freund ausgeben.

Beispiel:

     class PartNode{
public:
friend class PartsList; // deklariert PartsList als Freund
// von PartNode
};

Friend-Funktionen

Manchmal will man den freundschaftlichen Zugriff nicht für eine gesamte Klasse, sondern nur für eine oder zwei Funktionen dieser Klasse gewähren. Das läßt sich realisieren, indem man die Elementfunktionen der anderen Klasse und nicht die gesamte Klasse als Friend deklariert. In der Tat kann man jede Funktion als Friend-Funktion deklarieren, ob sie nun eine Elementfunktion einer anderen Klasse ist oder nicht.

Friend-Funktionen und das Überladen von Operatoren

Listing 15.1 stellte eine String-Klasse bereit, die den Operator operator+ überschrieb. Zusätzlich wurde ein Konstruktor deklariert, der einen konstanten char-Zeiger übernahm, so daß es möglich war, aus C-Strings String-Objekte zu erzeugen. Auf diese Weise konnten Sie einen String erzeugen und ihm einen C-String anhängen.

C-Strings sind Zeichen-Arrays mit einem abschließenden Null-Zeichen, wie char mystring[] = "Hello World".

Was jedoch nicht möglich war: einen C-String (einen Zeichenstring) zu erzeugen und ihm ein String-Objekt wie im folgenden Beispiel anzuhängen:

char cString[] = {"Hello"};
String sString(" world");
String sStringTwo = cString + sString; //Fehler

C-Strings haben keinen überladenen operator+. Wie Sie bereits in Kapitel 10, »Funktionen - weiterführende Themen«, gelernt haben, verbirgt sich hinter dem Aufruf cString + sString im Grunde genommen der Aufruf cString.operator+(sString). Da Sie operator+ jedoch nicht für einen C-String aufrufen können, wird ein Compiler- Fehler gemeldet.

Dieses Problem läßt sich lösen, indem Sie in String eine Friend-Funktion deklarieren, die operator+ überlädt und zwei String-Objekte als Argumente übernimmt. Der C- String wird von dem entsprechenden Konstruktor in ein String-Objekt konvertiert und anschließend wird operator+ mit den beiden String-Objekte aufgerufen.

Listing 15.8: Der freundliche +-Operator

1:     //Listing 15.8 - Friend-Operatoren
2:
3: #include <iostream.h>
4: #include <string.h>
5:
6: // Rudimentaere String-Klasse
7: class String
8: {
9: public:
10: // Konstruktoren
11: String();
12: String(const char *const);
13: String(const String &);
14: ~String();
15:
16: // Ueberladene Operatoren
17: char & operator[](int offset);
18: char operator[](int offset) const;
19: String operator+(const String&);
20: friend String operator+(const String&, const String&);
21: void operator+=(const String&);
22: String & operator= (const String &);
23:
24: // Allgemeine Zugriffsfunktionen
25: int GetLen()const { return itsLen; }
26: const char * GetString() const { return itsString; }
27:
28: private:
29: String (int); // privater Konstruktor
30: char * itsString;
31: unsigned short itsLen;
32: };
33:
34: // Standardkonstruktor erzeugt String von 0 Byte
35: String::String()
36: {
37: itsString = new char[1];
38: itsString[0] = '\0';
39: itsLen=0;
40: // cout << "\tString-Standardkonstruktor\n";
41: // ConstructorCount++;
42: }
43:
44: // Privater (Hilfs-) Konstruktor, der nur von Methoden
45: // der Klasse zum Erzeugen neuer Null-Strings der
46: // erforderlichen Groeße verwendet wird.
47: String::String(int len)
48: {
49: itsString = new char[len+1];
50: for (int i = 0; i<=len; i++)
51: itsString[i] = '\0';
52: itsLen=len;
53: // cout << "\tString(int)-Konstruktor\n";
54: // ConstructorCount++;
55: }
56:
57: //Konvertiert einen Zeichen-Array in einen String
58: String::String(const char * const cString)
59: {
60: itsLen = strlen(cString);
61: itsString = new char[itsLen+1];
62: for (int i = 0; i<itsLen; i++)
63: itsString[i] = cString[i];
64: itsString[itsLen]='\0';
65: // cout << "\tString(char*)-Konstruktor\n";
66: // ConstructorCount++;
67: }
68:
69: // Kopierkonstruktor
70: String::String (const String & rhs)
71: {
72: itsLen=rhs.GetLen();
73: itsString = new char[itsLen+1];
74: for (int i = 0; i<itsLen;i++)
75: itsString[i] = rhs[i];
76: itsString[itsLen] = '\0';
77: // cout << "\tString(String&)-Konstruktor\n";
78: // ConstructorCount++;
79: }
80:
81: // Destruktor, gibt zugewiesenen Speicher frei
82: String::~String ()
83: {
84: delete [] itsString;
85: itsLen = 0;
86: // cout << "\tString-Destruktor\n";
87: }
88:
89: // Zuweisungsoperator, gibt vorhandenen Speicher frei,
90: // kopiert dann String und Groeße
91: String& String::operator=(const String & rhs)
92: {
93: if (this == &rhs)
94: return *this;
95: delete [] itsString;
96: itsLen=rhs.GetLen();
97: itsString = new char[itsLen+1];
98: for (int i = 0; i<itsLen;i++)
99: itsString[i] = rhs[i];
100: itsString[itsLen] = '\0';
101: return *this;
102: // cout << "\tString-Operator=\n";
103: }
104:
105: // Nicht konstanter Offset-Operator, gibt Referenz
106: // auf Zeichen zurueck, das sich damit aendern
107: // laesst!
108: char & String::operator[](int offset)
109: {
110: if (offset > itsLen)
111: return itsString[itsLen-1];
112: else
113: return itsString[offset];
114: }
115:
116: // Konstanter Offset-Operator fuer konstante
117: // Objekte (siehe Kopierkonstruktor!)
118: char String::operator[](int offset) const
119: {
120: if (offset > itsLen)
121: return itsString[itsLen-1];
122: else
123: return itsString[offset];
124: }
125: // Erzeugt einen neuen String durch Anfuegen von rhs
126: // an den aktuellen String
127: String String::operator+(const String& rhs)
128: {
129: int totalLen = itsLen + rhs.GetLen();
130: String temp(totalLen);
131: int i, j;
132: for (i = 0; i<itsLen; i++)
133: temp[i] = itsString[i];
134: for (j = 0, i = itsLen; j<rhs.GetLen(); j++, i++)
135: temp[i] = rhs[j];
136: temp[totalLen]='\0';
137: return temp;
138: }
139:
140: // erzeugt einen neuen String, indem ein String
141: // an einen anderen String gehaengt wird
142: String operator+(const String& lhs, const String& rhs)
143: {
144: int totalLen = lhs.GetLen() + rhs.GetLen();
145: String temp(totalLen);
146: int i, j;
147: for (i = 0; i<lhs.GetLen(); i++)
148: temp[i] = lhs[i];
149: for (j = 0, i = lhs.GetLen();; j<rhs.GetLen(); j++, i++)
150: temp[i] = rhs[j];
151: temp[totalLen]='\0';
152: return temp;
153: }
154:
155: int main()
156: {
157: String s1("String Eins ");
158: String s2("String Zwei ");
159: char *c1 = { "C-String Eins " } ;
160: String s3;
161: String s4;
162: String s5;
163:
164: cout << "s1: " << s1.GetString() << endl;
165: cout << "s2: " << s2.GetString() << endl;
166: cout << "c1: " << c1 << endl;
167: s3 = s1 + s2;
168: cout << "s3: " << s3.GetString() << endl;
169: s4 = s1 + c1;
170: cout << "s4: " << s4.GetString() << endl;
171: s5 = c1 + s2;
172: cout << "s5: " << s5.GetString() << endl;
173: return 0;
174: }

s1: String Eins
s2: String Zwei
c1: C-String Eins
s3: String Eins String Zwei
s4: String Eins C-String Eins
s5: C-String Eins String Zwei

Bis auf operator+ wurden die Implementierungen der String-Methoden unverändert aus Listing 15.1 übernommen. Zeile 20 überlädt einen neuen operator+, der zwei konstante String-Referenzen übernimmt und einen String zurückgibt. Diese Funktion wird als Friend deklariert.

Beachten Sie, daß operator+ keine Elementfunktion dieser oder einer anderen Klasse ist. Die Deklaration der Operatorfunktion in der String-Klasse dient lediglich dazu, sie als Friend zu kennzeichnen. Da sie aber nun einmal deklariert wird, wird kein anderer Funktionsprototyp benötigt.

Die Implementierung von operator+ befindet sich in den Zeilen 142 bis 153. Beachten Sie, daß sie der Implementierung des früheren +-Operators sehr ähnlich ist. Hier übernimmt die Funktion jedoch zwei Strings und manipuliert diese ausnahmslos über deren öffentliche Zugriffsfunktionen.

Das Rahmenprogramm veranschaulicht den Einsatz dieser Funktion (Zeile 171). Dort wird operator+ auf einen C-String angewendet.

Friend-Funktionen

Die Deklaration einer Friend-Funktion erfolgt mittels des Schlüsselwortes friend und der vollständigen Spezifikation der Funktion. Eine Funktion, die als friend deklariert wurde, erhält keinen Zugriff auf den this-Zeiger der Klasse, sie hat jedoch vollen Zugriff auf alle privaten und geschützten Datenelemente und -funktionen.

Beispiel:

     class PartNode
{ // ...
// Elementfunktion einer anderen Klasse als friend deklarieren
friend void PartsList::Insert(Part *);
// eine globale Funktion als friend deklarieren
friend int SomeFunction();
// ...
};

Überladung des Ausgabe-Operators

Jetzt sind Sie soweit, Ihre String-Klasse so auszustatten, daß String-Objekte, wie Objekte der elementaren Datentypen mit cout ausgegeben werden können. Bis jetzt mußten Sie, um einen String auszugeben, folgenden Code aufsetzen:

cout << einString.GetString();

Besser wäre aber folgende Schreibweise:

cout << einString;

Um dies zu erreichen, müssen Sie den Ausgabe-Operator operator<<() überladen. In Kapitel 16, »Streams«, erfahren Sie Näheres zur Arbeit mit iostreams. Hier soll Listing 15.9 zeigen, wie operator<< mit Hilfe einer friend-Funktion überladen werden kann.

Listing 15.9: Überladen des Ausgabe-Operators <<

1:     #include <iostream.h>
2: #include <string.h>
3:
4: class String
5: {
6: public:
7: // Konstruktoren
8: String();
9: String(const char *const);
10: String(const String &);
11: ~String();
12:
13: // Ueberladene Operatoren
14: char & operator[](int offset);
15: char operator[](int offset) const;
16: String operator+(const String&);
17: void operator+=(const String&);
18: String & operator= (const String &);
19: friend ostream& operator<<
20: ( ostream& theStream,String& theString);
21: // Allgemeine Zugriffsfunktionen
22: int GetLen()const { return itsLen; }
23: const char * GetString() const { return itsString; }
24:
25: private:
26: String (int); // privater Konstruktor
27: char * itsString;
28: unsigned short itsLen;
29: };
30:
31:
32: // Standardkonstruktor erzeugt String von 0 Byte Laenge
33: String::String()
34: {
35: itsString = new char[1];
36: itsString[0] = '\0';
37: itsLen=0;
38: // cout << "\tString-Standardkonstruktor\n";
39: // ConstructorCount++;
40: }
41:
42: // Privater (Hilfs-) Konstruktor, der nur von Methoden
43: // der Klasse zum Erzeugen eines neuen Null-Strings der
44: // erforderlichen Groeße verwendet wird.
45: String::String(int len)
46: {
47: itsString = new char[len+1];
48: for (int i = 0; i<=len; i++)
49: itsString[i] = '\0';
50: itsLen=len;
51: // cout << "\tString(int)-Konstruktor\n";
52: // ConstructorCount++;
53: }
54:
55: // Konvertiert einen Zeichen-Array in einen String
56: String::String(const char * const cString)
57: {
58: itsLen = strlen(cString);
59: itsString = new char[itsLen+1];
60: for (int i = 0; i<itsLen; i++)
61: itsString[i] = cString[i];
62: itsString[itsLen]='\0';
63: // cout << "\tString(char*)-Konstruktor\n";
64: // ConstructorCount++;
65: }
66:
67: // Kopierkonstruktor
68: String::String (const String & rhs)
69: {
70: itsLen=rhs.GetLen();
71: itsString = new char[itsLen+1];
72: for (int i = 0; i<itsLen;i++)
73: itsString[i] = rhs[i];
74: itsString[itsLen] = '\0';
75: // cout << "\tString(String&)-Konstruktor\n";
76: // ConstructorCount++;
77: }
78:
79: // Destruktor, gibt zugewiesenen Speicher frei
80: String::~String ()
81: {
82: delete [] itsString;
83: itsLen = 0;
84: // cout << "\tString-Destruktor\n";
85: }
86:
87: // Zuweisungsoperator, gibt vorhandenen Speicher frei,
88: // kopiert dann String und Groeße
89: String& String::operator=(const String & rhs)
90: {
91: if (this == &rhs)
92: return *this;
93: delete [] itsString;
94: itsLen=rhs.GetLen();
95: itsString = new char[itsLen+1];
96: for (int i = 0; i<itsLen;i++)
97: itsString[i] = rhs[i];
98: itsString[itsLen] = '\0';
99: return *this;
100: // cout << "\tString-Operator=\n";
101: }
102:
103: // Nicht konstanter Offset-Operator, gibt Referenz
104: // auf Zeichen zurueck, das sich damit aendern
105: // laeßt!
106: char & String::operator[](int offset)
107: {
108: if (offset > itsLen)
109: return itsString[itsLen-1];
110: else
111: return itsString[offset];
112: }
113:
114: // Konstanter Offset-Operator fuer konstante
115: // Objekte (siehe Kopierkonstruktor!)
116: char String::operator[](int offset) const
117: {
118: if (offset > itsLen)
119: return itsString[itsLen-1];
120: else
121: return itsString[offset];
122: }
123:
124: // Erzeugt einen neuen String durch Anfuegen von rhs
125: // an den aktuellen String
126: String String::operator+(const String& rhs)
127: {
128: int totalLen = itsLen + rhs.GetLen();
129: String temp(totalLen);
130: int i, j;
131: for (i = 0; i<itsLen; i++)
132: temp[i] = itsString[i];
133: for (j = 0; j<rhs.GetLen(); j++, i++)
134: temp[i] = rhs[j];
135: temp[totalLen]='\0';
136: return temp;
137: }
138:
139: // aendert aktuellen String, liefert nichts zurueck
140: void String::operator+=(const String& rhs)
141: {
142: unsigned short rhsLen = rhs.GetLen();
143: unsigned short totalLen = itsLen + rhsLen;
144: String temp(totalLen);
145: int i, j;
146: for (i = 0; i<itsLen; i++)
147: temp[i] = itsString[i];
148: for (j = 0, i = 0; j<rhs.GetLen(); j++, i++)
149: temp[i] = rhs[i-itsLen];
150: temp[totalLen]='\0';
151: *this = temp;
152: }
153:
154: // int String::ConstructorCount =
155: ostream& operator<< ( ostream& theStream,String& theString)
156: {
157: theStream << theString.itsString;
158: return theStream;
159: }
160:
161: int main()
162: {
163: String theString("Hello world.");
164: cout << theString;
165: return 0;
166: }

Hello world.

Zeile 19 deklariert operator<< als friend-Funktion, die eine ostream-Referenz und eine String-Referenz übernimmt und eine ostream-Referenz zurückgibt. Beachten Sie, daß es sich dabei nicht um eine Elementfunktion von String handelt. Der Operator liefert eine Referenz auf ein ostream-Objekt zurück, damit Aufrufe von operator<< wie folgt verkettet werden können:

cout << "meinAlter: " << itsAge << " Jahre.";

Die Implementierung der friend-Funktion finden Sie in den Zeilen 155 bis 159. Der Code verbirgt lediglich die Weiterleitung des Strings an ostream, und genau das soll ja auch erreicht werden. In Kapitel 16 werden Sie mehr zur Überladung der Operatoren und des Operator>> erfahren.

Zusammenfassung

Heute haben Sie gelernt, wie man funktionelle Aufgaben an eingebettete Objekte delegiert. Sie haben auch gesehen, wie man eine Klasse mit Hilfe einer anderen (durch Einbettung oder private Vererbung) implementiert. Die Einbettung ist insofern eingeschränkt, als die neue Klasse keinen Zugriff auf geschützte Elemente der eingebetteten Klasse hat und sie auch nicht die Elementfunktionen des eingebetteten Objekts überschreiben kann. Einbettung ist einfacher anzuwenden als private Vererbung und sollte möglichst den Vorzug erhalten.

Weiterhin wurde erläutert, wie man sowohl Friend-Funktionen als auch Friend-Klassen deklariert. Am Beispiel des Ausgabe-Operators haben Sie gesehen, wie man eine Friend-Funktion definiert und wie man Objekte selbst definierter Klassen über cout ausgeben kann.

Denken Sie daran, daß öffentliche Vererbung eine ist-ein-Beziehung, Einbettung eine hat-ein-Beziehung und private Vererbung implementiert mit Hilfe von ausdrückt. Die Beziehung delegiert an kann sowohl durch Einbettung als auch durch private Vererbung ausgedrückt werden. Einbettung ist jedoch gebräuchlicher.

Fragen und Antworten

Frage:
Warum ist es so wichtig, zwischen ist-ein, hat-ein und implementiert mit Hilfe von zu differenzieren?

Antwort:
Das Ziel von C++ ist die Implementierung gut konzipierter objektorientierten Programme. Durch Auseinanderhalten dieser Beziehungen können Sie leichter sicherstellen, daß Ihr Entwurf auch mit der abzubildenden Wirklichkeit übereinstimmt. Außerdem führt ein gut konzipierter Entwurf häufig auch zu einem leichter verständlichen Code.

Frage:
Warum sollte man die Einbettung der privaten Vererbung vorziehen?

Antwort:
Eine der Herausforderungen der modernen Programmierung ist, der zunehmenden Komplexität Herr zu werden. Je weniger Gedanken Sie sich bei der Arbeit mit den Objekten Ihrer Klassen um die Details von deren Implementierung machen müssen, um so mehr Komplexität können Sie in Ihren Programmen bewältigen. Enthaltene Klassen verbergen ihre Details, durch private Vererbung werden die Implementierungsdetails bloßgelegt.

Frage:
Warum macht man nicht alle Klassen zu Freunden aller Klassen, die sie verwenden?

Antwort:
Wenn man eine Klasse zum Friend einer anderen macht, legt man die Details der Implementierung frei und verringert die Kapselung. Im Idealfall sollte man möglichst viele Details jeder Klasse vor allen anderen Klassen verbergen.

Frage:
Wenn eine Funktion überladen wurde, müssen Sie dann alle überladenen Versionen der Funktion als friend deklarieren?

Antwort:
Ja. Wenn Sie eine Funktion überladen und sie als Friend einer anderen deklarieren, müssen Sie jede andere Version, der Sie ebenfalls diesen Zugriff einräumen wollen, ebenfalls als friend deklarieren.

Workshop

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

Quiz

  1. Wie erzeugt man eine ist-ein-Beziehung?
  2. Wie erzeugt man eine hat-ein-Beziehung?
  3. Was ist der Unterschied zwischen Einbettung und Delegierung?
  4. Was ist der Unterschied zwischen Delegierung und implementiert mit Hilfe von?
  5. Was ist eine friend-Funktion?
  6. Was ist eine friend-Klasse?
  7. Wenn Dog ein Friend von Boy ist, ist Boy dann auch ein Friend von Dog?
  8. Wenn Dog ein Friend von Boy ist und Terrier sich von Dog ableitet, ist Terrier dann auch ein Friend von Boy?
  9. Wenn Dog ein Friend von Boy ist und Boy ein Friend von House ist, ist Dog dann auch ein Friend von House?
  10. Wo innerhalb einer Klassendeklaration sollte man eine friend-Funktion deklarieren?

Übungen

  1. Setzen Sie die Deklaration einer Klasse Animal auf, die ein String-Objekt als Datenelement enthält.
  2. Deklarieren Sie eine Klasse BoundedArray, die ein Array darstellt.
  3. Wie deklariert man eine Klasse Menge auf der Grundlage der Klasse Array.
  4. Erweitern Sie Listing 15.1 um einen Eingabe-Operator (>>) fuer die String-Klasse.
  5. FEHLERSUCHE: Was ist falsch an folgendem Programm?
    1:     #include <iostream.h>
    2:
    3: class Animal;
    4:
    5: void setValue(Animal& , int);
    6:
    7:
    8: class Animal
    9: {
    10: public:
    11: int GetWeight()const { return itsWeight; }
    12: int GetAge() const { return itsAge; }
    13: private:
    14: int itsWeight;
    15: int itsAge;
    16: };
    17:
    18: void setValue(Animal& theAnimal, int theWeight)
    19: {
    20: friend class Animal;
    21: theAnimal.itsWeight = theWeight;
    22: }
    23:
    24: int main()
    25: {
    26: Animal peppy;
    27: setValue(peppy,5);
    28: }
  6. Beheben Sie den Fehler in Übung 5, so daß sich der Code kompilieren läßt.
  7. FEHLERSUCHE: Was ist falsch an diesem Code?
    1:     #include <iostream.h>
    2:
    3: class Animal;
    4:
    5: void setValue(Animal& , int);
    6: void setValue(Animal& ,int,int);
    7:
    8: class Animal
    9: {
    10: friend void setValue(Animal& ,int);
    11: private:
    12: int itsWeight;
    13: int itsAge;
    14: };
    15:
    16: void setValue(Animal& theAnimal, int theWeight)
    17: {
    18: theAnimal.itsWeight = theWeight;
    19: }
    20:
    21:
    22: void setValue(Animal& theAnimal, int theWeight, int theAge)
    23: {
    24: theAnimal.itsWeight = theWeight;
    25: theAnimal.itsAge = theAge;
    26: }
    27:
    28: int main()
    29: {
    30: Animal peppy;
    31: setValue(peppy,5);
    32: setValue(peppy,7,9);
    33: }
  8. Beheben Sie den Fehler in Übung 7, so daß sich der Code kompilieren läßt.



vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


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