Vererbung

Willemers Informatik-Ecke


Diese Seite basiert auf Inhalten aus meinem Buch "Einstieg in C++".


Das Nachfolgebuch heißt C++. Der Einstieg und ist bei Wrox im Verlag Wiley-VCH erschienen.


C++ Hauptseite

»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.

Basisklasse

Eine Klasse kann als Basis zur Entwicklung einer neuen Klasse dienen, ohne dass ihr Code geändert werden muss. Dazu wird die neue Klasse definiert und dabei angegeben, dass sie eine abgeleitete Klasse der Basisklasse ist. Daraufhin gehören alle öffentlichen Elemente der Basisklasse auch zur neuen Klasse, ohne dass sie erneut deklariert werden müssen. Man sagt, die neue Klasse erbt die Eigenschaften der Basisklasse.

Wiederverwendung des Codes

Da die Basisklasse nicht geändert werden muss und der Code ohne weitere Anpassung in die abgeleitete Klasse eingebunden wird, ist diese Form der Wiederverwendung von Code weit gehend risikofrei.

Spezialisierung

Durch die Elemente, die in der abgeleiteten Klasse definiert werden, wird die abgeleitete Klasse zu einem besonderen Fall der Basisklasse. Sie besitzt alle Eigenschaften der Basisklasse. Sie können neue Elemente hinzufügen. Am folgenden Beispiel wird deutlich, warum das Hinzufügen von Eigenschaften eine Spezialisierung ist.

Beispiel

Aus Sicht eines Computerprogramms haben alle Personen Namen, Adressen und Telefonnummern. Geschäftspartner haben darüber hinaus eine Bankverbindung. Da die Geschäftspartner auch Personen sind, haben sie neben ihrer Bankverbindung auch Namen, Adressen und Telefonnummern. Einige Geschäftspartner können Kunden sein. Kunden haben zusätzlich zu den Eigenschaften eines Geschäftspartners noch eine Lieferanschrift. Lieferanten sind keine Kunden, aber auch Geschäftspartner. Sie haben noch offene Rechnungen. Selbst Mitarbeiter sind eigentlich Geschäftspartner, denn sie haben eine Bankverbindung. Darüber hinaus haben sie eine Krankenkasse. Außendienstler haben alle Eigenschaften eines Mitarbeiters und zusätzlich ihren Bezirk.

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.

»Ist ein«

Man spricht in diesem Fall von einer »Ist ein«-Beziehung. Der Kunde »ist ein« Geschäftspartner. Er hat alle Eigenschaften eines Geschäftspartners und fügt ihnen nur seine Besonderheiten, hier die Lieferanschrift, hinzu. Neben den Datenelementen vererben sich auch die Funktionen, wie die oben erwähnte Adressprüfung. Lediglich die Konstruktoren, die Destruktoren und die Zuweisungsoperatoren werden nicht vererbt.

Beispiel Firma

Von welcher Klasse eine Klasse erbt, wird in C++ dadurch ausgedrückt, dass bei der Klassendefinition nach dem Schlüsselwort class und dem Klassennamen ein Doppelpunkt gesetzt wird. Danach werden das Schlüsselwort public und der Name der Basisklasse genannt. Um die gemeinsamen Eigenschaften nicht für jede Personengruppe neu zu entwickeln, entwickelt man erst einmal eine Basisklasse Person. Jede der Personengruppen kann von Person abgeleitet werden und erbt alle Eigenschaften einer Person. Was den Klassen gemeinsam ist, wird in Person implementiert. Die Krankenkasse wird nur beim Mitarbeiter implementiert. Dagegen wird die Lieferanschrift nur beim Kunden implementiert. Sollen dann auch Lieferanten verwaltet werden, kann auf den Typ Person zurückgegriffen werden. Auch der Lieferant erbt alles von der Person und besitzt noch einige Eigenschaften mehr, wie beispielsweise eine Liste mit offenen Rechnungen.

[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;
};

Kompatibilität zur Basisklasse

Da öffentlich (public) abgeleitete Klassen die komplette Schnittstelle ihrer Basisklasse enthalten, sind sie zur Basisklasse zuweisungskompatibel. Ein Objekt vom Typ Mitarbeiter kann also einem Objekt vom Typ Person zugewiesen werden. Nach der Zuweisung sind aber die Informationen über die Krankenkasse und die Bankverbindung verloren. Das kopierte Objekt besitzt also die Erweiterungen der abgeleiteten Klasse nicht mehr. Einem Zeiger auf die Basisklasse kann die Adresse des Objekts einer abgeleiteten Klasse zugewiesen werden. In diesem Fall gehen dem Objekt keine Informationen verloren, weil es ja nicht verändert wird. Umgekehrt funktioniert das nicht. Einem Objekt einer abgeleiteten Klasse kann nicht das Objekt einer Basisklasse zugewiesen werden.

Person person;
Mitarbeiter mitarbeiter;

    person = mitarbeiter; // ok
    mitarbeiter = person; // das mag der Compiler nicht

Beispiel tDatum

Wenn Sie eine Klasse tDatum geschrieben haben, die für einen Kalender eingesetzt werden soll, dann benötigen Sie auch Feiertage. Diese unterscheiden sich aus Sicht eines Kalenders dadurch, dass jeder Feiertag einen Namen hat. Sie könnten natürlich jedem Datum einen Namen hinzufügen. Die meisten Tage sind aber keine Feiertage. Besser ist die Idee, eine Klasse tFeiertag von tDatum abzuleiten und ihr nur den Namen hinzuzufügen.

[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.

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.

[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:

[Ändern der Zugriffsart]

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!
}

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.

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.

[Privates Ableiten]

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.

[Funktionsaufruf der Basisklasse (kasfunc.cpp)]

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;
}

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.

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.

[Konstruktoraufruf (kaskonstrukt.cpp)]

class tBasis
{
public:
    tBasis(int i); // Kein Standardkonstruktor
};

class tSpezialfall : public tBasis
{
public:
    tSpezialfall() : tBasis(5) // Basiskonstruktor aufrufen
    {
        ...
    }
};

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.

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.


Informatik-Ecke Einstieg in C++ (C) Copyright 2005 Arnold Willemer