C++-Programmierung: Vererbung

Alte Seite
Diese Seite gehört zum alten Teil des Buches und wird nicht mehr gewartet. Die Inhalte sollen in das neue Buch einfließen: C++-Programmierung/ Inhaltsverzeichnis.
Eine aktuelle Seite zum gleichen Thema ist unter C++-Programmierung/ Objektorientierte Programmierung verfügbar.

Vererbung (inheritance) ist ein zentrales Konzept der OOP. Eine Klasse kann Elemente, also Variablen, Konstanten und Funktionen, von einer oder mehreren anderen erben - das heißt sie übernehmen oder eventuell verändern.

Grundlagen

Bearbeiten

Vererbung fügt einer bestimmten Klasse neue Funktionalität hinzu. So können Sie beispielsweise eine Klasse Person zum einen so ergänzen, dass sie Mitarbeiterdaten aufnimmt, und zum anderen so, dass Kundendaten gespeichert werden können.

class Person
{
  string name;

  //...
};

class Mitarbeiter : Person
{
  long sozialversicherungsNr;

  //...
};

class Kunde : Person
{
  Rechnung rechnungen[];

  //...
};

Die Klassen Mitarbeiter und Kunde sind von der Basisklasse (base class, superclass) Person abgeleitet (derived). Sie erben die Membervariable name.

Danach verfügen nicht nur alle drei Klassen über die Variable name, sondern können auch überall verwendet werden, wo Person als Datentyp angegeben ist (als Funktionsparameter, Variablenzuweisungen, usw.).

Zugriffskontrolle

Bearbeiten

Die Vererbungsart zeigt an, ob beim Vererben der Zugriff auf Elemente der Basisklasse eingeschränkt wird. Sie wird vor dem Namen der Basisklasse angegeben. Wie bei Memberdeklarationen gibt es die Schlüsselwörter public, protected und private (Standard-Vererbungsart). Die Deklarationen

class A { /* ... */ };
class B : public A    { /* ... */ };
class C : protected A { /* ... */ };
class D : private A   { /* ... */ }; // (oder (Standard-Vererbungsart): "class D : A { /* ... */ };")

bewirken folgendes:

Ist ein Element in A public protected private
... wird es in B public protected nicht übergeben
... wird es in C protected protected nicht übergeben
... wird es in D private private nicht übergeben

Beachten Sie, dass friend-Beziehungen nicht vererbt werden.

Mehrfachvererbung

Bearbeiten

Eine Klasse kann von mehreren Basisklassen erben:

class A
{
  int x;

  //...
};

class B 
{
  double y;

  //...
};

class C : public A, public B
{
  char z;

  //...
};

Die Klasse C vereint die Funktionalitäten von A und B und fügt noch etwas hinzu.

Elementfunktionen

Bearbeiten

Eine Methode der abgeleiteten Klasse kann die zugänglichen Membervariablen und -funktionen der Basisklasse ohne explizite Qualifizierung (oder gleichwertig: mit dem this-Zeiger) ansprechen. Sie müssen aber die Basisklasse in folgenden Situationen explizit angeben:

  • Die abgeleitete Klasse überschreibt eine Funktion der Basisklasse mit einer eigenen Definition. Im Beispiel
class Person {
    // ...
  public:
    void ausgeben() const; // gibt Personendaten auf stdout aus
};

class Mitarbeiter : public Person {
    // ...
  public:
    void ausgeben() const; // überschreibt Person::ausgeben
};

könnten Sie die neue Funktion ausgeben() so implementieren:

void Mitarbeiter::ausgeben() const {
  Person::ausgeben();                    // ruft Methode der Basisklasse auf
  cout << sozialversicherungsNr << endl; // zusätzliche Funktionalität
}
  • Bei Mehrfachvererbung kommt es zu Namenskonflikten, wenn „dieselbe“ Variable oder Funktion (d.h. gleicher Name und gleiche Parameterliste) in mehreren Basisklassen vorkommt.

Das Schlüsselwort const in den Beispielen deutet darauf hin, dass die Funktionen das Objekt nicht verändern.

Konstruktoren und Destruktoren

Bearbeiten

Jede Instanz der abgeleiteten Klasse - im Beispiel ein Mitarbeiter - enthält eine Instanz der Basisklasse Person als Teilobjekt. D.h. immer wenn eine solche Instanz erzeugt wird, muss (auch) ein Konstruktor der Basisklasse aufgerufen werden. Das sieht z.B. so aus:

Mitarbeiter::Mitarbeiter(const string& n, long sv)
: Person(n), sozialversicherungsNr(sv)
{ /* ... */ }

Ein Basisklassen-Teilobjekt wird also mit der gleichen Syntax initialisiert wie eine Membervariable. Nachdem alle diese Initialisierungen erfolgt sind, wird der Rumpf des Konstruktors ausgeführt.

Wenn die Instanz vernichtet wird, laufen diese Schritte in umgekehrter Reihenfolge ab: zuerst wird der Rumpf des Destruktors ausgeführt, dann evtl. Destruktoren der Membervariablen und schließlich die Destruktoren der Basisklassen.

dynamic_cast

Bearbeiten

Wie Sie aus dem Kapitel Typumwandlung wissen, ist es generell wenig sinnvoll, Zeiger verschiedener Typen ineinander umzuwandeln, weil diese Typen unverträgliche interne Darstellungen aufweisen. Handelt es sich dabei aber um zwei durch eine Vererbungsbeziehung verbundene Klassen, so gibt es in einer Richtung überhaupt keine Probleme:

Mitarbeiter meier;
Person *pperson = &meier;

Jetzt zeigt pperson auf das Person-Teilobjekt der Instanz meier. Hier muss nicht explizit umgewandelt werden. Der Compiler weiß, dass jeder Mitarbeiter eine Person darstellt und erledigt den Cast automatisch. Bei Referenzen funktioniert es genauso.

In der umgekehrten Richtung geht die Umwandlung natürlich nicht so ohne weiteres, denn nicht jede Person ist auch ein Mitarbeiter. Es wäre z. B. fatal, einen Zeiger des Typs Kunde* in Mitarbeiter* zu casten. Mit dem Operator dynamic_cast können Sie zur Laufzeit prüfen, ob der Cast im Einzelfall erlaubt ist oder nicht:

Person *pperson;
//pperson = ... zuweisen
Mitarbeiter *pmitarb = dynamic_cast<Mitarbeiter*> (pperson);

Wenn pperson tatsächlich auf einen Mitarbeiter zeigt, erhalten Sie einen gültigen Zeiger auf diese Instanz. Schlägt der Cast fehl, gibt dynamic_cast den Nullzeiger zurück. Bei Referenzen wird im Fehlerfall die Ausnahme bad_cast ausgeworfen.

Der dynamic_cast ist sehr nützlich, wird aber bei weitem nicht so oft gebraucht wie Sie jetzt vielleicht denken mögen. Es gibt nämlich ein Sprachkonstrukt, mit dem sich die Aufgabe, zur Laufzeit die passende Methode auszuwählen, häufig noch eleganter und effizienter lösen lässt. Mehr dazu im Kapitel Polymorphie.

Virtuelle Vererbung

Bearbeiten
 
Nicht virtuelle Vererbung

Eine abgeleitete Klasse kann wiederum als Basisklasse einer Vererbungsbeziehung dienen. Auf diese Weise fungiert eine allgemeine Klasse als Ausgangspunkt für einen ganzen „Vererbungsbaum“. Eine interessante Situation tritt ein, wenn die Baumgestalt verloren geht: dank der Mehrfachvererbung kann es passieren, dass zwei Klassen durch mehrere Vererbungswege verbunden sind. C++ überlässt dem Programmierer die Entscheidung, ob die zur mehrfach vorhandenen Basisklasse gehörenden Teilobjekte zu einem einzigen verschmolzen werden sollen oder nicht.

Wenn Sie getrennte Teilobjekte haben wollen, müssen Sie nichts weiter tun. Eine fiktive Klasse zum Arbeiten mit Dateien:

class Datei {
  unsigned int position;
  /* ... */
};

class DateiZumLesen     : public Datei { /* ... */ };
class DateiZumSchreiben : public Datei { /* ... */ };

class DateiZumLesenUndSchreiben: public DateiZumLesen, public DateiZumSchreiben { /* ... */ };
 
Virtuelle Vererbung

Jede Instanz der Klasse DateiZumLesenUndSchreiben hat zwei Teilobjekte der Basisklasse Datei. Das ist hier ein sinnvoller Ansatz, damit Lese- und Schreibzeiger an verschiedenen Positionen stehen können.

Sollen die Teilobjekte verschmolzen werden, kennzeichnen Sie die Vererbung mit dem Schlüsselwort virtual:

// Modifiziertes Beispiel "Person"

class Person {
  string name;
  // ...
};

class Mitarbeiter : public virtual Person { /* ... */ };
class Kunde       : public virtual Person { /* ... */ };

class MitarbeiterUndKunde : public Mitarbeiter, public Kunde { /* ... */ };

Jetzt besitzt eine Instanz der Klasse MitarbeiterUndKunde nur ein Teilobjekt der Basisklasse Person. Insbesondere ist die Membervariable name nur einmal vorhanden und kann konfliktfrei unter diesem Namen angesprochen werden. Beim Anlegen einer Instanz vom Typ MitarbeiterUndKunde wird jetzt allerdings der Konstruktor der Klasse Person nicht mehr indirekt durch die Konstruktoren der Klassen Mitarbeiter und Person aufgerufen, sondern muss explizit aus dem Konstruktor der Klasse MitarbeiterUndKunde aufgerufen werden.

Entwurf einer Klasse

Bearbeiten

In der Programmierpraxis stellt sich häufig folgendes Problem. Es soll eine Klasse B entworfen werden, welche die Funktionalität einer gegebenen Klasse A „benutzt“. Dies kann prinzipiell auf drei Arten realisiert werden:

  • B wird von A abgeleitet (Vererbung),
  • B hat eine Membervariable vom Typ A (Enthaltensein),
  • B hat eine Membervariable vom Typ „Zeiger auf A“ (Verweis).

Hier gibt es keinen Königsweg. Erfahrene Entwickler wie Bjarne Stroustrup und Grady Booch empfehlen, sich an die folgende Faustregel zu halten: Wenn jede Instanz der Klasse B eine Art von A ist, sollte Vererbung zum Zuge kommen. Beispiel: Jedes Auto ist ein Fahrzeug, also sollte die Klasse Auto von der allgemeinen Klasse Fahrzeug abgeleitet werden. Ein Auto ist aber kein Motor, sondern hat einen Motor. Deshalb sollte Auto eine Membervariable vom Typ Motor haben und nicht etwa von Motor abgeleitet werden. Meist fällt die Entscheidung schwerer als in diesem Beispiel, denn leider lassen sich nur die wenigsten Klassen so konkret veranschaulichen.