Vererbung |
»Ich fürchte, es handelt sich hierbei um eine Erbkrankheit.« - »Gut, Herr Doktor. Dann schicken Sie doch bitte Ihre Rechnung an meinen Vater.«
Die Informatik steckt voller schöner Analogien. So hat die objektorientierte Programmierung den Begriff der »Vererbung« eingeführt, wenn eine Klasse von einer anderen Klasse abgeleitet wird und deren Eigenschaften übernimmt. Der Begriff Vererbung hat hier weniger mit dem Testament eines verblichenen Verwandten zu tun als mit den biologischen Regeln, die ein Herr Mendel dankenswerterweise entdeckte. Da meine Kenntnisse in Biologie eher bedauernswert sind, müssen Sie hier auf einen unterhaltsamen Ausflug in die Biologie verzichten. Aber wenn Sie einmal ein Biologie-Buch in die Hände bekommen, werden Sie feststellen, dass sich die mendelschen Regeln sowieso nicht genauso verhalten wie Basisklassen und deren abgeleiteten Klassen. Dennoch kenne ich mindestens einen Biologen, der ein prima Programmierer geworden ist.
An dieser Stelle steht im Buch die Abbildung "Personenklassen" (abbpersonenerbe)
Damit stellt die Ausgangsklasse Person die Verallgemeinerung dar und jede abgeleitete Klasse eine Spezialisierung. Der Vorteil dieser Technik ist, dass der bestehende Code der Basisklasse nicht noch einmal für die neu geschaffene Klasse geschrieben werden muss. Beispielsweise würde eine Prüffunktion der Adresse, die der Klasse Person hinzugefügt wird, automatisch auch allen anderen Klassen, die direkt oder indirekt von Person abgeleitet wurden, hinzugefügt, ohne dass eine Zeile Code mehr geschrieben werden müsste. Eine Änderung in der Klasse Mitarbeiter würde immer auch auf die Außendienstler durchschlagen. Es gibt aber keine Rückwirkung auf die Geschäftspartner.
[Personen]
class Person
{
public:
string Name, Adresse, Telefon;
};
class Partner : public Person
{
public:
string Kto, BLZ;
};
class Mitarbeiter : public Partner
{
public:
string Krankenkasse;
};
class Kunde : public Partner
{
public:
string Lieferadresse;
};
class Lieferant : public Partner
{
public:
tOffenePosten *Rechnungen;
};
class Aussendienst : public Mitarbeiter
{
public:
tBezirk Bezirk;
};
Person person;
Mitarbeiter mitarbeiter;
person = mitarbeiter; // ok
mitarbeiter = person; // das mag der Compiler nicht
[Feiertag]
class tFeiertag : public tDatum
{
public:
char Name[40];
};
tFeiertag Ostern;
Ostern.Tag = 25;
Ostern.Monat = 4;
Anschließend können Sie mit dem Objekt Ostern genauso umgehen
wie mit jeder Instanz von tDatum. Die Klasse
tFeiertag erbt alle Eigenschaften
von tDatum und fügt lediglich die Besonderheiten eines Feiertags
hinzu.
Ein wichtiger Vorteil besteht darin, dass Sie die Klasse tDatum
nicht verändern müssen. Jede Veränderung eines Code-Segments kann zu Fehlern
führen. Aus diesem Grund kann es auch sein, dass Sie in größeren Projekten
überhaupt keine Berechtigung bekommen, Klassen zu ändern, von denen Sie
etwas ableiten wollen.
Gerade weil der Anwender der Basisklassen nicht den Code in der Basisklasse
anpassen muss, sind Klassenbibliotheken flexibler als Funktionsbibliotheken.
Änderungen werden einfach in einer abgeleiteten Klasse durchgeführt.
[protected]
[Ändern der Zugriffsart]
Ein solches Vorgehen werden Sie schon deswegen in der Praxis selten finden,
weil es den Eindruck macht, dass beim Design nicht sorgfältig
gearbeitet wurde.
Es ist also nur als Notlösung zu betrachten, um ungeschickt vergebene
Zugriffsrechte durch Ableitung zu korrigieren.
[Privates Ableiten]
[Funktionsaufruf der Basisklasse (kasfunc.cpp)]
Im Beispiel wird die Funktion TuWas() in der Klasse
tSpezialfall zunächst die Funktion
TuWas() der Klasse tBasis aufrufen und dann die Sonderfälle
der abgeleiteten Klasse behandeln.
[Konstruktoraufruf (kaskonstrukt.cpp)]
Das Anlegen eines Objekts vom Typ tSpezialfall ruft den
Standardkonstruktor auf. Ohne den Initialisierer würde der Compiler den
Standardkonstruktor von tBasis aufrufen. Den gibt es allerdings
nicht. Durch den Initialisierer wird der Konstruktor mit dem Integer-Parameter
explizit aufgerufen, bevor die Initialisierung von tSpezialfall
beginnt.
Zugriff auf die Vorfahren
Elemente einer Klasse sind standardmäßig nur für andere Elemente dieser Klasse
zugreifbar. Elemente, die auch von anderen Klassen erreichbar sein sollen,
werden als public deklariert.
Das gilt auch bei der Vererbung. Eine abgeleitete Klasse kann nicht auf
die privaten Elemente der Basisklasse zugreifen.
Dagegen kann sie auf die öffentlichen Elemente der Basisklasse zugreifen
wie auf die eigenen.
Durch die Vererbung gibt es neben private und
public eine zusätzliche Zugriffsvariante. Das Attribut heißt
protected und bewirkt, dass nur abgeleitete Klassen Zugriff auf
die Elemente bekommen.
Im darauf folgenden Abschnitt sehen wir uns an, was es eigentlich heißt,
dass eine Vererbung öffentlich durchgeführt wird.
protected
Neben den Zugriffsrechten private und
public bringt die Vererbung eine dritte Variante mit sich.
Mit ihr können Klassenelemente so privatisiert werden, dass auf sie außer von
der eigenen Klasse nur von der abgeleiteten Klasse zugegriffen werden kann.
Dafür verwendet man das Schlüsselwort protected.
class Basis
{
private:
int privat;
protected:
int protect;
public:
int publik;
};
class Abgelitten : public Basis
{
void zugriff()
{
a = privat; // Das gibt Ärger!
a = protect; // Das funktioniert.
a = publik; // Das funktioniert sowieso.
}
};
int main()
{
Basis myVar;
a = myVar.privat; // Das läuft natürlich nicht.
a = myVar.protect; // Das geht auch nicht.
a = myVar.publik; // Das funktioniert.
}
Ändern der Öffentlichkeit
Eine abgeleitete Klasse kann die ihr zugänglichen Elemente der Basisklasse
in eine andere Öffentlichkeit stellen. Beispiel:
class Basis
{
private:
int privat;
protected:
int protect;
public:
int publik;
};
class Abgelitten : public Basis
{
protected:
using Basis::publik;
public:
using Basis::protect;
};
int main()
{
Abgelitten a;
b = a.protect; // O Wunder, es geht!
}
Zugriffsattribute zur Vererbung
Bisher haben wir nur Ableitungen betrachtet, die das Schlüsselwort
public vor den Namen der Basisklasse stellen. Es ist nahe liegend,
dass dort statt public auch
protected oder
private stehen kann.
Die Konsequenzen sollten Sie schon deswegen kennen, weil das Weglassen
des Vererbungsattributs immer bedeutet, dass die Klasse privat abgeleitet
wird.
Öffentliche Elemente der Basisklasse werden durch privates Ableiten
in der abgeleiteten Klasse privat.
class Basis
{
private:
int privat;
protected:
int protect;
public:
int publik;
};
class Abgelitten : Basis // Vorsicht: das ist private!
{
int f1() { return privat; } // das geht nicht!
int f2() { return protect; } // das geht
int f3() { return publik; } // das geht
};
int main()
{
Abgelitten a;
Basis b;
i = a.publik; // Das geht schief!
i = b.publik; // Das funktioniert einwandfrei.
b = a; // Das funktioniert wieder nicht.
}
Beschränkung
Eine privat abgeleitete Klasse wie hier Abgelitten hat die gleichen
Zugriffsmöglichkeiten innerhalb einer Elementfunktion wie bei einer
öffentlichen Vererbung.
Das kann man an den drei Beispielen für Elementfunktionen sehen. Der
Unterschied wird bei den Zugriffen von außen deutlich. Man kann bei dem
abgeleiteten Objekt nicht mehr auf die öffentlichen Elemente der Basisklasse
zugreifen. Auch die Möglichkeit, ein Objekt der abgeleiteten Klassen einem
Objekt der Basisklasse zuzuweisen, wird vom Compiler sofort unterbunden.
protected
Eine protected abgeleitete Klasse unterscheidet sich in den
Zugriffsrechten von außen nicht von einer privat abgeleiteten Klasse.
Eine Konvertierung ist allerdings innerhalb einer Elementfunktion noch
möglich.
In der Praxis finden Vererbungen fast nur öffentlich statt.
Als private oder
protected gekennzeichnete Ableitungen bringen wenig praktischen
Nutzen. Wenn Sie einen Einsatz für eine solche Vererbung finden, sollten Sie
die Stelle ausgiebig kommentieren.
Elemente der Basisklassen
Bei der Ableitung einer Klasse werden die Elemente der Basisklasse
vererbt. Sofern die Elemente als public oder
protected deklariert sind, kann auf sie innerhalb der abgeleiteten
Klasse genauso zugegriffen werden wie auf eigene Elemente.
Wenn Sie ein Element in der abgeleiteten Klasse definieren,
dessen Name bereits in der Basisklasse verwendet wird,
wird das gleichnamige Element der Basisklasse überdeckt.
Funktionsaufruf
In manchen Fällen sollen Funktionen der Basisklasse auch in der abgeleiteten
Klasse zur Verfügung stehen. Es sollen nur ein paar Zeilen ergänzt werden.
Dann wird die Funktion in der abgeleiteten Klasse neu implementiert und die
Funktion der Basisklasse
an passender Stelle in der neuen Funktion direkt aufgerufen.
Um die Funktion der Basisklasse aufrufen zu können,
stellen Sie dem Funktionsnamen den Namen der Basisklasse, durch zwei
Doppelpunkte getrennt, voran.
class tBasis
{
public:
int TuWas(int a);
};
class tSpezialfall : public tBasis
{
public:
int TuWas(int a);
};
int tSpezialfall::TuWas(int a)
{
int altWert = tBasis::TuWas(a);
...
return altWert;
}
Konstruktoren und Zuweisung
Kaskadierende Konstruktoren
Bevor der Konstruktor einer abgeleiteten Klasse ausgeführt wird, wird immer
der Konstruktor der Basisklasse gestartet. Umgekehrt ist es beim Destruktor.
Hier wird der Destruktor der Basisklasse zuletzt aufgerufen.
Dieses Verhalten ist logisch, da abgeleitete Klassen auf den Eigenschaften
der Basisklassen aufbauen. Entsprechend muss das Basisobjekt
konstruiert sein, bevor der Konstruktor der abgeleiteten Klasse aufgerufen wird.
Entsprechendes gilt beim Destruktor. Der Destruktor der Basisklasse muss
zuletzt aufgerufen werden, damit das Basisobjekt nicht bereits zerstört ist,
wenn die abgeleitete Klasse noch versucht, die Erweiterungen freizugeben.
Parameterunterschiede der Konstruktoren
Wenn Sie Ihre Klasse von einer Basisklasse ableiten, die keinen Konstruktor
anbietet, der die gleichen Parameter hat wie Ihr Konstruktor, dann müssen
Sie den Konstruktor der Basisklasse explizit aufrufen. Das können Sie tun,
indem Sie einen Initialisierer verwenden.
Im folgenden Beispiel hat die Basisklasse lediglich einen Konstruktor, der
einen Integerwert erwartet. Dadurch gibt es keinen Standardkonstruktor. Die
abgeleitete Klasse hat aber einen Standardkonstruktor, über den sich der
Compiler beklagen würde, weil er kein Gegenstück in der Basisklasse findet.
Damit dies nicht geschieht, wird der Konstruktor der Basisklasse explizit
als Initialisierer aufgerufen. Bei Aufruf des Standardkonstruktors der
abgeleiteten Klasse wird der Basiskonstruktor mit dem Parameter 5 aufgerufen.
class tBasis
{
public:
tBasis(int i); // Kein Standardkonstruktor
};
class tSpezialfall : public tBasis
{
public:
tSpezialfall() : tBasis(5) // Basiskonstruktor aufrufen
{
...
}
};
Kopierkonstruktor
Besitzt die Basisklasse einen Kopierkonstruktor, wird dieser nicht automatisch
vererbt. Der Kopierkonstruktor der Basisklasse kann durch einen Initialisierer
aufgerufen werden, wie das folgende Beispiel zeigt:
tSpezialFall(const tSpezialFall& objekt) :tBasis(objekt)
{
...
}
Zuweisungsoperator
Auch der Zuweisungsoperator wird nicht automatisch weitervererbt.
Typischerweise ruft man die Zuweisung der Basisklasse direkt als
Funktion operator= auf, da nur so der Zuweisungsoperator der
Basisklasse benannt werden kann.
tSpezialFall& operator=(const tSpezialFall& objekt)
{
if (this!=&objekt)
{
// Vermeide Selbstkopie!
tBasis::operator=(objekt);
// eigene Elemene zuweisen
}
return *this;
}
Mehrfachvererbung
Syntax
In C++ ist es möglich, eine Klasse von mehreren Basisklassen abzuleiten.
Dabei wird nach dem Doppelpunkt jede einzelne Klasse mit ihrem
Ableitungsattribut, durch Komma getrennt, aufgezählt.
class Auto : public Motor, public Kutsche
{
...
};
Namenskonflikte
Das Auto erbt alle Eigenschaften und Funktionen einer Kutsche und die eines
Motors. Dabei könnte es zu Namenskonflikten kommen, da der Programmierer der
Klasse Motor nicht alle Namen mit dem Autor der Klasse
Kutsche abgesprochen haben wird.
Sollte der Begriff Lager bei beiden vorkommen, würde man
den Namen der Basisklasse, durch zwei Doppelpunkte getrennt, voranstellen,
um die Herkunft deutlich zu machen.
class Auto : public Motor, public Kutsche
{
...
a = Motor::Lager;
...
b = Kutsche::Lager;
...
};
Umstritten
In der Literatur gibt es einen gewissen Dissenz darüber, ob Mehrfachvererbungen
sinnvoll sind. Programme, die Mehrfachvererbung nutzen, können leicht
unübersichtlich und fehlerträchtig werden. (vgl. Kaiser, Richard:
C++ mit dem Borland C++ Builder.
Springer-Verlag, Berlin-Heidelberg, 2002. S. 811.)
Das ist der Grund, warum einige andere Programmiersprachen die
Mehrfachvererbung gar nicht erst zulassen.
Diese Seite basiert auf Inhalten aus dem Buch
Arnold Willemer: Einstieg in C++
Mit freundlicher Genehmigung und Unterstützung des Verlags galileo computing
| Informatik-Ecke Einstieg in C++ |
(C) Copyright 2005 Arnold Willemer
|