Einfürhung in C++ Kapitel 9 — Mehrfachvererbung

Mehrfachvererbung ist die Möglichkeit, Daten und Methoden aus mehr als nur einer Basisklasse in eine abgeleitete Klasse zu übernehmen. Mehrfachvererbung und einige andere neuere Erweiterungen der Sprache werden wir in diesem Kapitel besprechen, dazu einige der möglichen zukünftigen Entwicklungen.

Nachdem Du mit dieser Einführung fertig bist, solltest Du über genug Erfahrung mit C++ verfügen, um neue Konstrukte selbst zu studieren, wenn die Compiler-Autorinnen sie implementieren.

Mehrfachvererbung

Eine bedeutende Erweiterung von C++ war die Möglichkeit, beim Erstellen einer neuen Klasse Methoden und Variablen von zwei oder mehr Elternklassen zu übernehmen. Das ist Mehrfachvererbung, die von vielen als wesentlich für eine objektorientierte Programmiersprache angesehen wird. Es war aber nicht leicht, ein gutes Beispiel für Mehrfachvererbung zu finden. Das Ergebnis kann auch nicht gut genannt werden: es macht nichts Sinnvolles. Es illustriert aber die Verwendung von Mehrfachvererbung in C++, und das soll einstweilen unser primäres Anliegen sein.

Das größte Problem der Mehrfachvererbung stellt das Übernehmen von Variablen oder Methoden mit demselben Namen aus verschiedenen Elternkalassen dar. Welche der gleichnamigen Variablen oder Methoden soll verwendet werden? Das wollen wir in den nächsten Beispielprogrammen zeigen.

Simple Mehrfachvererbung

Beispielprogramm MEHRVER1.CPP

Schon der ersten Blick auf MEHRVER1.CPP wird Dir enthüllen, daß wir in den Zeilen 4 und 22 zwei einfache Klassen definieren, FahrenderLastwagen und Fahrerin.

Um das Programm möglichst einfach zu halten, haben wir alle Methoden inline definiert. Somit findet sich der Code für die Methoden schnell. Alle Variablen in beiden Klassen haben wir als protected deklariert, sie sind also in allen abgeleiteten Klassen verfügbar. Den Code selbst haben wir möglichst einfach gehalten, um uns auf das Studium der Schnittstelle konzentrieren zu können anstatt uns mit komplexen und -izierten Methoden herumschlagen zu müssen. [Lieber schon schlagen wir uns mit komplexierten Sätzen (herum)!] Wie wir schon festgestellt haben, wird sich das Kapitel 12 dann mit nicht-trivialen Methoden befassen.

In Zeile 32 beginnen wir die Definition einer weiteren Klasse mit dem Namen GefahrenerLaster, die alle Daten und alle Methoden der beiden zuvor definierten Klassen übernimmt. In den letzten beiden Kapiteln haben wir uns damit befaßt, wie wir eine Klasse beerben. Das ableiten einer Klasse aus zwei oder mehreren Klassen geschieht im Grunde genauso, nur daß wir eine Liste von Elternklassen, die durch Beistriche getrennt sind, verwenden, wie etwa in Zeile 32. Du hast sicherlich bemerkt, daß wir vor den Klassennamen das Schlüsselwort public verwenden, um die Methoden der Elternklasse in der Subklasse frei verwenden zu können. In diesem Fall haben wir keine neuen Variablen definiert, führen mit der Subklasse aber zwei neue Methoden ein, in den Zeilen 35 bis 42.

Einführung in C++ bringt Dich in Partnerschaft mit Amazon.de von Null auf Programmieren in ein paar Klicks.

Zum Beispiel kann simpel sein:

C++. Der Einstieg
Ein bewährtes Buch auf halben Wegs aktuellem Stand: Arnold Willemers "C++. Der Einstieg" eignet sich tatsächlich zum Einstieg ins Programmieren mit C++ und erklärt klar strukturiert mit, ganz wichtig, allerhand Beispielen alles Wichtige.
›› Mehr C++-Bücher

Wir definieren ein Objekt mit dem Namen MercedesBMW, was bedeuten soll, daß jemand mit dem Namen Mercedes einen (hypothetischen) BMW Lastwagen fährt. Das Objekt MercedesBMW besteht aus vier Variablen, drei von der Klasse FahrenderLastwagen und die eine der Klasse Fahrerin. Alle vier Variablen können wir in jeder der Methoden der Klasse GefahrenerLaster in derselben Weise manipulieren wie in einer Klasse, die nur von einer Basisklasse abgeleitet ist. Einige Beispiele findest Du in den Zeilen 50 bis 59 des Hauptprogrammes und es sollte Dir ein Leichtes sein, weitere Bildschirmausgaben hinzuzufügen.

Alle Regeln, die wir bei der einfachen Vererbung für private und protected Variablen und public und private Methodenübernahme aufgestellt haben, gelten auch für die Mehrfachvererbung.

Doppelte Methodennamen bei Mehrfachvererbung in C++

Wie Dir nicht entgangen sein wird, haben beide Elternklassen eine Methode mit dem Namen Initialisiere() und wir übernehmen beide in die Subklasse ohne Probleme. Sobald wir aber versuchen sollten, an eine der beiden eine Nachricht zu senden, haben wir ein Problem, da das System nicht weiß, welche der beiden Methoden wir meinen. Diese Schwierigkeit werden wir uns im nächsten Beispielprogramm ansehen, und auch eine Lösung haben wir natürlich schon im Talon.

Wir haben im Hauptprogramm keine Objekte der Elternklassen definiert. Da es sich bei den beiden Elternklassen aber auch nur um ganz normale Klassen handelt, sollte es offensichtlich sein, daß sie auch als solche verwendet werden können. Du kannst das ruhig machen, um Deine Kenntnisse auf diesem Gebiet zu überprüfen.

Kompiliere dieses Programm und führe es aus, wenn Du es komplett verstanden hast.

Mehr über doppelte Methodennamen

Beispielprogramm: MEHRVER2.CPP

Das zweite Beispielprogramm dieses Kapitels, MEHRVER2.CPP, illustriert die Verwendung von Klassen, die Methoden mit demselben Namen aus verschiedenen Elternklassen übernehmen.

Wir haben jeder der drei Klassen eine Methode mit dem Namen KostenProGanzemTag() hinzugefügt. Damit wollen wir zeigen, wie derselbe Methodenname in allen Klassen verwendet werden kann. Die Klassendefinitionen stellen kein Problem dar, die Methoden werden einfach wie gehabt definiert. Ein Problem stellt sich uns jedoch, wenn wir eine der Methoden verwenden wollen, haben sie doch alle denselben Namen, dieselbe Anzahl an und dieselben Typen von Parametern und auch denselben Rückgabetyp. Dadurch kann keine Überladungsregeln entscheiden, welche der Methoden verwendet werden soll.

Wie wir eindeutig festlegen, welche Methode wir meinen, zeigen die Methodenaufrufe in den Zeilen 63, 67 und 71 des Hauptprogrammes. Wir geben den Klassennamen vor dem Methodennamen an, durch einen zweifachen Doppelpunkt von diesem getrennt, also so wie in der Implementation der Methode. Das wird auch Qualifikation des Methodennamen genannt. In Zeile 71 wäre diese Vorgangsweise nicht notwendig, da es sich um die Methode der abgeleiteten Klasse handelt, die Vorrang vor den Elternklassen hat. Du könntest durchaus auch alle Methodennamen qualifizieren, wenn die Namen aber eindeutig sind, übernimmt der Compiler diese Arbeit für Dich, was den Code einfacher zu schreiben und lesen macht.

Kompiliere dieses Programm, führe es aus, und sieh Dir das Ergebnis an.

Wir vergehen uns an einigen Prinzipien des Objektorientierten Programmierens

Einer der Grundpfeiler objektorientierten Programmierens ist, daß man bei der Vererbung einer Basisklasse in eine abgeleitete Klasse implizit annehmen kann, die abgeleitete Klasse "ist eine" Art der Basisklasse, mit Betonung auf "ist eine". Es wäre nicht besonders sinnvoll (vielleicht aber originell und interessant), die Charakteristika eines Eisbären in eine Klasse für Hochhäuser zu übernehmen, da wir nicht sagen können "Ein Hochhaus ist ein Eisbär." Da aber "Ein Hochhaus ist ein Gebäude" sehr wohl seine Berechtigung hat, wäre es sinnvoll, die Charakteristika eines Gebäudes in einer Hochhausklasse zu übernehmen und diesen die für Hochhäuser kennzeichnenden Eigenschaften hinzuzufügen.

Unser Beispiel paßt also nicht wirklich in dieses Schema ("Ein gefahrener Laster ist eine Fahrerin"?), allerdings ist es doch relativ einfach, das Konzept zu erfassen. Auch hier hat unser Interesse wieder mehr der Technik gegolten als gutem objektorientiertem Programmieren (??). Du kannst alle möglichen "richtigen" und "falschen" Arten der Vererbung später studieren. Zunächst wollen wir uns einmal das Handwerk aneignen.

Doppelte Variablennamen

Beispielprogramm: MEHRVER3.CPP

Beide Basisklassen in MEHRVER3.CPP haben eine Variable mit dem gleichen Namen.

Gemäß den Regeln der Vererbung hat ein Objekt der Klasse GefahrenerLaster zwei Variablen mit dem Namen Gewicht. Das wäre ein Problem, würde der Definition von C++ nicht einen Weg enthalten, auf jede einzeln zuzugreifen. Wie Du vielleicht erraten hast, verwenden wir die Qualifikation, um die Variablen zu benutzen. Es sollte auch noch gesagt werden (obwohl es auf der Hand liegt), daß kein Grund besteht, nicht auch der abgeleitete Klasse eine Variable mit wieder demselben Namen zu verpassen. Um diese zu verwenden, wäre dann keine Qualifizierung notwendig, aber mit dem Namen der abgeleiteten Klasse natürlich möglich.

Hast Du also einmal die einfache Vererbung verstanden, ist Vielfachvererbung nichts weiter als eine Ausdehnung der gleichen Gesetze und Regeln. Wenn Du Variablen oder Methoden mit demselben Namen übernimmst, mußt Du dem Compiler mittels der Qualifizierung die richtige mitteilen.

Die Konstruktoren der beiden ererbten Klassen werden vor dem Konstruktor der abgeleiteten Klasse ausgeführt. Die Konstruktoren der Basisklassen werden in der Reihenfolge ihrer Deklaration in der header-Datei aufgerufen.

Ein praktisches Beispiel für Mehrfachvererbung

Beispielprogramm: ZEITDAT.H

Die Datei ZEITDAT.H gibt ein praktisches Beispiel für Mehrfachvererbung. Wir kehren zu den uns schon wohlbekannten Klassen DatumNeu und Tageszeit zurück.

Du kannst von dieser kleinen header-Datei einiges lernen, da es unser erstes Beispiel für Elementinitialisierung bei Mehrfachvererbung ist. Für diese Klasse existieren zwei Konstruktoren. Der erste ist sehr simpel und tut selbst überhaupt nichts, wie Du aus Zeile 13 ersehen kannst. Es werden also die Standardkonstruktoren für die Klassen DatumNeu und Tageszeit ausgeführt. In beiden Fällen wird der Konstruktor verwendet, der keine Parameter verlangt. Ein solcher existiert naturgemäß für beide Klassen.

Der zweite Konstruktor ist da schon interessanter, da er nicht einfach die Standardkonstruktoren verwendet, sondern einige seiner Parameter an die Konstruktoren der ererbten Klassen weitergibt. Nach dem Doppelpunkt in Zeile 14 stehen zwei Elementinitialisierer, mit denen wir Elemente der Klasse initialisieren. Da die beiden Elternklassen vererbt wurden, sind auch sie Elemente dieser Klasse und können initialisiert werden, wie wir es zeigen. Beide Elementinitialisierungen sind Aufrufe eines Konstruktors der Basisklasse und es ist offensichtlich, daß ein Konstruktor für die jeweilige Basisklasse mit einer korrespondierenden Anzahl und denselben Typen von Parametern existieren muß. In Zeile 15 rufen wir eigentlich den Standardkonstruktor der Klasse DatumNeu auf, da wir keine Parameter angeben. Wir könnten auch einfach das System den Standardkonstruktor aufrufen lassen, wenn wir dies aber selbst tun, sehen wir gleich, was passiert.

Nach den Elementinitialisierungen wird der normale Code des Konstruktors der abgeleiteten Klasse ausgeführt. Dieser findet sich in unserem Beispiel in Zeile 17.

Reihenfolge der Elementinitialisierung

Die Reihenfolge der Initialisierung der Elemente scheint ein wenig eigenartig zu sein, folgt aber einigen bestimmten Regeln. Du wirst Dich erinnern, wie wir im letzten Kapitel festgestellt haben, daß die Reihenfolge nicht der in der Liste folgt, sondern einer anderen strikten Reihenfolge, über die Du volle Kontrolle hast. Alle geerbten Klassen werden zunächst in der Reihenfolge, in der sie im Klassenkopf aufgeführt sind, initialisiert. Wären die Zeilen 15 und 16 vertauscht, würde die Klasse DatumNeu immer noch als erste initialisiert werden, da sie in Zeile 8 als erste erscheint. Wir haben erwähnt, daß C++-Klassen ihre Ahnen ehren und ihre Eltern vor sich selbst initialisieren. Das sollte eine ganz nette Eselsbrücke sein.

Dann werden alle lokalen Klassenelemente, so es solche gibt, initialisiert. Dabei folgt C++ wieder der Reihenfolge, in der sie in der Klasse deklariert sind, und nicht der der Initialisierungsliste.

Schließlich, nachdem alle Elementinitialisierungen in der richtigen Reihenfolge ausgeführt worden sind, wird der Code des Konstruktors selbst ganz normal abgearbeitet.

Wir verwenden die neue Klasse

Beispielprogramm: VERWZD.CPP

Das Beispielprogramm mit dem Namen VERWZD.CPP verwendet die Klasse DatumZeit, die wir gerade erstellt haben. Auch hier ist das Hauptprogramm wieder möglichst simpel gehalten. Für das Objekt mit dem Namen Jetzt verwenden wir den Standardkonstruktor, für die Objekte Geburtstag und Spezial den Konstruktor mit den Elementinitialisierungen.

Du solltest beim Verständnis des übrigen Codes keinerlei Schwierigkeiten haben. Du wirst feststellen, daß die Klasse, haben wir sie einmal fertiggestellt, sehr einfach zu verwenden ist.

Klassenschablonen (Class Templates) in C++

Bei der Entwicklung eines Programmes kommt es oft vor, daß man eine Operation auf mehr als einen Datentyp anwenden will. Zum Beispiel willst Du eine Liste von int Variablen, eine von float Variablen und eine mit Zeichenketten sortieren. Es ist nicht besonders sinnvoll, für alle drei Listen eigene Routinen zu schreiben, wenn die Sortierlogik immer dieselbe ist. Mit Klassenschablonen bist Du aus dem Schneider, da Du eine Funktion schreiben kannst, die imstande ist, alle drei Listen zu sortieren.

In der Ada Programmiersprache etwa ist dieses Konzept schon länger verwirklicht. Die Softwareindustrie hat fertige, fehlerbereinigte Routinen entwickelt, die mit vielen verschiedenen Datentypen arbeiten. So auch für C++. Es gibt Routinen zum sortieren, für Stapel, Schlangen usw. geben. Eine solche Bibliothek ist im Rahmen des ANSI-C++ Standards auch verfügbar: die STL - Standard Template Library. Das Studium dieser Bibliothek übersteigt zwar den Rahmen dieser Einführung, aber es ist sicherlich sinnvoll, sich diese und ihre Verwendung einmal genauer anzusehen.

Die nächsten drei Beispielprogramme werden die Verwendung von Schablonen gemäß ANSI-C++ Standard demonstrieren. Bei antiken C++-Compilern kann es da zu Problemen kommen. In jedem Fall aber solltest Du die Beispiele gut studieren.

Usere erste Schablone (Template)

Beispielprogramm: SCHABL1.CPP

Das Programm SCHABL1.CPP ist unser erstes Beispiel für die Verwendung einer Schablone. Dieses Programm ist so einfach, daß wir eigentlich nicht viel darüber sagen müssen, aber es illustriert die Verwendung des Typenparameters.

Die Schablone steht in den Zeilen 4 bis 8. Die erste Zeile zeigt uns (und dem Compiler), daß ein Typ ersetzt werden soll, nämlich der Typ JEDER_TYP. Dieser Typ kann von jedem anderen ersetzt werden, der mit der Vergleichsoperation in Zeile 7 verwendet werden kann. Wenn Du eine Klasse definiert und den Operator > überladen hast, kannst Du die Schablone mit Objekten dieser Klasse verwenden. Du mußt also nicht extra eine Funktion Maximum() für jeden Typen oder jede Klasse in Deinem Programm schreiben.

Diese Funktion wird automatisch für jeden Typen, mit dem sie im Programm aufgerufen wird, verfügbar. Der Code selbst ist glaube ich nicht der schwierigste.

Es wird Dir vielleicht aufgefallen sein, daß Du einen ganz ähnlichen Effekt auch mit einem Makro erzielen kannst. Bei einem Makro wird aber keine Typenüberprüfung durchgeführt. Daher und weil in C++ die inline Deklaration verfügbar ist, werden Makros von C++ Programmiererinnen (zum Glück!) so gut wie nie verwendet.

Eine Klassenschablone (Class Template)

Beispielprogramm: SCHABL2.CPP

Das Beispielprogramm in Datei SCHABL2.CPP ist etwas komplizierter, da es eine Schablone für eine ganze Klasse anstatt für eine einzelne Funktion beinhaltet. Der Code der Schablone steht in den Zeilen 6 bis 16 und ein genaueres Hinsehen zeigt, daß es sich dabei um eine komplette Klassendefinition handelt. Du hast recht, wenn Du meinst, der Stapel ist schwach bis sehr schwach, da nichts den Zugriff auf einen leeren Stapel verhindert und nichts einen vollen anzeigt. Unsere Absicht war es aber wieder, die Verwendung von Typenparametern (anhand einer möglichst simplen Klasse) zu illustrieren.

In Zeile 25 erzeugen wir ein Objekt mit dem Namen intStapel, einen Stapel für Variablen vom Typ int und ein zweites Objekt mit dem Namen floatStapel zum Speichern von Variablen vom Typ float. In beiden Fällen geben wir den Typ, mit dem das Objekt arbeiten soll, in "<<" Klammern an. Das System erzeugt dann das Objekt indem es zuerst alle Vorkommen von JEDER_TYP durch den angegebenen Typ ersetzt und dann ein Objekt diesen Typs erzeugt. Es kann jeder Typ verwendet werden, dem etwas zugewiesen werden kann, da wir dies mit dem Typenparameter in den Zeilen 13 und 14 tun.

Obwohl alle Zeichenketten verschieden lang sind, können wir unseren Stapel sogar zum Speicher von Zeichenketten verwenden, wenn wir nur Zeiger auf die Zeichenketten und nicht die ganze Zeichenkette selbst speichern. Das zeigen wir mit dem Objekt charStapel, das wir in Zeile 27 definieren und später dann auch verwenden.

Wenn Du Dich ein wenig mit diesem Programm beschäftigst, sollte Dir die Funktionsweise bald vertraut und klar sein. Du solltest es dann kompilieren und ausführen, wenn Dein Compiler mit Schablonen umgehen kann.

Wiederverwertung der Stapelklasse als C++-Schablone

Beispielprogramm: SCHABL3.CPP

Das Programm mit dem Namen SCHABL3.CPP verwendet die gleiche Klasse als Schablone, die wir im letzten Programm definiert haben, aber als Typen der Stapelelemente verwendet sie die Klasse Datum. Genauer gesagt verwendet sie einen Zeiger auf ein Objekt der Klasse Datum.

Du kannst auch die Klasse selbst im Stapel speichern und nicht nur einen Zeiger auf sie. Das wäre aber extrem ineffizient, da bei jeder Interaktion mit dem Stapel die gesamte Klasse in jenen hineinkopiert oder wieder herauskopiert würde. Die Verwendung eines Zeigers wird also bevorzugt und deshalb haben auch wir das getan.

Die drei letzten Beispielprogramme kannst Du kompilieren (und ausführen), wenn Ihr Compiler Schablonen unterstützt. Typenparameter sind Teil der C++ Spezifikationen und die meisten neueren Compiler unterstützen sie.

Was soll mein nächster Schritt sein?

Wieder haben wir einen Eckpfeiler des Programmierens in C++ aufgestellt. Mit dem Wissen um die Vererbungslehre hast Du nahezu alles, was Du benötigst, um die objektorientierten Konzepte von C++ effektiv einzusetzen. Du solltest das Studium also wieder einmal Studium sein lassen und zu programmieren beginnen. Was wir noch nicht behandelt haben, sind die virtuellen Funktionen, denen sich die nächsten beiden Kapitel widmen.

(weiter »)

Einführung in C++ bringt Dich in Partnerschaft mit Amazon.de von Null auf Programmieren in ein paar Klicks.

Zum Beispiel mehrfachbrauchbar:

Effektives modernes C++
So schwer ist das Programmieren nicht, wie Du hoffentlich auch an und in dieser Einführung siehst. Und auch bessere Programme schreiben ist leichter als man glaubt, wenn man den Meyers intus hat.
›› Mehr C++-Bücher

[ so ]