Einführung in C++ Kapitel 7 — Vererbung

Ein Grund für die Verwendung von Vererbung ist die Möglichkeit, alten Code wiederzuverwenden, wobei Du diesen aber leicht verändern kannst, wenn der Code eines früheren Projektes die Anforderungen des neuen nicht ganz genau erfüllt. Es macht keinen Sinn, bei jedem neuen Programm mit null zu beginnen, da es sicherlich Code gibt, der in vielen Programmen vorkommt und Du solltest so viel davon verwenden wie möglich. Allerdings werden beim Adaptieren alter Klassen auch leicht und schnell Fehler gemacht. Solche Fehler passieren nicht so schnell, wenn Du die alte Klasse läßt, wie sie ist, und ihr einfach hinzufügst, was Du brauchst. Ein weiterer Grund für die Verwendung von Vererbung ergibt sich, wenn Du in einem Programm viele Klassen benötigst, die sehr ähnlich, aber eben doch nicht gleich sind.

In diesem Kapitel werden wir uns auf die Technik der Vererbung konzentrieren und wie wir diese in ein Programm einbauen. Bessere Beispiele, warum wir Vererbung überhaupt verwenden würden werden wir in späteren Kapiteln geben, wenn wir uns einige praktische Beispiele objektorientierten Programmierens ansehen. Das Konzept der Vererbung ist in einigen modernen Programmiersprachen vorhanden, jede geht natürlich ein wenig anders damit um. C++ erlaubt es Ihnen, alle oder einen Teil der Elemente und Methoden einer Klasse zu übernehmen, einige zu verändern und neue Elemente und Methoden hinzuzufügen. Es steht Dir also alle Flexibilität zur Verfügung und auch hier wurde die Methode gewählt, die in einem möglichst effizienten Code resultiert.

Eine einfache Klasse als Hors d'œuvre

Beispielprogramm: VEHIKEL.H

Abb. 7.1Die einfache Klasse, die Du in VEHIKEL.H findest, soll uns als Ausgangspunkt für das Studium der Vererbungslehre dienen. An dieser header-Datei ist nichts Außergewöhnliches, sie ist sehr einfach gehalten. Sie besteht aus vier Methoden, die wir zum Manipulieren der Daten unseres Vehikels verwenden können. Was die einzelnen Funktionen tun ist jetzt nicht wichtig. Wir werden uns auf diese Klasse später als Basisklasse oder Elternklasse beziehen, einstweilen aber soll sie nichts sein als eine ganz normale Klasse und wir werden sie auch so verwenden, um zu zeigen, daß sie sich nicht von all den Klassen unterscheidet, die wir schon kennen. Auf das neue Schlüsselwort protected werden wir gleich zurückkommen. Bild 7-1 ist eine graphische Darstellung der Klasse Vehikel.

Beachte die Zeilen 4, 5 und 19 bis zum Ende des Kapitels nicht. Wir werden Sie dann erklären. Diese Datei kann nicht kompiliert oder ausgeführt werden, da es sich ja nur um eine header-Datei handelt.

Die Implementation userer Vehikel-Klasse in C++

Beispielprogramm: VEHIKEL.CPP

Die Datei VEHIKEL.CPP enthält die Implementation der Klasse Vehikel. Die Methode mit dem Namen Initialisiere() weist den Variablen Raeder und Gewicht die Werte der Parameter zu. Wir haben Methoden, die die Anzahl der Raeder und das Gewicht zurückgeben und schließlich eine, die eine einfache Rechnung ausführt und das Gewicht zurückgibt, das auf jedem Rad lastet. Später werden wir einige Beispiele für Methoden geben, die interessantere Dinge vollbringen, jetzt sind wir aber mehr an der Schnittstelle interessiert, wir halten also die Implementation relativ simpel.

Wie wir oben festgehalten haben, handelt es sich um eine sehr einfache Klasse, die wir im nächsten Programm verwenden werden. Später in diesem Kapitel werden wir sie als Basisklasse verwenden. Du solltest die Klasse nun kompilieren, ausführen kannst Du die Klasse alleine freilich nicht.

Wir verwenden die Vehikel-Klasse

Beispielprogramm: TRANSPRT.CPP

Das Beispielprogramm TRANSPRT.CPP verwendet die Klasse Vehikel. Damit sollte klar sein, daß die Klasse Vehikel tatsächlich nicht mehr ist als eine normale Klasse in C++. Wir werden ein bißchen mehr aus ihr machen, wenn wir sie in den nächsten paar Beispielprogrammen als Basisklasse verwenden, um die Vererbung zu demonstrieren. Vererbung verwendet eine existierende Klasse und erweitert sie um Funktionalität, um eine andere, möglicherweise komplexere Aufgabe zu erfüllen.

Das Verständnis dieses Programmes sollte Dir mittlerweile wirklich keine Schwierigkeiten mehr bereiten. Wir deklarieren vier Objekte der Klasse Vehikel, initialisieren sie und geben einige der Werte am Bildschirm aus, um zu zeigen, daß die Klasse Vehikel wie eine einfache Klasse verwendet werden kann, da es sich ja um eine einfache Klasse handelt. Wir nennen diese Klasse (noch) einfache Klasse, im Gegensatz zu einer Basisklasse oder einer Kindklasse, wie wir es gleich tun werden.

Wenn Du dieses Programm verstanden hast, kompiliere es und führe es aus. Linke dazu die Objektdatei der Klasse Vehikel mit der des Programmes.

Unsere erste abgeleitete Klasse

Beispielprogramm: AUTO.H

Die Datei AUTO.H enthält unser erstes Beispiel einer abgeleiteten- oder Kindklasse. Die Klasse Vehikel wird durch :public Vehikel in Zeile 7 ererbt. Die abgeleitete Klasse Auto besteht aus allen Elementen der Basisklasse Vehikel und allen ihren eigenen Elementen. Obwohl wir an der Klasse mit dem Namen Vehikel nichts verändert haben, wurde sie zur Basisklasse allein durch die Art und Weise, wie wir sie hier verwenden. Obzwar sie hier als Basisklasse eingesetzt wird, gibt es keinen Grund, sie nicht weiterhin auch als ganz normale Klasse zu verwenden wie etwa im vorigen Beispielprogramm. Sie kann in ein und demselben Programm als Basisklasse und als einfache Klasse auftreten. Über die Frage, ob es sich um eine Basisklasse oder eine einfache Klasse handelt, entscheidet allein die Verwendung.

Wir müssen uns wieder ein wenig der Terminologie widmen. Im objektorientierten Programmieren wird eine Klasse, die von einer anderen Elemente erbt, generell abgeleitete Klasse oder Kindklasse genannt, in C++ spricht man meist von einer abgeleiteten Klasse. Da die beiden Ausdrücke recht bildhaft sind und sie meist austauschbar verwendet werden, werden auch wir in dieser Einführung beide gebrauchen. So ist der "richtige" Ausdruck für die Klasse, von der geerbt wird, in C++ Basisklasse, aber auch Elternklasse oder Superklasse können verwendet werden.

Eine Basisklasse ist eine recht generelle Klasse, die ein breites Spektrum an Objekten umfaßt, wogegen eine abgeleitete Klasse etwas spezieller und eingeengter, dafür aber auch nützlicher ist. Nehmen wir zum Beispiel an, wir hätten eine Basisklasse mit dem Namen Programmiersprache und eine abgeleitete Klasse mit dem Namen C++. Mit der Basisklasse könnten wir Pascal, Ada, C++ oder jede andere Programmiersprache definieren, sie könnte uns aber keine Auskunft darüber geben, wie Klassen in C++ verwendet werden, da sie von jeder Programmiersprache nur ein allgemeines Bild vermittelt. Die abgeleitete Klasse C++ hingegen könnte die Verwendung von Klassen definieren, nicht jedoch eine der anderen Programmiersprachen, da sie dafür zu eingeengt ist. Eine Basisklasse ist genereller, eine abgeleitete Klasse spezieller.

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

Zum Beispiel weiter-, ab-, um- und übergeleitet:

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

Im Fall unseres Beispielprogrammes kann die Basisklasse Vehikel verwendet werden, um so unterschiedliche Objekte wie Lastwägen, Autos, Fahrräder oder jedes andere Vehikel zu deklarieren. Mit der Klasse Auto hingegen können wir nur Objekte deklarieren, die auch Autos sind, da wir die Informationen, die mit dieser Klasse verwendet werden können, eingeschränkt haben. Deshalb ist die Klasse Auto spezieller und eingeschränkter als die Klasse Vehikel. Die Vehikel Klasse ist umfassender.

Wenn wir eine noch spezieller Klasse wollen, können wir Auto als Basisklasse verwenden um eine Klasse mit dem Namen Sportauto zu deklarieren und Informationen wie etwa Hoechstgeschwindigkeit oder Manta? inkludieren, Informationen also, die für ein normales Auto nicht wirklich von Belang sind. Die Klasse Auto würde dann zur selben Zeit als abgeleitete Klasse und als Basisklasse fungieren, womit klar sein sollte, daß diese Ausdrücke sich nur darauf beziehen, wie eine Klasse verwendet wird.

Wie deklariert man in C++ eine abgeleitete Klasse?

Genug des allgemeinen Geschwafels. Wir definieren eine abgeleitete Klasse, indem wir erstens die header-Datei der Basisklasse importieren (Zeile 5) und zweitens den Namen der Basisklasse nach dem Namen der abgeleiteten Klasse, von diesem durch einen Doppelpunkt getrennt, angeben (Zeile 7). Beachte das Schlüsselwort public gleich nach dem Doppelpunkt vorerst nicht. Es definiert öffentliche Vererbung, der wir unser nächstes Kapitel widmen wollen. Alle Objekte, die wir als Objekte der Klasse Auto deklarieren, bestehen also aus den beiden Variablen der Klasse Vehikel, da sie diese erben, und der Variable, die wir in der Klasse Auto selbst deklarieren, Passagieranzahl.

Abb. 7-2

Ein Objekt dieser Klasse hat drei der vier Methoden der Klasse Vehikel und die beiden neuen, die wir hier deklarieren. Die Methode Initialisiere() der Klasse Vehikel ist nicht verfügbar, da sie durch die lokale Version der Methode Initialisiere(), die Teil der Klasse Auto ist, verdeckt wird. Wird also ein Methodenname der Basisklasse in der abgeleiteten Klasse wiederholt, wird die lokale Version verwendet, um ein Anpassen der Methoden zu erlauben. Bild 7-2 zeigt ein Objekt der Klasse Auto.

Die Implementation der Basisklasse muß nur in kompilierter Form vorhanden sein. Der Code der Implementation kann aus ökonomischen Gründen vorenthalten werden, um Entwicklerinnen von Software zu helfen. Die header-Datei der Basisklasse muß allerdings verfügbar sein, da die Definition der Klasse benötigt wird, um sie zu verwenden.

Die Implementation der Auto-Klasse in C++

Beispielprogramm: AUTO.CPP

Die Datei AUTO.CPP ist die Implementation der Klasse Auto. Es wird Dir sicherlich gleich auffallen, daß man nirgends erkennen kann, daß es sich um eine abgeleitete Klasse handelt. Das kann nur aus der header-Datei der Klasse ersehen werden. Da wir nicht einmal feststellen können, ob es sich um eine abgeleitete oder eine einfache Klasse handelt, erfolgt die Implementation in genau derselben Weise wie bei jeder anderen Klasse auch.

Die Implementationen der beiden Methoden sind in genau derselben Art und Weise geschrieben wie Methoden für jede andere Klasse. Wenn Du das Gefühl hast, verstanden zu haben, worum es geht, solltest Du diese Datei für spätere Verwendung kompilieren.

Eine weitere abgeleitete Klasse

Beispielprogramm: LASTER.H

Die Datei LASTER.H gibt ein weiteres Beispiel für eine Klasse, die die Klasse Vehikel als Basisklasse verwendet und sie erweitert. Natürlich erweitert sie die Basisklasse anders als die Klasse Auto, da wir uns jetzt auf die Dinge konzentrieren, die für einem Lastwagen signifikant sind. Die Klasse fügt zwei Variablen und drei Methoden hinzu. Beachte auch hier das Schlüsselwort public nicht. Bild 7-3 repräsentiert die Klasse Laster.

Abb. 7-3

Es ist wichtig, zu erkennen, daß die beiden Klassen Auto und Laster absolut nichts miteinander zu tun haben, sie sind nur zufällig beide von derselben Basisklasse abgeleitet.

Sowohl die Klasse Auto als auch die Klasse Laster haben eine Methode mit dem Namen Passagiere(). Das ist aber kein Problem. Wenn Klassen in irgendeiner Beziehung zueinander stehen (wie diese mit derselben Basisklasse), erwartet man, daß sie auch ähnliche Dinge tun. In diesem Fall ist es logisch, auch denselben Methodennamen in beiden abgeleiteten Klassen zu verwenden.

Die Implementation der Lastwagen-Klasse in C++

Beispielprogramm: LASTER.CPP

Die Datei LASTER.CPP enthält die Implementation der Klasse Laster. Es geschieht hier nichts Außergewöhnliches.

Du solltest kein Problem haben, diese Implementation zu verstehen. Kompiliere also diese Klasse in Vorbereitung unseres Beispielprogrammes, das dann alle drei Klassen, die wir in diesem Kapitel definiert haben, verwenden wird.

Wir verwenden alle drei Vehikel-Klassen

Beispielprogramm: ALLEVEH.CPP

Die Datei ALLEVEH.CPP gibt ein Beispiel für ein Programm, das alle drei in diesem Kapitel diskutierten Klassen verwendet. Es deklariert Objekte der Elternklasse Vehikel und der beiden Kindklassen. Das soll zeigen, daß alle drei Klassen in einem Programm verwendet werden können.

Wir importieren die header-Dateien aller Klassen in den Zeilen 3 bis 5, damit wir die Klassen auch verwenden können. Beachte, daß die Implementationen der Klassen nicht sichtbar sind und dies auch nicht notwendig ist. Das ermöglicht es, den Code zu verwenden, ohne direkten Zugriff auf den Quellcode zu haben. Die Informationen in den header-Dateien müssen allerdings schon verfügbar sein.

In diesem Beispiel deklarieren wir nur ein Objekt für jede Klasse, wir könnten aber nach Herzenslust Objekte deklarieren und verwenden, so viele wir brauchen. Der Code für dieses Programm ist sehr klar. Wir haben die Klassen zuvor entwickelt, fehlerbereinigt und gespeichert und dabei die Schnittstelle möglichst einfach gehalten. Ansonsten findet sich in diesem Programm nichts Neues, das Verständnis sollte also keine allzu großen Schwierigkeiten bereiten.

Das Kompilieren dieses Programmes ist mit ein wenig Arbeit verbunden, aber nicht kompliziert. Die drei Klassen und das Hauptprogramm (mit der Funktion main()) können in beliebiger Reihenfolge kompiliert werden. Es müssen nur alle vier kompiliert sein, bevor wir sie linken können. Schließlich kannst Du das resultierende Programm ausführen. Tu das. Eine effiziente Nutzung von C++ wird es ständig erfordern, viele einzelne Dateien zu kompilieren und dann zu linken. Das liegt in der Natur von C++, sollte aber keine allzu schwere Last auf Deinen (atlantischen) Schultern sein, wenn Du über ein praktisches "make" oder einen Compiler mit Projektverwaltung verfügst.

Wozu #ifndef VEHIKEL_H?

Ich habe versprochen, auf die eigenartigen Präprozessoranweisungen in den Zeilen 4, 5 und 19 von VEHIKEL.H zurückzukommen. Nun ist die Zeit reif. Wenn wir die abgeleitete Klasse Auto definieren, müssen wir die gesamte Definition der Schnittstelle der Klasse Vehikel zur Verfügung stellen, da Auto eine von Vehikel abgeleitete Klasse ist und alles über die Basisklasse wissen muß. Wir tun dies, indem wir die header-Datei der Klasse Vehikel in die Klasse Auto importieren. Aus dem gleichen Grund müssen wir sie auch in die Klasse Laster importieren.

Schließlich müssen wir auch das Programm ALLEVEH.CPP über die Details der drei Klassen informieren und deshalb die header-Dateien importieren, was wir in den Zeilen 3 bis 5 auch tun. Das führt aber zu einem Problem. Wenn der Präprozessor zur Klasse Auto kommt, importiert es die Klasse Vehikel, da sie in der header-Datei der Klasse Auto aufgeführt ist. Da aber die Klasse Vehikel schon in Zeile 3 von ALLEVEH.CPP importiert wurde, wird sie zweimal importiert und wir versuchen damit, die Klasse Vehikel noch einmal zu deklarieren. Natürlich ist die Deklaration dieselbe, was das System aber nicht weiter kümmert: es verbietet schlicht und einfach eine zweite Deklaration einer Klasse. Wir gestatten das zweifache Importieren der Datei und verhindern gleichzeitig das zweifache Importieren der Klasse mit dem Wort VEHIKEL_H. Wenn das Wort schon definiert wurde, wird die Deklaration der Klasse übergangen, wurde es aber noch nicht definiert, wird die Deklaration importiert und gleichzeitig VEHIKEL_H definiert. Das Endergebnis dieses Kunstgriffs ist, daß die Klasse nur einmal importiert (und damit deklariert) wird, obwohl wir die Datei öfters importieren. Du solltest kein Problem haben, die Logik zu verstehen, wenn Du Dich kurz damit auseinandergesetzt hast.

Obwohl ANSI-C mehrfache Deklarationen erlaubt, solange sie identisch sind, verbiete C++ dies. Der Hauptgrund dafür ist, daß es für den Compiler sehr schwierig wäre, festzustellen, ob er schon einen Konstruktoraufruf für ein nochmals definiertes Element gemacht hat. Ein wiederholter Konstruktoraufruf für ein einzelnes Objekt kann ziemlichen Unsinn anrichten, also wurde das von vornherein ausgeschlossen, indem per Definitionem wiederholte Definitionen verboten wurden. Das stellt in keinem praktischen Programm ein Problem dar.

Wir haben den Namen VEHIKEL_H gewählt, weil es sich dabei um den Namen der Datei handelt, wobei der Punkt durch eine Unterstrich ersetzt wurde. Wenn Du das konsequent mit allen Deinen Klassen so hältst, kann es nie zu Namenskonflikten kommen, da jede Datei einen eigenen Namen haben muß, solange sich alle in einem Verzeichnis befinden. Es ist sicherlich gut, sich an diesen Kunstgriff zu gewöhnen. Alle weiteren header-Dateien in dieser Einführung werden ihn verwenden, um wiederholte Definitionen von Klassen zu verhindern und um Dir ein Beispiel zu sein. Du solltest das wirklich in allen Klassendefinitionen verwenden, wie trivial sie auch sein mögen.

Unsere erste praktische Vererbung in C++

Beispielprogramm: DATUMNEU.H

Wir schließen an Kapitel 5 an, erben die Klasse Datum in der Datei DATUMNEU.H und erweitern sie um eine Variable und eine Methode. Eigentlich ist dies kein besonders guter Weg, TagDesJahres zur Klasse Datum hinzuzufügen, da sie ein Teil der Struktur ist, die der Systemaufruf in der Klasse Datum zurückgibt. Wir sind aber mehr daran interessiert, Vererbung in einem brauchbaren Beispiel zu demonstrieren als eine perfekte Klasse zu entwerfen, wir werde also damit leben können.

Beispielprogramm: DATUMNEU.CPP

Die Datei DATUMNEU.CPP enthält die Implementation der neuen Methoden und sollte Dich vor keine großen Rätsel stellen. Diese Klassenimplementation verwendet den Array Tage[] aus der Implementation der Klasse Datum, der dort als globale Variable definiert wurde. Die Logik der Methode HoleTagDesJahres() ist sehr einfach. Sie kümmert sich nicht einmal um Schaltjahre. Wir sind eben nicht wirklich daran interessiert, eine gute Datumsklasse zu schreiben, wohl aber am Erlernen der Vererbung.

Beispielprogramm: VERDATN.CPP

Schließlich verwendet das Programm mit dem Namen VERDATN.CPP die neue Klasse in einer einfachen Weise, um zu illustrieren, daß die abgeleitete Klasse genauso einfach zu verwenden ist wie die Basisklasse und das Programm eigentlich keine Möglichkeit hat, festzustellen, daß es eine abgeleitete Klasse verwendet.

Du solltest auch dieses Programm kompilieren und linken, um darin weitere Erfahrung zu sammeln. Du mußt die Objektdateien der originären Klasse Datum, der abgeleiteten Klasse DatumNeu und der Hauptprogrammes linken.

Programmieraufgaben

  1. Erzeuge in ALLEVEH.CPP ein weiteres Objekt der Klasse Vehikel mit dem Namen Fahrrad und verfahre damit in etwa so wie mit dem Hochrad. Du mußt dann nur das Hauptprogramm neu kompilieren und alle vier Dateien linken. Die drei Klassen mußt Du nicht neu kompilieren.
  2. Erweitere die Klasse Laster um eine Methode, die das Gewicht des Lasters plus dem der Ladung zurückgibt und schreibe in ALLEVEH.CPP Code, der diesen Wert liest und am Bildschirm ausgibt. Du mußt also die Dateien LASTER.H, LASTER.CPP und natürlich ALLEVEH.CPP ändern.
  3. Erweitere die Klasse Name, die Du in Kapitel 5 geschrieben hast, um die Variable Geschlecht und um Methoden, den Wert dieser Variable zu setzen und auszulesen. Erlaubt sind nur 'M' oder 'F' (???). Du solltest diese Erweiterungen durchführen, indem Du eine von Name abgeleitete Klasse erstellst.

(weiter »)

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

Zum Beispiel Ahnen:

C: Programmieren von Anfang an
Diese Einführung in C++ setzt ein Grund legendes Verständnis der Programmiersprache C voraus. Doch keine Angst: Helmut Erlenkötters geniales "C Programmieren von Anfang an" vermittelt dieses Verständnis in Nu und Schwupp-di-wupp.
›› Mehr C-Bücher

[ verhält ]