Programmieren in C++: Aufbau


Kapitel 3: Vererbung


Inhaltsverzeichnis

Dieses Buch ist unter einer Creative Commons-Lizenz lizensiert.


3.1 Allgemeines

Vorteil objektorientierter Programmiersprachen

Vererbung ermöglicht Entwicklern, Zusammenhänge zwischen Klassen vom Typ "ist eine Art von" auszudrücken. Bei zwei Klassen tier und biene wäre zum Beispiel die Klasse tier die Elternklasse von biene, weil eine Biene ein Tier ist.

In C++ kann eine Klasse beliebig viele Elternklassen haben. Gäbe es zum Beispiel eine weitere Klasse namens fliegend, wäre biene die Kindklasse von tier und fliegend, weil es sich bei der Biene um ein Tier handelt, das fliegen kann.

Da Klassen aus Methoden und Eigenschaften bestehen, werden diese durch die Vererbung automatisch in Kindklassen übernommen. Eine Methode fliegen() in der Klasse fliegend könnte also für ein Objekt vom Typ biene aufgerufen werden, weil die Methode durch die Vererbung auch in der Kindklasse vorhanden ist, so als wäre sie dort definiert.

Die Aufteilung von Methoden und Eigenschaften in unterschiedliche Klassen, die durch Vererbung in Beziehung gesetzt werden, macht es möglich, zielgenaue Funktionen zu entwickeln, die nur die Methoden und Eigenschaften verwenden, die benötigt werden. Anstatt zum Beispiel eine Funktion landen() zu entwickeln, die einen Parameter vom Typ biene erwartet und daher ausschließlich für Bienen verwendet werden kann, wäre es besser, wenn sie einen Parameter vom Typ fliegend akzeptieren würde. Denn Landen hat etwas mit fliegenden Objekten zu tun und nicht nur mit Bienen. Würde dann später zum Beispiel eine Klasse flugzeug hinzugefügt werden, so würde diese Klasse natürlich von fliegend abgeleitet werden und könnte sofort mit einer Funktion landen() verwendet werden.


3.2 Einfachvererbung

Eigenschaften und Methoden von einer Klasse erben

Unter der Einfachvererbung versteht man eine Klasse, die lediglich eine einzige Elternklasse hat.

#include <string> 

class vater 
{ 
  public: 
    vater(std::string name) 
      : Name(name) 
    { 
    } 

    void autofahren() 
    { 
    } 

  private: 
    std::string Name; 
}; 

Oben wird eine Klasse vater definiert. Diese Klasse besitzt eine private Eigenschaft Name vom Typ std::string. Die Eigenschaft kann über einen öffentlichen Konstruktor gesetzt werden.

Die Klasse besitzt außerdem eine öffentliche Methode autofahren(). Der Methodenrumpf ist leer. Diese Methode soll verdeutlichen, dass Objekte der Klasse vater eine Fähigkeit Autofahren besitzen sollen.

Der Übersicht halber befinden sich in der Klasse vollständige Methodendefinitionen. Selbstverständlich kann auch bei Vererbung die Methodendefintion von der Methodendeklaration getrennt werden und in einer anderen Datei liegen. Für gewöhnlich wird dies auch gemacht.

Nun wird eine Klasse definiert, die von der Klasse vater abgeleitet wird.

#include "vater.h" 

class kind : public vater 
{ 
}; 

Die Klasse kind erbt von der Klasse vater. Im Code geben Sie hierzu hinter dem Klassennamen einen Doppelpunkt an gefolgt von einem Zugriffsattribut, in diesem Fall public. Sinn und Zweck des Zugriffsattributs erfahren Sie später. Hinter dem Zugriffsattribut folgt der Name der Klasse, von der geerbt werden soll. Vergessen Sie außerdem nicht, die Klasse, von der Sie erben, bekannt zu machen, indem Sie wie oben beispielsweise die entsprechende Header-Datei einbinden.

So einfach wie es aussieht ist die Vererbung jedoch nicht. Es gibt mehrere Dinge, die Sie beachten müssen und um die wir die Klasse kind erweitern müssen, damit sie tatsächlich eingesetzt werden kann.

Sie erinnern sich sicher an die vier speziellen Methoden einer Klasse aus dem Kapitel 2, Spezielle Methoden: Konstruktor, Destruktor, Copy-Konstruktor und Zuweisungsoperator werden nie vererbt. Jede Klasse besitzt ihren eigenen Konstruktor, Destruktor, Copy-Konstruktor und Zuweisungsoperator. Es ist nicht möglich, die Implementation einer dieser Methoden an eine andere Klasse per Vererbung weiterzugeben.

Wenn Sie sich unsere beiden Beispiel-Klassen ansehen: Die Klasse vater definiert einen Konstruktor. Konstruktoren werden, wie Sie jetzt wissen, nicht vererbt. Das heißt, damit einem Objekt vom Typ kind ein eigener Name gegeben werden kann, muss ein eigener Konstruktor in der Klasse kind definiert werden, der einen entsprechenden Parameter erwartet. Die Klasse kind wird daher wie folgt erweitert.

#include "vater.h" 
#include <string> 

class kind : public vater 
{ 
  public: 
    kind(std::string name) 
      : Name(name) 
    { 
    } 
}; 

So wie oben könnte ein Versuch aussehen, einen entsprechenden Konstruktor zu definieren. Damit Sie gleich Bescheid wissen: Obiger Code ist falsch.

Im neu definierten Konstruktor wird auf eine Eigenschaft Name in der Initialisierungsliste zugegriffen, um sie mit dem als Parameter übergebenen Wert zu initialisieren. Es gibt hier zwei Probleme: Zum einen können Sie in Initialisierungslisten nur auf Eigenschaften zugreifen, die in der Klasse auch tatsächlich definiert und nicht nur geerbt sind. Zum anderen ist trotz Vererbung in der Klasse kind keine Eigenschaft Name verfügbar.

Dass in Initialisierungslisten nicht auf geerbte Eigenschaften zugegriffen werden kann, ist eigentlich kein allzugroßes Problem. Sie können entweder auf eine Initialisierung in der Initialisierungsliste verzichten und die Eigenschaft stattdessen direkt im Methodenrumpf des Konstruktors setzen, oder aber - das ist der bessere Weg - Sie initialisieren die Eigenschaft, indem Sie einen entsprechenden Konstruktor der Elternklasse aufrufen. Das sieht im Beispiel wie folgt aus.

#include "vater.h" 
#include <string> 

class kind : public vater 
{ 
  public: 
    kind(std::string name) 
      : vater(name) 
    { 
    } 
}; 

Die Eigenschaft Name kann in der Initialisierungsliste nicht gesetzt werden, da sie nicht in der Klasse kind definiert ist, sondern nur geerbt wurde. Die Elternklasse bietet jedoch einen Konstruktor an, der den Wert der Eigenschaft Name setzt. Indem in der Initialisierungsliste der entsprechende Konstruktor der Elternklasse aufgerufen wird, kann die Eigenschaft Name initialisiert werden.

Damit Sie verstehen, warum Sie den Umweg über die Elternklasse gehen müssen: Grundsätzlich sind immer die Klassen, die Eigenschaften definieren, für die Initialisierung verantwortlich. Das heißt, immer dann, wenn Sie Eigenschaften in einer Klasse nur erben, müssen Sie über Konstruktoraufrufe der Elternklasse in der Initialisierungsliste die Eigenschaften setzen. Immerhin sind in den Elternklassen die Eigenschaften definiert - also wissen diese Klassen auch am besten Bescheid, wie sie zu initialisieren sind.

Das zweite Problem, das bereits kurz erwähnt wurde, ist, dass in der Klasse kind keine Eigenschaft Name verfügbar ist - der Code ist also immer noch nicht richtig. Damit das Beispiel endlich funktioniert, muss das Zugriffsattribut für die Eigenschaft Name in der Klasse vater wie folgt geändert werden.

#include <string> 

class vater 
{ 
  public: 
    vater(std::string name) 
      : Name(name) 
    { 
    } 

    void autofahren() 
    { 
    } 

  protected: 
    std::string Name; 
}; 

Das Zugriffsattribut protected ist neu. Es spielt nur bei der Vererbung eine Rolle. Für eine Klasse selbst ist protected gleichbedeuted mit private. In der Klasse vater ist also die Eigenschaft Name immer noch geschützt und nicht von außen zugreifbar. Erst bei der Vererbung wird der Unterschied zwischen private und protected deutlich: Mit private deklarierte Merkmale sind in Kindklassen nicht direkt verfügbar; Merkmale, die mit protected deklariert sind, hingegen schon.

Nachdem die Eigenschaft Name in der Klasse vater nun mit protected deklariert ist, kann in der Kindklasse kind auch tatsächlich auf diese zugegriffen werden. Vorher, als die Eigenschaft mit private deklariert war, handelte es sich um eine private Eigenschaft der Klasse vater, die zwar auch vererbt wird, vor direkten Zugriffen in Kindklassen aber geschützt ist.

Wenn Sie eine Klasse erstellen, müssen Sie demnach überlegen, ob es eventuell Sinn machen könnte, wenn von dieser - später einmal von Ihnen oder von einem anderen Entwickler - eine Klasse abgeleitet wird. Sie müssen dann entscheiden, ob Sie bestimmte Eigenschaften nicht mit private, sondern mit protected deklarieren - nämlich die Eigenschaften, auf die Kindklassen direkten Zugriff haben sollen. Eigenschaften, die mit private geschützt sind, können ansonsten ausschließlich von Methoden verwendet werden, die ihrerseits public oder protected sind und daher von einer Kindklasse aufgerufen werden können.

Sie können in C++ nicht nur über Zugriffsattribute in einer Klasse festlegen, ob Merkmale vererbt werden oder nicht. Sie können über ein Zugriffsattribut, das bei der Definition einer Klassenbeziehung durch Vererbung angegeben wird, zusätzlich festlegen, wie Merkmale vererbt werden. Dieses Zugriffsattribut steht vor der Elternklasse. Im Beispiel bisher sind Merkmale mit dem Zugriffsattribut public vererbt worden. Dieses Zugriffsattribut wird üblicherweise für Vererbungen verwendet.

class kind : public vater

Eine Vererbung mit dem Zugriffsattribut public ist wie folgt definiert: Merkmale, die mit protected in der Elternklasse deklariert sind, sind auch in der Kindklasse protected. Merkmale, die in der Elternklasse mit public deklariert sind, sind auch in der Kindklasse public.

Im Folgenden sehen Sie eine grafische Übersicht zu den Regeln, nach denen Zugriffsattribute für geerbte Merkmale in Kindklassen geändert werden.

Tabelle 3.1. Vererbungsregeln
Zugriffsattribut bei Vererbung
private protected public
Zugriffsattribut in Elternklasse private - - -
protected private protected protected
public private protected public

Wenn Sie sich die Übersicht ansehen, lassen sich ein paar grundlegende Dinge erkennen: Mit private deklarierte Merkmale sind in einer Kindklasse nicht zugänglich. Das Zugriffsattribut public bei der Vererbung bedeutet, dass die Zugriffsattribute der geerbten Merkmale nicht geändert werden. Eine Vererbung mit den Zugriffsattributen private und protected ändert die Zugriffsattribute der geerbten Merkmale - und zwar so, dass die Zugriffsmöglichkeiten eingeschränkt werden. Es ist nie möglich, durch Vererbung die Zugriffsmöglichkeiten auf Merkmale einer Klasse zu erweitern.

Beachten Sie, dass ausschließlich die Vererbung mit public eine echte Vererbung ist. Wenn Sie private oder protected bei der Vererbung verwenden, ändern sich die Zugriffsrechte der geerbten Merkmale. Die Kindklasse unterstützt dann nicht mehr die gleiche Schnittstelle wie die Elternklasse. Die Vererbung mit private oder protected entspricht in der Terminologie der Objektorientierung der Komposition, nicht der Vererbung. Wenn Sie das verwirrt, merken Sie sich, immer nur public bei der Vererbung zu verwenden. Die Vererbung mit private oder protected macht nur dann Sinn, wenn Sie den Unterschied zwischen den Konzepten Vererbung und Komposition kennen, wie sie in objektorientierten Modellen verwendet werden.

Kommen wir zurück zum Beispiel. Nachdem die Klassen angepasst wurden und die Vererbung nun funktioniert, könnten Sie beispielsweise folgendes Programm schreiben.

#include "kind.h" 

int main() 
{ 
  kind k("Boris"); 
  k.autofahren(); 
} 

Das Entscheidende ist: Sie können für das Objekt vom Typ kind die Methode autofahren() aufrufen, obwohl die Klassendefinition keine derartige Methode enthält. Durch den Vererbungsmechanismus steht diese Methode in der Kindklasse zur Verfügung und kann daher so verwendet werden, als wäre sie in der Klasse definiert.


3.3 Mehrfachvererbung

Eigenschaften und Methoden von mehreren Klassen erben

Die Mehrfachvererbung unterscheidet sich nicht allzusehr von der Einfachvererbung. Es tritt jedoch ein besonderes Problem auf.

#include <string> 

class mutter 
{ 
  public: 
    mutter(std::string name) 
      : Name(name) 
    { 
    } 

    void kochen() 
    { 
    } 

  protected: 
    std::string Name; 
}; 

Zuerst wird eine neue Klasse mutter definiert. Sie enthält einen Konstruktor und eine öffentliche Methode kochen(). Diese Methode soll verdeutlichen, dass Objekte vom Typ mutter die Fähigkeit Kochen besitzen.

Die bereits zu Beginn des Kapitels definierten Klassen vater und kind werden nun zusammen mit der neuen Klasse mutter verwendet, um die Mehrfachvererbung einzusetzen. Die Klasse kind soll hierbei Merkmale der Klassen vater und mutter erben.

Die Definition der Klasse kind sieht nun wie folgt aus.

#include "vater.h" 
#include "mutter.h" 
#include <string> 

class kind : public vater, public mutter 
{ 
  public: 
    kind(std::string name) 
      : vater(name) 
    { 
    } 
}; 

Die Mehrfachvererbung ist sehr schnell erstellt: Sie geben einfach hinter dem Doppelpunkt mehrere Klassen an, von denen geerbt werden soll, und trennen diese Klassen durch Komma. Beachten Sie, dass Sie nicht nur den jeweiligen Klassennamen angeben müssen, sondern für jeden Klassennamen zusätzlich ein Zugriffsattribut. Ein Zugriffsattribut bezieht sich also immer nur auf die Vererbung von einer Elternklasse, nicht auf alle Elternklassen zusammen.

Überlegen Sie sich, welche Merkmale der Elternklassen geerbt werden. Was die Methoden betrifft, so wird von der Klasse vater autofahren() geerbt und von der Klasse mutter kochen(). Objekte vom Typ kind können demnach Autofahren und Kochen.

Was die Eigenschaften betrifft, so erbt kind von der Klasse vater Name und von der Klasse mutter ebenfalls eine Eigenschaft Name. Inwiefern kann dies zu Problemen führen?

Das Erben gleichnamiger Merkmale aus Elternklassen kann unschöne Folgen haben. Jede Programmiersprache legt selbst fest, nach welchen Regeln in einem derartigen Fall verfahren wird. In Java wurde die Mehrfachvererbung gar nicht erst in die Sprache aufgenommen, um sie nicht zu verkomplizieren. Die Regel in C++, was bei Vererbung gleichnamiger Merkmale geschehen soll, lautet: Der Programmierer hat für Eindeutigkeit zu sorgen. Was das genau heißt, soll nachfolgend erläutert werden.

Die Klasse kind wird nun so geändert, dass die Eigenschaft Name nicht mehr in der Initialisierungsliste gesetzt wird, sondern direkt im Methodenrumpf des Konstruktors.

#include "vater.h" 
#include "mutter.h" 
#include <string> 

class kind : public vater, public mutter 
{ 
  public: 
    kind(std::string name) 
    { 
      Name = name; 
    } 
}; 

Jetzt haben wir genau das Problem, dass auf eine Eigenschaft Name zugegriffen werden soll, die es zweimal in der Klasse kind gibt - einmal von vater und einmal von mutter geerbt. Es ist aus dem Code heraus nicht ersichtlich, auf welche dieser beiden geerbten Eigenschaften nun eigentlich zugegriffen werden soll. Gemäß der Regel in C++, dass der Programmierer für Eindeutigkeit zu sorgen hat, muss der Code wie folgt geändert werden.

#include "vater.h" 
#include "mutter.h" 
#include <string> 

class kind : public vater, public mutter 
{ 
  public: 
    kind(std::string name) 
    { 
      vater::Name = name; 
    } 
}; 

Nun ist klar, dass Sie auf die Eigenschaft Name zugreifen wollen, die in der Elternklasse vater definiert und von dieser geerbt wurde. Sie stellen hierzu einfach den Klassennamen inklusive dem Zugriffsoperator für Klassen der Eigenschaft voran, um ganz klar anzugeben, mit welcher Eigenschaft Sie arbeiten möchten. Statt die Eigenschaft Name aus der Klasse vater zu verwenden, könnten Sie natürlich auch genausogut an dieser Stelle die Eigenschaft aus der Klasse mutter verwenden.

Ein Problem bei gleichnamigen Merkmalen ist, dass Sie eventuell nur eines benötigen - so wie in unserem Beispiel. Es reicht völlig, wenn die Klasse kind eine Eigenschaft Name hat - zwei werden gar nicht gebraucht. In diesem Fall wird also Speicherplatz verschwendet, wenn natürlich auch nur minimal.

Das andere Problem betrifft die Verwendung von gleichnamigen Merkmalen. Sie müssen jedesmal die Klassen mit Zugriffsoperator angeben, um eindeutig festlegen zu können, welches Merkmal Sie eigentlich genau meinen. Sowas macht den Code etwas schwieriger zu schreiben und ist unnötige Arbeit. Es gibt jedoch Situationen, in denen Sie um diese Schreibweise nicht herumkommen, wenn Sie beispielsweise keinen Einfluss auf den Code der Elternklassen haben und es so zwangsläufig mit gleichnamigen Eigenschaften zu tun bekommen.


3.4 Virtuelle Vererbung

Merkmale virtuell vererben

In einer Klassenhierarchie, deren Designer Sie sind und über die Sie die Kontrolle haben, können Sie das Problem, dass auf einmal das gleiche Merkmal zweimal in einer Kindklasse vorhanden ist, über virtuelle Vererbung lösen. Dazu müssen Sie aber eine neue Elternklasse für vater und mutter erstellen: Die virtuelle Vererbung verhindert nämlich lediglich, dass ein Merkmal, das über verschiedene Stränge von der gleichen Elternklasse geerbt wird, mehrmals in einer Kindklasse vorhanden ist.

Im Folgenden wird eine neue Elternklasse person erstellt, in der die Eigenschaft Name definiert wird.

#include <string> 

class person 
{ 
  public: 
    person(std::string name) 
      : Name(name) 
    { 
    } 

  protected: 
    std::string Name; 
}; 

Die beiden Klassen vater und mutter werden nun von person abgeleitet. Dabei wird das Schlüsselwort virtual verwendet, um die virtuelle Vererbung einzusetzen. Im Folgenden wird die Klasse vater definiert - die Klasse mutter sieht entsprechend aus.

#include "person.h" 
#include <string> 

class vater : public virtual person 
{ 
  public: 
    vater(std::string name) 
      : person(name) 
    { 
    } 

    void autofahren() 
    { 
    } 
}; 

Wenn nun die Klasse kind von vater und mutter abgeleitet wird, erhält sie aufgrund der virtuellen Vererbung nur eine einzige Eigenschaft Name. Das Schlüsselwort virtual wird dabei nicht bei der Definition der Klasse kind angegeben.

#include "vater.h" 
#include "mutter.h" 
#include <string> 

class kind : public vater, public mutter 
{ 
  public: 
    kind(std::string name) 
      : person(name) 
    { 
    } 
}; 

Um nun die eine Eigenschaft Name initialisieren zu können, die virtuell vererbt wurde, muss direkt auf einen entsprechenden Konstruktor in der Klasse person zugegriffen werden. In diesem Fall darf also nicht ein entsprechender Konstruktor der Klasse vater oder mutter aufgerufen werden.


3.5 Abstrakte Klassen

Klassen-Schnittstellen vorgeben

Abstrakte Klassen sind Klassen, die mindestens eine Methode deklarieren, aber nicht definieren. Das sieht etwas merkwürdig aus, weil zwar ein Methodenkopf in der Klasse angegeben ist, nur nirgendwo der Methodenrumpf steht. Es ist also festgelegt, wie eine Methode heißt, welche Parameter sie erwartet und welchen Rückgabewert sie liefert - es steht aber nirgendwo, was eigentlich genau passieren soll, wenn diese Methode aufgerufen wird. Derartige Methoden werden in C++ rein virtuelle Methoden genannt.

Weil nicht festgelegt ist, was im Falle eines Methodenaufrufs passieren soll, können und dürfen rein virtuelle Methoden nicht aufgerufen werden. Um diesen Aufruf zu verhindern ist es nicht möglich, Objekte vom Typ einer abstrakten Klasse zu erstellen. Da stellt sich die Frage, welchen Sinn abstrakte Klassen haben, wenn sie nicht instanziiert werden können.

Abstrakte Klassen werden immer im Zusammenhang mit Vererbung verwendet: Eine Klasse, die von einer abstrakten Klasse erbt, muss ihrerseits alle rein virtuellen Methoden der Elternklasse implementieren, wenn sie keine abstrakte Klasse sein will. Ist auch nur eine einzige rein virtuelle Methode aus der Elternklasse nicht definiert, so handelt es sich auch bei der Kindklasse um eine abstrakte Klasse.

Das heißt, abstrakte Klassen zwingen Kindklassen eine bestimmte Schnittstelle auf. Eine Klasse ist durch eine abstrakte Elternklasse gezwungen, bestimmte Methoden zu implementieren, wenn sie nicht selber eine abstrakte Klasse sein will. Weniger technisch bedeutet das, dass sich eine Kindklasse einer abstrakten Elternklasse wie eine Elternklasse verhalten muss, also die gleichen Fähigkeiten wie in der Elternklasse vorgegeben besitzen muss.

Genug der Theorie - im Folgenden sehen Sie ein Beispiel.

class fahrzeug 
{ 
  public: 
    virtual void bewegen() = 0; 
}; 

Die Klasse fahrzeug besitzt ein einziges Merkmal, nämlich eine öffentliche Methode bewegen(). Diese Methode ist jedoch rein virtuell. Im Code wird dies dadurch gekennzeichnet, dass vor die Methodendeklaration das Schlüsselwort virtual und hinter die Deklaration = 0 gesetzt wird. Weil die Klasse fahrzeug nun eine rein virtuelle Methode enthält, ist sie abstrakt. Eine Methodendefinition für bewegen() gibt es nicht - der fehlende Methodenrumpf ist ja genau das Kennzeichen rein virtueller Methoden.

Es ist nicht möglich, Objekte vom Typ der Klasse fahrzeug zu erstellen. Daher wird nun eine neue Klasse definiert, die von der Klasse fahrzeug abgeleitet wird und die rein virtuelle Methode bewegen() definiert. Dies geschieht der Übersicht halber auch direkt innerhalb der Klassendefinition, obwohl dies wie gewohnt in einer anderen Datei geschehen kann und soll.

#include "fahrzeug.h" 
#include <iostream> 

class automobil : public fahrzeug 
{ 
  public: 
    void bewegen() 
    { 
      std::cout << "Brummbrumm" << std::endl; 
    } 
}; 

Die Klasse automobil erbt von der Elternklasse fahrzeug die rein virtuelle Methode bewegen() und implementiert sie nun. Somit handelt es sich bei automobil nicht um eine abstrakte Klasse - schließlich ist keine rein virtuelle Methode in dieser Klasse vorhanden.

Die Implementation der Methode bewegen() sieht so aus, dass das typische Geräusch eines Autos, nämlich Brummbrumm, einfach auf den Bildschirm ausgegeben wird.

Im Folgenden wird eine zweite Klasse definiert, die ebenfalls von fahrzeug abgeleitet wird.

#include "fahrzeug.h" 
#include <iostream> 

class roller : public fahrzeug 
{ 
  public: 
    void bewegen() 
    { 
      std::cout << "Rollroll" << std::endl; 
    } 
}; 

Die zweite Klasse heißt roller und implementiert auch die rein virtuelle Methode bewegen(). Die Methode gibt auf den Bildschirm Rollroll aus, also eine auf einem Roller typische Tätigkeit.

Abschließend wird ein kleines Programm geschrieben, das sich der oben definierten Klassen bedient.

#include "automobil.h" 
#include "roller.h" 

void start(fahrzeug &Fahrzeug) 
{ 
  Fahrzeug.bewegen(); 
} 

int main() 
{ 
  automobil Auto; 
  roller Roller; 

  start(Auto); 
  start(Roller); 
} 

Das Programm besteht wie immer aus der Funktion main() und zusätzlich aus einer Funktion start(). start() erwartet einen Parameter vom Typ fahrzeug. Beachten Sie, dass der Parameter als Referenz angegeben ist - das ist wichtig, weil sonst der Code nicht kompiliert. Bei fahrzeug handelt es sich nämlich, wie Sie wissen, um eine abstrakte Klasse. Werden Parameter nicht als Referenz, sondern als Kopie an eine Funktion übergeben, wird durch den Copy-Konstruktor ein lokales Objekt erstellt. Von der Klasse fahrzeug darf und kann aber kein Objekt erstellt werden. Indem Sie den Parameter als Referenz übergeben, wird der Copy-Konstruktor nicht aufgerufen und auch kein lokales neues Objekt erstellt. Es wird vielmehr mit dem echten Objekt gearbeitet, das der Funktion als Parameter angegeben wurde - genau das ist ja der Sinn von Referenzen.

Schön und gut - es wird zwar nun kein lokales Objekt vom Typ fahrzeug in der Funktion start() erstellt, nur müssen Sie ja beim Funktionsaufruf ein derartiges Objekt übergeben? Der Trick ist: Sie können anstatt einem Objekt vom Typ fahrzeug auch ein Objekt vom Typ einer Kindklasse übergeben. Dies funktioniert nicht nur mit abstrakten Klassen, sondern in jeder beliebigen Klassenhierarchie, in der die Vererbung zwischen Klassen mit public erfolgt. Eine derartige Kindklasse ist eine Spezialisierung einer Elternklasse, besitzt also Merkmale der Elternklasse und zusätzlich eigene in der Klasse selbst definierte Merkmale. Weil aber Merkmale der Elternklasse vorhanden sind, verhält sich eine Kindklasse wie eine Elternklasse - und kann überall dort verwendet werden, wo eine Elternklasse gefordert ist.

In der Funktion start() wird die Methode bewegen() für das als Parameter übergebene Objekt aufgerufen. Dies ist insofern kein Problem, als dass die Klasse fahrzeug ja eine derartige Methode enthält.

In der Funktion main() werden zwei Objekte vom Typ der Klasse automobil und roller erstellt. Diese Objekte werden als Parameter der Funktion start() übergeben, was wie Sie nun wissen funktioniert, weil es sich hierbei um Objekte vom Typ einer Kindklasse von fahrzeug handelt. Nachdem in start() die Methode bewegen() für die Objekte aufgerufen wird, wird einmal für das Auto Brummbrumm und einmal für den Roller Rollroll auf den Bildschirm ausgegeben.

Was sind nun die Vorteile von abstrakten Klassen? Ein Vorteil steckt bereits in der Bezeichnung abstrakte Klasse: Es handelt sich hierbei um eine Art Oberklasse, die in gewisser Weise Schnittstellen für Kindklassen bündelt und vorgibt. Weil im Programm selber jedoch nur konkrete Kindklassen verwendet werden sollen und die Oberklasse für eine sinnvolle Verwendung im Programm zu abstrakt ist, können Sie die Klasse durch rein virtuelle Methoden auch in C++ tatsächlich abstrakt machen. Im obigen Beispiel macht es keinen Sinn, Fahrzeuge zu verwenden: Ein Fahrzeug ist entweder ein Auto oder ein Roller. Fahrzeug ist zu allgemein, um mit dieser Klasse arbeiten zu können. Wohlgemerkt: Dies ist vielleicht in diesem Beispiel der Fall, kann jedoch natürlich in anderen Projekten völlig anders aussehen.

Bei Oberklassen wie fahrzeug macht es auch keinen Sinn, eine Methode bewegen() zu implementieren. Ein Auto macht Brummbrumm, ein Roller Rollroll, was aber macht ein Fahrzeug? Die Klasse ist so allgemein, dass der Entwickler nicht wissen kann, wie eine konkrete Implementierung der rein virtuellen Methode aussehen kann. Auch dies ist ein häufiger Grund, warum rein virtuelle Methoden erstellt werden: Es ist bekannt, dass diese Methode später benötigt wird. Es ist jedoch noch nicht bekannt, wie diese Methode genau aussehen soll. In unserem Beispiel ist klar, dass sich ein Fahrzeug bewegen können muss - sonst wäre es kein Fahrzeug. Wie die genaue Bewegungsart jedoch aussieht, hängt von den abgeleiteten und konkreten Kindklassen ab. Die Fahrzeugklasse kann die genaue Definition der Methode bewegen() nicht kennen.


3.6 Aufgaben

Übung macht den Meister

Sie können die Lösungen zu allen Aufgaben in diesem Buch als ZIP-Datei erwerben.

  1. Definieren Sie eine Klasse angestellter mit den geschützten Eigenschaften Name und Gehalt und einem speziellen Konstruktor, über den die geschützten Eigenschaften bei einer Objektdefinition initialisiert werden können. Definieren Sie außerdem zwei öffentliche Methoden: arbeitet() besitzt keinen Rückgabewert, erwartet keinen Parameter und gibt bei Aufruf Namen und Gehalt des Angestellten auf die Standardausgabe aus. berichtet() besitzt keinen Rückgabewert, erwartet keinen Parameter und ist rein virtuell. Kompilieren Sie den Code, um ihn auf Fehlerfreiheit zu überprüfen.

    Die Aufgabe sollte kein Problem sein. Der spezielle Konstruktor muss zwei Parameter vom Typ std::string und int erhalten und mit diesen Parametern die Eigenschaften der Klasse in einer Initialisierungsliste setzen. Die Vorgehensweise zur Definition einer abstrakten Methode berichtet() können Sie in diesem Kapitel abschauen.

  2. Definieren Sie eine Klasse vorstandsmitglied und leiten Sie sie von der Klasse angestellter aus Aufgabe 1 ab. Definieren Sie einen speziellen Konstruktor, über den einem Vorstandsmitglied bei der Objektdefinition ein Name zugewiesen werden kann. Das Gehalt liegt für jedes Vorstandsmitglied bei 10000 Euro pro Monat. Die Klasse vorstandsmitglied darf nicht abstrakt sein. Kompilieren Sie den Code, um ihn auf Fehlerfreiheit zu überprüfen.

    In der Klasse angestellter müssen die privaten Merkmale mit dem Schlüsselwort protected definiert sein, damit sie in der Klasse vorstandsmitglied zugänglich sind. Der Konstruktor von vorstandsmitglied erwartet lediglich einen Parameter vom Typ std::string. Dieser und der Zahlenwert 10000 werden an den Konstruktor von angestellter in der Initialisierungsliste von vorstandsmitglied übergeben, um auf diese Weise die geerbten Eigenschaften zu setzen. Implementieren Sie die geerbte rein virtuelle Methode berichtet(), indem Sie zum Beispiel eine Meldung auf die Standardausgabe ausgeben, dass das Vorstandsmitglied an den Vorstandsvorsitzenden berichtet.

  3. Definieren Sie eine abstrakte Klasse abteilung mit den geschützten Eigenschaften Abteilungsname und Meeting und einer öffentlichen Methode berichtet() ohne Rückgabewert und ohne Parameter. In der Eigenschaft Meeting soll später ein Wochentag eingetragen werden, an dem sich die Abteilung jeweils trifft. Kompilieren Sie den Code, um ihn auf Fehlerfreiheit zu überprüfen.

    Diese Aufgabe wird analog zu Aufgabe 1 gelöst. Der Hinweis, dass in der Eigenschaft Meeting später ein Wochentag gespeichert werden soll, soll Ihnen lediglich helfen, einen geeigneten Datentyp zu finden.

  4. Definieren Sie eine Klasse marketing und leiten Sie sie von der Klasse abteilung aus Aufgabe 3 ab. Marketing-Abteilungen treffen sich immer Dienstags. Die Klasse marketing darf nicht abstrakt sein.

    Damit marketing nicht abstrakt ist, müssen Sie lediglich die geerbte Methode berichtet() von der Elternklasse abteilung implementieren. Geben Sie zum Beispiel die Meldung auf die Standardausgabe aus, dass die Abteilung an den Vorstand berichtet. Ansonsten wird diese Aufgabe analog zu Aufgabe 2 gelöst.

  5. Definieren Sie eine Klasse leiter_kommunikation und leiten Sie sie von den Klassen vorstandsmitglied aus Aufgabe 2 und marketing aus Aufgabe 4 ab. Definieren Sie außerdem einen speziellen Konstruktor, um bei einer Objektdefinition einen Namen angeben zu können. Erstellen Sie ein Objekt vom Typ leiter_kommunikation und übergeben Sie dieses Objekt an eine freistehende Funktion berichtet(), die als einzigen Parameter ein Objekt vom Typ leiter_kommunikation erwartet. Rufen Sie innerhalb der Funktion für den Parameter die Methode berichtet() auf, um auf die Standardausgabe auszugeben, an wen die Marketing-Abteilung im Unternehmen berichten muss.

    Für die Klasse leiter_kommunikation müssen Sie Mehrfachvererbung verwenden. Das führt zu dem Problem, dass Sie aus den Elternklassen zwei gleichnamige Methoden berichtet() erben. Um in der globalen Funktion eine Meldung auszugeben, an wen die Marketing-Abteilung im Unternehmen berichtet, müssen Sie diejenige Methode berichtet() aufrufen, die die Klasse leiter_kommunikation von der Klasse marketing geerbt hat. Sie müssen hier den Namen der Elternklasse inklusive dem Zugriffsoperator für Klassen angeben, damit eindeutig festgelegt ist, auf welche der geerbten gleichnamigen Methoden Sie zugreifen wollen.