Vererbung

Einführung

Ein wesentlicher Punkt in der OOP ist die Möglichkeit der Vererbung, d.h. im Prinzip, das man eine Klasse deklarieren kann, die die Funktionalität einer bereits existierenden Klasse erbt und diese u.U. noch erweitert. Man nennt eine Klasse, die von einer anderen Klasse deren Funktionalität erbt, eine abgeleitete Klasse. Die Klasse von der die Funktionalität geerbt wird, wird als Basisklasse bezeichnet.

Deklaration

Eine Klasse um die Funktionalität einer anderen Klasse zu erweitern ist im Prinzip ganz einfach. Im allgemeinen sieht die Deklaration wie folgt aus:

class <derivedclass> : <baseclass>
{
	/* --- Implementierung weiterer Methoden ---
	...
	*/
};

Nun erbt die abgeleitete Klasse alle öffentlichen Member der Basisklasse. So ließe sich z.B. von der bereits existierenden 'RECTANGLE'-Klasse ableiten. Nennt man die abgeleitete Klasse dann etwa 'CRect', dann könnte die Deklaration in etwa so aussehen:

class CRect : RECTANGLE
{
};

Nun ist es möglich innerhalb der Klasse 'CRect' auch auf die vier Methoden der 'RECTANGLE'-Klasse zuzugreifen. Standardmäßig sind alle von der Basisklasse vererbten Member in der abgeleiteten Klasse "geschützt" (also 'private'). Dies lässt sich jedoch einfach ändern. Sollen alle "öffentlichen" Member (also 'public') der Basisklasse auch in der abgeleiteten Klasse "öffentlich" (also 'public') sein, dann muss die letzte Deklaration wie folgt abgeändert werden:

class CRect : public RECTANGLE	//öffentliche Vererbung
{
};

Die vorige Deklaration entspräche folgender:

class CRect : private RECTANGLE	//private Vererbung
{
};

Verwendet man die öffentliche Vererbung, dann ließen sich mit der "neuen alten" Klasse 'CRect' nun auch folgende Aufrufe realisieren:

void main(void)
{
	CRect r;

	r.SetHeight(500);
	r.SetWidth(1001);

	cout << "Breite: " << r.GetWidth() << endl;	//Ausgabe: "Breite: 1000"
	cout << "Hoehe: " << r.GetHeight() << endl;	//Ausgabe: "Hoehe: 500"
}

'CRect' hat somit die vier Methoden von  'RECTANGLE' geerbt und 'CRect' kann jetzt genauso verwendet werden, wie bereits die 'RECTANGLE'-Klasse. Sogar der Kopierkonsruktor und der Standardkonstruktor wurden vererbt. Das erkennt man vor allem daran, dass bei der Bildung einer Instanz von 'CRect' die Werte für Breite und Höhe mit '0' (und nicht mit einem willkürlichen Wert) initialisiert werden. 

Erweiterung der Funktionalität durch das Erben von Methoden

Nun lassen sich aber auch in der abgeleiteten Klasse weitere Member deklarieren. Denkbar wäre in dem gegenwärtigen Beispiel der 'CRect'-Klasse eine Erweiterung um eine Funktion, welche die Fläche des Rechtecks ausrechnet. Die Implementierung könnte dann in etwa so aussehen:

class CRect : public RECTANGLE
{
public:
	unsigned long GetArea(void)
	{
		return (GetWidth() * GetHeight());
	}
};

Um die Höhe und Breite des Rechtecks zu bestimmen muss 'CRect'  die von 'RECTANGLE' geerbten Methoden verwenden. Denn die abgeleitete Klasse hat keinerlei Zugriff auf "private" Member der Basisklasse. Es existiert jedoch ein weitere Zugriffsbezeichner, der diese Beschränkung aufhebt, ohne die privaten Member auch nach außen hin gänzlich zugänglich machen zu müssen. Die Basisklasse muss die Member, die nach außen hin "geschützt" bleiben, in allen abgeleiteten jedoch zugänglich sein sollen, lediglich als 'protected' deklarieren. Dann sind die Member auch in der abgeleiteten Klasse 'protected', d.h. nach außen bleibt der Zugriff weiterhin verwehrt, aber innerhalb der Basisklasse sind die Member frei zugänglich. Ändert man nun in der Klasse 'RECTANGLE' für die Member 'm_ulWidth' und 'm_ulHeight' den Zugriffsbezeichner von 'private' auf 'protected', dann ließe sich die Methode 'GetArea' von 'CRect' auch wie folgt implementieren:

class CRect : public RECTANGLE
{
public:
	unsigned long GetArea(void)
	{
		return (m_ulWidth * m_ulHeight);
	}
};

Folgender Aufruf würde dann aber weiterhin scheitern:

void main(void)
{
	CRect r;

	r.m_ulWidth = 0;	//Fehler C2248: Kein Zugriff auf protected Element
}

Das Schlüsselwort 'protected' lässt sich auch bei der Vererbung verwenden (im Prinzip also eine geschützte Vererbung):

class CRect2 : protected RECTANGLE
{
};

In diesem Fall wären alle Member, die 'CRect2' von 'RECTANGLE' erbt "geschützt" (also 'protected'). Somit ließen sich weder die vier Methoden außerhalb der Klasse aufrufen, noch wäre ein Zugriff auf die Member 'm_ulWidth' und 'm_ulHeight' von außerhalb möglich. 

Somit hängen die Zugriffsrechte für jeden einzelnen Member, den eine abgeleitete Klasse von seiner Basisklasse erbt, zum einen von dem Zugriffsbezeichner der Member der Basisklasse und zum anderen vom Zugriffsbezeichner, der bei der Vererbung verwendet wird, ab. Private Member lassen sich generell überhaupt nicht vererben. Für alle anderen Member einer Basisklasse gilt folgendes: Bei der privaten Vererbung  (also 'private') sind alle vererbten Member (egal ob 'protected' oder 'public') in der abgeleiteten Klasse 'private'. Bei einer "geschützten" Vererbung (also 'protected') sind alle vererbten Member in der abgeleiteten Klasse 'protected'. Ist die Vererbung hingegen "öffentlich" (also 'public'), dann behalten die Member ihre Zugriffsrechte auch innerhalb der Basisklasse. 

'CRect' erbt zwar den Standard- und den Kopierkonstruktor, nicht aber den Konstruktor, der das Angeben von Höhe und Breite ermöglicht. Dieser sollte demzufolge noch einmal implementiert werden:

class CRect : public RECTANGLE
{
public:
	CRect(unsigned long ulWidth = 0, unsigned long ulHeight = 0)
	{
		SetWidth(ulWidth);
		SetHeight(ulHeight);
	}
	unsigned long GetArea(void)
	{
		return (m_ulWidth * m_ulHeight);
	}
};

Allerdings scheint dies ein wenig unnötig zu sein, denn der entsprechende Konstruktor der Basisklasse ruft genau die gleichen Funktionen auf. Besser wäre es, wenn man einfach den Konstruktor der Basisklasse aufrufen würde. Dies geht tatsächlich:

class CRect : public RECTANGLE
{
public:
	CRect(unsigned long ulWidth = 0, unsigned long ulHeight = 0)
	{
		RECTANGLE(ulWidth, ulHeight);
	}

	unsigned long GetArea(void)
	{
		return (m_ulWidth * m_ulHeight);
	}

};

Nun wir einfach die bereits existierende Variante des Konstruktors der Basisklasse aufgerufen und deren Implementierung verwendet. Allerdings ist diese Variante auch noch nicht perfekt, da jeder Konstruktor, einer abgeleiteten Klasse zunächst einmal den Standardkonstruktor der Basisklasse aufruft und in diesem Fall somit die Member zunächst mit '0' initialisiert, anschließend aber noch einmal verändert werden. Dies ist ein Aufruf zu viel. Dem kann man aber ganz leicht nachhelfen. Konstruktoren besitzen die Möglichkeit sogenannte "Elementinitialisierunglisten" zu verwenden. In denen lassen sich Membervariablen initialisieren aber auch Konstruktoren angeben. Dies klingt komplizierter als es ist. Man muss lediglich den Konstruktor wie folgt ändern:

class CRect : public RECTANGLE
{
public:
	CRect(unsigned long ulWidth = 0, unsigned long ulHeight = 0) : RECTANGLE(ulWidth, ulHeight)
	{
	}

	unsigned long GetArea(void)
	{
		return (m_ulWidth * m_ulHeight);
	}

};

Nun wird der Standardkonstruktor der Basisklasse nicht mehr verwendet. Stattdessen verwendet 'CRect' den "geeigneteren" Konstruktor von 'RECTANGLE'. An anderer Stelle werde ich näher auf diese "Elementinitialisierunglisten" eingehen. An dieser Stelle erscheinen sie wahrscheinlich als noch nicht unbedingt notwendig.

Übrigens lassen sich auch statische Methoden vererben. So könnte man beispielsweise (zumindest im Debug-Modus) jede Klasse von der Klasse 'CInstance' (s. Lektion: Statische Member) ableiten, um bei Programmende überprüfen zu können, ob alle Instanzen vollständig abgebaut wurden. So könnte man u.a. die Klasse 'RECTANGLE' von 'CInstance' ableiten. Dann besitzt auch die Klasse 'CRect' den Zugriff auf die statische Memberfunktion 'GetInstances' von 'CInstance':

void main(void)
{
	CRect r;
	cout << "Instanzen: " << r.GetInstances() << endl;	//Ausgabe: "Instanzen: 1"
}

Allerdings würde der Instanzzähler für alle Klassen, die von 'CInstance' abgeleitet sind, der selbe sein, d.h. wenn nun neben einer Instanz von 'CRect' auch eine Instanz von 'CInstance' gebildet wird, dann wird beim Aufruf der Methode 'GetInstance' immer '2'  zurückgegeben. Dabei ist es unerheblich, wie die Funktion 'CInstance' aufgerufen wird (über die Instanz von 'CInstance', über die Instanz von 'CRect' oder über den Operator '::'). Auf diese Weise lässt sich aber am Ende jedes Speicherleck erkennen, welches durch irgendeine nicht freigegebene Instanz einer Klasse entsteht, die von 'CInstance' abgeleitet ist. 

Erweitern von Funktionalität durch das Überschreiben von geerbten Methoden

Es lassen sich innerhalb einer abgeleiteten Klasse auch Methoden, die die Klasse von einer Basisklasse erbt, überschreiben. Somit wird, wenn man diese Methode aufruft, nicht die Methode der Basisklasse aufgerufen, sondern die der abgeleiteten Klasse. Im Falle der 'CRect'-Klasse könnte man so beispielsweise die Methode 'SetWidth' überschreiben, um auch Werte über '1000' bei der Angabe der Breite zuzulassen. Dies könnte dann in etwa so aussehen:

class CRect : public RECTANGLE
{
public:
	CRect(unsigned long ulWidth = 0, unsigned long ulHeight = 0) : RECTANGLE(ulWidth, ulHeight)
	{
	}

	unsigned long GetArea(void)
	{
		return (m_ulWidth * m_ulHeight);
	}
	void SetWidth(unsigned long ulWidth)
	{
		m_ulWidth = ulWidth;
	}
};

Nun lässt sich als Breite jeder beliebige Wert festlegen, wohingegen die Höhe immer noch an die Grenze von 1000 gebunden ist. Dazu folgendes Beispiel:

void main(void)
{
	CRect r;

	r.SetHeight(1001);
	r.SetWidth(1001);

	cout << "Breite: " << r.GetWidth() << endl;	//Ausgabe: "Breite: 1001"
	cout << "Hoehe: " << r.GetHeight() << endl;	//Ausgabe: "Hoehe: 1000"
} 

Hier sollte allerdings noch erwähnt werden, dass nun wieder ein Problem mit dem Konstruktor auftritt. Möchte man nämlich eine Instanz gleich mit zwei Werten initialisieren, dann werden jeweils die Funktionen der Basisklasse (also von 'RECTANGLE') und nicht die der abgeleiteten Klasse (also 'CRect') aufgerufen. Also sollte im Konstrukto die eingene Methode aufgerufen werden:

class CRect : public RECTANGLE
{
public:
	CRect(unsigned long ulWidth = 0, unsigned long ulHeight = 0) : RECTANGLE(0, ulHeight)
	{
		SetWidth(ulWidth);
	}

	unsigned long GetArea(void)
	{
		return (m_ulWidth * m_ulHeight);
	}
	void SetWidth(unsigned long ulWidth)
	{
		m_ulWidth = ulWidth;
	}
};

Die using-Deklaration

Nun ist es durchaus möglich in einer abgeleiteten Klasse eine Funktion zu deklarieren, die den gleichen Namen aber eine andere Deklaration, wie eine von der Basisklasse geerbte Funktion besitzt. Dazu folgendes Beispiel:

class base
{
public:
	int func() {return 0;}
};

class derived	:	public base
{
public:
	int func(double d) {return (int)d;}
};

Bildet man nun eine Instanz von 'D', dann lässt sich nur die eigene Funktion 'func' aufrufen, nicht die der Basisklasse (zumindest bei herkömmlicher Art und Weise):

derived d;
d.func(2.0);	//Ok: 'func' von 'derived' wird aufgerufen 
d.func();	//C2660: 'func' : Funktion akzeptiert keine 0 Parameter

Die Methode der Basisklasse scheint nun nicht mehr zu existieren. Allerdings lässt sie sich mit Hilfe des bereits bekannten Bereichsauflösungsoperators '::' dennoch aufrufen:

derived d;
d.base::func();	//Ok: 'func' von 'base' wird aufgerufen

Eine andere Möglichkeit, die Methode der Basisklasse aufzurufen, besteht darin, das Schlüsselwort 'using' innerhalb der abgeleiteten Klasse zu verwenden:

class derived	:	public base
{
public:
	//Funktion der Basisklasse per 'using'-Deklaration
	using base::func;

	int func(double d) {return (int)d;}
};

Mit Hilfe dieser 'using'-Deklaration wird die abgeleitete Klasse 'derived' angewiesen, auch die Funktion der Basisklasse neben der eigenen zur Verfügung zu stellen, d.h. nun sind folgende Aufrufe wieder möglich:

derived d;
d.func(2.0);	//Ok: 'func' von 'derived' wird aufgerufen 
d.func();	//Ok: 'func' von 'base' wird aufgerufen

Virtuelle Funktionen

Deklariert man einen Zeiger auf eine Klasse, dann kann dieser Zeiger in einen Zeiger einer Basisklasse gecastet werden. Entsprechendes gilt für Referenzen. Da eine abgeleitete Klasse die gesamte Funktionalität, d.h. alle zugänglichen Member von der Basisklasse erbt, ist dazu nicht einmal ein Operator zum expliziten Casting notwendig. Dazu folgendes Beispiel:

CRect r;
RECTANGLE *pr = &r, &refr =  r;

Nun lässt sich mit Hilfe des Zeigers 'pr' und mit Hilfe der Referenz 'refr' auf alle Member von 'r' zugreifen, die 'CRect' von 'RECTANGLE' geerbt hat. Somit können über Zeiger und über Referenz Höhe und Breite gelesen und verändert werden:

pr->SetWidth(600);
refr.SetHeight(480);

cout << "Breite: " << r.GetWidth() << endl;	//Ausgabe: "Breite: 600"
cout << "Hoehe: " << r.GetHeight() << endl;	//Ausgabe: "Hoehe: 480"

Alles scheint erwartungsgemäß zu laufen. Allerdings taucht an einer Stelle ein Problem auf. Wenn man über den Zeiger oder die Referenz die Methode 'SetWidth' aufruft, dann wird nicht die überschriebene Variante innerhalb von 'CRect' verwendet sondern die ursprüngliche Variante der Basisklasse 'RECTANGLE', denn Zeiger und Referenz sind vom Datentyp 'RECTANGLE'. Das dürfte nicht erwünscht sein, denn die Instanz auf die mit Hilfe des Zeigers und der Referenz verwiesen wird, unterstützt auch Breiten über '1000', aber eine solche Breite lässt sich über Zeiger und Referenz via 'SetWidth' nicht setzen, da die Methode der Basisklasse aufgerufen wird, die eben solche Werte nicht übernimmt. Das erkennt man leicht an folgendem Aufruf:

pr->SetWidth(1500);

cout << "Breite: " << r.GetWidth() << endl;	//Ausgabe: "Breite: 1000"

Wünschenswerter wäre es also, wenn nicht der Typ des Zeigers oder der Referenz ('RECTANGLE') bei der Wahl der aufzurufenden Methode ausschlaggebend wäre, sondern vielmehr der Typ der Instanz ('CRect') auf die verwiesen wird. Dazu verwendet man das Schlüsselwort 'virtual'. Es wird einfach bei der Deklaration der entsprechenden Funktion (also bei 'SetWidth') innerhalb der Basisklasse (also 'RECTANGLE') mit angegeben. In allen abgeleiteten Klassen ist diese Funktion, dann automatisch virtuell, d.h. eine weitere Angabe des Schlüsselwortes wird dann nicht mehr benötigt. Um also die Methode 'SetWidth' in 'RECTANGLE' als virtuell zu deklarieren, muss die Deklaration wie folgt geändert werden:

class RECTANGLE
{
private:
	unsigned long m_ulWidth, m_ulHeight;
public:
	//Kopierkonstruktor	
	RECTANGLE(const RECTANGLE& r);

	//Konstruktor
	RECTANGLE(unsigned long = 0, unsigned long = 0);

	//Setzen der Höhe
	void SetHeight(unsigned long ulHeight);
	
	//Ermitteln der Höhe
	unsigned long GetHeight(void);

	//Setzen der Breite
	virtual void SetWidth(unsigned long);
	
	//Ermitteln der Breite
	unsigned long GetWidth(void);
	
	
};

Nun sollte das obige Beispiel erwartungsgemäß funktionieren:

pr->SetWidth(1500);

cout << "Breite: " << r.GetWidth() << endl;	//Ausgabe: "Breite: 1500"

Ist auch nur eine Funktion innerhalb einer Klasse virtuell (egal ob eigens implementiert, oder von einer Basisklasse geerbt), dann wird für diese Klasse eine Tabelle mit Funktionszeigern für jede virtuelle Funktion (die sogenannte 'vftable') angelegt. Durch einen internen Zeiger erhält die Instanz Zugriff auf diese Tabelle. Sobald also irgendeine Klasse eine solche Tabelle besitzt, dann enthält auch jede abgeleitete Klasse eine solche Tabelle. Wird nun einem Basisklassenzeiger die Adresse einer Instanz von einer abgeleiteten Klasse zugewiesen, dann erhält eben dieser Zeiger den Zugriff auf die Tabelle mit den Funktionszeigern. Auf diese Weise ist es dem Basisklassenzeiger möglich immer die "richtige" Funktion aufzurufen. Soviel zur Theorie....

Im Prinzip macht es wenig Sinn, Methoden einer Klasse als nicht virtuell zu deklarieren, wenn von ihr abgeleitet werden soll. Sind Methoden als virtuell deklariert worden ist es zudem nicht zwingend, dass sie in abgeleiteten Klassen überschrieben werden. Wenn Destruktoren implementiert werden, dann sollten insbesondere diese auch als virtuell deklariert werden. Angenommen wir haben folgende Klasse:

class CSimpleArrayBase
{
public:
	//Sollte in jedem Fall überladen werden
	virtual int GetLength(void)
	{
		return -1;
	}
	
	//Destruktor
	virtual ~CSimpleArrayBase(void)
	{
	}
};

Von dieser Klasse soll nun die bereits bekannte Klasse 'CSimpleArray' abgeleitet werden, die zudem noch um eine Funktion ('GetLength') erweitert wird, die die Länge des Arrays zurückgeben soll und demzufolge in etwa so aussieht:

class CSimpleArray : public CSimpleArrayBase
{
private:
	int *m_pData, m_Length;
	
	//Kopiermethode
	void _Copy(const CSimpleArray& arr);
public:
	//Konstruktor
	CSimpleArray(int Length = 0);
	
	///Kopierkonstruktor
	CSimpleArray(const CSimpleArray& arr);

	//Länge des Arrays
	int GetLength(void);
	
	//Destruktor
	~CSimpleArray(void);
	
	//Wert setzen
	int& DataAt(int Index);

	//Operator '=' (noch zu überarbeiten)
	CSimpleArray& operator=(const CSimpleArray& arr);
	
};

//Implementierung von GetLength
int CSimpleArray::GetLength(void)
{
	return m_Length;
}

//Implementierung des Destruktors
CSimpleArray::~CSimpleArray(void)
{
	if (m_pData) delete[] m_pData;
}

//Implementierung der anderen Methoden...

Nun muss man sich nur noch folgenden Quelltext vorstellen, wo eine Instanz von 'CSimpleArray' mit Hilfe des Operators 'new' auf dem Heap erzeugt wird und ein Zeiger des Typs 'CSimpleArrayBase' eben auf diese Instanz verweist:

CSimpleArrayBase* pBase = new CSimpleArray;

Wird die Instanz nicht mehr gebraucht so sollte sie per 'delete' wieder entfernt werden:

delete pBase;
pBase = 0;

Wäre nun der Destruktor nicht virtuell, dann würde der Destruktor von 'CSimpleArray' nie aufgerufen werden. Stattdessen wird lediglich der Destruktor von 'CSimpleArrayBase' aufgerufen, da dies ja der Datentyp des Zeigers ist. Aufgrund dessen sollte ein Destruktor einer Basisklasse stets virtuell sein, zumindest wenn die Möglichkeit besteht, dass eine Instanz einer abgeleiteten Klasse auf dem Heap erzeugt wird und selbst einen Destruktor implementiert, der notwendige Aufräumarbeiten erledigt. Denn andernfalls wird u.U. reservierter Speicher nicht freigegeben und es entstehen Speicherlecks.

Die Basisklasse 'CSimpleArrayBase' implementiert neben dem Destruktor noch eine Funktion 'GetLength' und gibt einfach '-1' als einen ungültigen Wert zurück ('CSimpleArrayBase' kann die tatsächliche Länge ja nicht ermitteln). Es macht auch kaum Sinn von 'CSimpleArrayBase' eine Instanz zu bilden, da sie keinerlei Funktionalität besitzt. Die einzige Idee bei der Deklaration dieser Klasse ist es in Form eines Zeigers auf eine Instanz eines Arrays (wie 'CSimpleArray') zu  zeigen und die Möglichkeit bereitzustellen von diesem Array die Länge zu ermitteln. Es lässt sich ganz einfach erzwingen, dass eine Funktion von abgeleiteten Klassen implementiert werden muss und dass von der Basisklasse keine Instanzen gebildet werden können. Man deklariert einfach mindestens eine Methode der Klasse als "rein virtuell" und implementiert diese innerhalb der Basisklasse nicht. Da in diesem Fall die Methode 'GetLength' sowieso nur ungültige Werte liefert, ist sie wie dafür geschaffen. Als "rein virtuell" wird sie wie folgt deklariert:

class CSimpleArrayBase
{
public:
	//Rein virtuell -> muss überladen werden
	virtual int GetLength(void) = 0;
	
	//Destruktor
	virtual ~CSimpleArrayBase(void)
	{
	}
};

Sobald eine Klasse wenigstens eine "rein virtuelle" Methode besitzt gilt sie als "abstrakt", d.h. es lassen sich von ihr keine Instanzen bilden. Alle Klassen, die von einer abstrakten Basisklasse abgeleitet sind, sind, solange sie nicht alle "rein virtuellen" Funktionen implementiert haben, ebenfalls "abstrakt". Dazu folgendes Beispiel: 

class A
{
public:
	virtual int f(void) = 0;
	virtual int g(void) = 0;
};

class B : public A
{
public:
	int f(void) {return 0;}
};

class C : public B
{
public:
	int g(void) {return f();}
};

Da 'A' zwei virtuelle Funktionen besitzt und 'B' von 'A' abgeleitet ist und auch nur eine Funktion (nämlich 'f') implementiert, sind sowohl 'A' als auch 'B' abstrakte Basisklassen. 'C' übernimmt von 'B' die Implementierung von 'f' und implementiert selbst noch die Funktion 'g' und hat somit keine nicht implementierten "rein virtuellen" Funktionen mehr und ist aufgrund dessen auch nicht abstrakt. Alles in allem lassen sich nur von 'C' Instanzen bilden. Von 'A' und 'B' lassen sich lediglich Zeiger und Referenzen erstellen:

C c;
A& a = c;
B* pb = &c;

int i = a.f();
i = pb->g();

Reihenfolge der Konstruktor- und Destruktoraufrufe

Wird eine Instanz einer abgeleiteten Klasse gebildet, dann wird erst ihr der Konstruktor derjenigen Basisklasse aufgerufen, welche in der Klassenhierarchie ganz oben steht. Als letztes folgt dann der Aufruf des eigenen Konstruktors. Wird eine Instanz abgebaut, dann verhält es sich mit dem Destruktor genau umgekehrt: Zunächst wird der eigene Destruktor und zuletzt derjenige der "obersten" Basisklasse aufgerufen. Das erkennt man schnell an folgendem Beispiel:

#include <iostream.h>
class C1
{
public:
	C1(void) {cout << "c'tor von C1" << endl;}
	virtual ~C1(void) {cout << "d'tor von C1" << endl;}
}; 

class C2 : public C1
{
public:
	C2(void) {cout << "c'tor von C2" << endl;}
	virtual ~C2(void) {cout << "d'tor von C2" << endl;}
}; 

class C3 : public C2
{
public:
	C3(void) {cout << "c'tor von C3" << endl;}
	virtual ~C3(void) {cout << "d'tor von C3" << endl;}
};

void main(void)
{
	C3 c3;
}

/* --- Ausgabe: ---
c'tor von C1
c'tor von C2
c'tor von C3
d'tor von C3
d'tor von C2
d'tor von C1
*/

Für die nächsten Lektionen

Dies war eine Einführung in die einfache Vererbung, die es u.a. ermöglicht bereits existierende Klassen um Funktionalität zu erweitern oder geerbte Funktionalität zu ändern. Außerdem wurden virtuelle und rein virtuell Memberfunktionen und in diesem Zusammenhang auch der Begriff der abstrakten Basisklasse vorgestellt. All dies wird in den nächsten Lektionen weiter vertieft. In der anschließenden Lektion wird es um Mehrfachvererbung gehen.

Zurück Nach oben Weiter