Konstruktor und Destruktor
Der Konstruktor ist der Erschaffer von Klasseninstanzen(Objekten), der Destruktor ihr Zerstörer.
Werden Konstruktor und Destruktor nicht explizit angegeben, so werden ein Standard - Konstruktor
und ein Standard Destruktor verwendet.
Schreibt man einen eigenen, non - standard constructor, so wird der Standard - Konstruktor dadurch
überschrieben. Ein nicht - Standard - Konstruktor übernimmt in der Regel Argumente. Das bedeutet zugleich,
dass man bei seinem Aufruf - und dieser erfolgt grundsätzlich bei jeder Erschaffung eines Objekts der
jeweiligen Klasse - auch Argumente mitgeben muss, da man sonst etwa den Fehler erhält:
"Kein geeigneter Standard - Konstruktor vorhanden!"
Syntax :
Eine Klasse hat einen Standardkonstruktor, wenn sie z.B. nichts enthält :
class X { } ;
Nun die Klasse X mit einem non - Standard - Konstruktor :
class X
{
X (int arg1, string arg2);
};
Der Konstruktor gibt niemals einen Rückgabewert, auch nicht void!
Der obige Konstruktor würde außerhalb der Klasse wie folgt implementiert:
X: :X(int arg1, string arg2)
{
perNr = arg1;
name = arg2;
}
Hat man nun den Standard - Konstruktor überschrieben, mit einem Konstruktor, der Argumente
entgegennimmt und will trotzdem die Möglichkeit behalten, Instanzen anzulegen, ohne dabei in
runden Klammern Parameter mitzugeben, dann muss man den Konstruktor überladen, d.h. man behält
den selbstgeschriebenen Konstruktor und schreibt den Standard - Konstruktor nocheinmal explizit.
Innerhalb der Klasse inline implementiert würde das so aussehen :
X ( ) { }
Syntax Destruktor:
standard : ~X( ){ }
non-standard : ~X( ){ cout<< "auf Wiedersehen!"; }
Abarbeitungsreihenfolge der Konstruktoren bei abgeleiteten Klassen
Was den Funktionsrumpf eines Konstruktors betrifft, so wird dieser zunächst in der Basisklasse
und dann in der abgeleiteten Klasse abgearbeitet, danach in den von der abgeleiteten Klasse abgeleiteten
Klassen. Im Fall von Elementinitialisierungslisten zeigt sich jedoch, dass dies für den Funktionskopf
nicht gilt. Hier werden nämlich Daten von der untersten abgeleiteten Klasse bis schließlich hin zur
Basisklasse durchgereicht, jeweils über den Funktionskopf.
Elementinitialisierer und Initialisierungslisten
Elementinitialisierer gehören zu Konstruktoren. Sie bestehen aus dem Namen der Eigenschaft,
für die sie verwendet werden und einem Anfangswert in runden Klammern dahinter. Vererbung ist keine
Voraussetzung für die Verwendung von Elementinitialisierern.
Beispiel(außerhalb der Klasse):
Statistik : : Statistik (Argumente) : angabe(a), datum(tag, monat)
{ Funktionskörper }
Wenn nun Vererbung hinzukommt, verlängert sich der Initialisierer zu einer Liste, in der die
Konstruktorwerte der abgeleiteten Klasse zur Basisklasse durchgereicht werden.
Syntax :
Lastwagen : : Lastwagen(string init_Ladung, double init_Lad_Menge, int init_ps, double init_kmh) :
Auto(init_ps, init_kmh) { }
Hier ist nun einiges zu beachten:
1) Die Parameter des Konstruktors der Basisklasse sind mit anzugeben.
2) Der Konstruktor der Basisklasse, der hinter dem Doppelpunkt aufgerufen wird, darf für seine
Argumente nur noch Namen angeben ohne Datentyp, weil es sich in diesem Teil nicht mehr um eine
Definition sondern nur noch um einen Funktionsaufruf handelt. Das ist bei anderen Funktionsaufrufen
genau so geregelt.
Mehrfachvererbung
Grundsätzlich ist es möglich, dass eine abgeleitete Klasse nicht nur einen Elter, sondern
mehrere Eltern hat. Die Unterklasse erbt dann alle Elemente beider oder mehr Klassen. Für
die Mehrfachvererbung gibt es zwei verschiedene Konzepte, die im Folgenden "additiv" und
"ersetzend" genannt werden.

"additive" Mehrfachvererbung

"ersetzende" Mehrfachvererbung
additive Mehrfachvererbung
Die abgeleitete Klasse erbt alles, was in den Basisklassen enthalten ist. Dabei werden Elemente
von anderen Elementen verdeckt, die den gleichen Namen haben.
ersetzende Mehrfachvererbung
Für diese Vorgehensweise müssen die Basisklassen, von denen abgeleitet werden soll, von einer
gemeinsamen Basisklasse abstammen, von der sie durch virtuelle Ableitung geerbt haben.
Syntax :
1) class Schaf : virtual public Tier
class Ziege : virtual public Tier
2) class Schiege : public Schaf, public Ziege
In diesem Fall kommt jedes gleichnamige Klassenelement in der Klasse Schiege nur einmal vor.
Verdeckung von Klassenelementen
Elemente von Basisklassen und abgeleiteten Klassen, die den gleichen Namen haben, werden voneinander
überdeckt. Mit dem Scope Resolution - Operator : : wird die Klasse, deren Element man benutzen will,
dann explizit angesprochen. Auf diese Weise stehen auch verdeckte Elemente wieder zur Verfügung.
statische Variablen und Funktionen
Statische Variablen, die in einer Klasse deklariert werden, müssen erneut außerhalb der Klasse deklariert
werden, weil sie quasi ein Link sind, der nach draußen zeigt. Die Deklaration außerhalb erfolgt ohne das
Schlüsselwort static. Eine statische Variable existiert genau einmal im gesamten Programm.
Ihre Lebensdauer erstreckt sich auf das gesamte Programm.
Der entscheidende Vorteil von statischen Variablen ist der, dass sie nicht beim Erzeugen eines neuen
Objekts, so wie die anderen Variablen erneut angelegt werden. Eine statische Variable nimmt man also genau immer
dann, wenn man aus einem best. Grund einen Wert braucht, der immer aktuell gehalten wird und der zugleich
allen Objekten / Instanzen zur Verfügung steht.
Bsp.: static int var;
Eine statische Elementfunktion ist zwar im Gültigkeitsbereich einer Klasse, existiert aber trotzdem
nicht in den Objekten dieser Klasse. Stattdessen kann sie direkt - ohne Instanz - mit dem Klassennamen
aufgerufen werden.
Arten der Vererbung
Ebenso wie die Deklaration eines Datenelements / Eigenschaft einer Klasse, kann auch die Vererbung selbst
public, protected oder private sein. Selten macht zwar etwas Anderes als public - Vererbung Sinn,
die Konzeption von C++ sieht jedoch alle 3 Möglichkeiten vor. Dabei wird das mögliche Ausmaß
des freien Zugriffs auf geerbte Daten- und Funktionselemente in der abgeleiteten Klasse festgelegt.
Nach private Vererbung sind alle Elemente private.
Nach protected Vererbung sind alle Elemente, die nicht sowieso schon private waren - protected.
Nach public Vererbung hat die abgeleitete Klasse private Zugriff auf protected Elemente und public Zugriff auf public Elemente.
"Freischalten" von Klassenmitgliedern nach private
Vererbung
Wenn man eine Klasse besitzt, die sehr viel kann, und man möchte eine Klasse davon ableiten, die
weniger können soll(so wie eine Adaptor - Klasse, z.B. stack als adaptor von deque),
dann redefiniert man ihre Schnittstellen, indem man sie zunächst private ableitet und dann einige
Funktionselemente nächträglich wieder zugänglich macht.
this - Zeiger
Der this - Zeiger ist die Art eines Objektes, "ich" zu sagen. Mit anderen Worten heißt das, er enthält
die Addresse des Objektes, in dem man sich gerade befindet. Normalerweise, d.h. wenn man ihn nicht
explizit anspricht, ist er ein versteckter Parameter, also nur implizit vorhanden, so wie der
Standard - Konstruktor. Meistens kann man ihn deshalb auch weglassen.
Wenn ich z.B. den kleiner Operator, <, bezüglich des Hubraums für eine Klasse Auto überladen will,
mit der Überladungsfunktion "bool operator < (const Auto& car)", kann ich sowohl
return (hub < car.hub); als auch
return (this -> hub < car.hub); schreiben.
friend - Klassen
friend ist ein Konzept, das das Data Hiding in C++ durchlöchert, d.h. wenn ich in einer Klasse
eine andere Klasse als friend deklariere, dann kann ich in der als friend deklarierten Klasse
auf alle DE und Funktionen der Klasse, in der friend deklariert wurde zugreifen.
Diese Möglichkeit wird v.A. zu Testzwecken häufig benutzt, wenn z.B. eine Kontrollklasse die Möglichkeit haben
soll, überall nachzugucken.
Syntax :
class A
{
friend class B;
};
friend - Funktionen
Analog zur friend - Klasse gibt es auch die Option, Funktionen als friend zu deklarieren.
Besonders interessant ist das, wenn man mit einer prozedural realisierten globalen Funktion, die gar
kein Mitglied einer Klasse ist, auf alle Datenelemente einer Klasse zugreifen will.
Syntax :
class A
{
friend class B: :Ausgabe( );
};
Polymorphie
Polymorphie (Vielgestaltigkeit) beinhaltet die Möglichkeit, erst zur Laufzeit festzulegen, welcher
Quellcode ausgeführt werden soll. Dies nennt man auch "späte Bindung", im Gegensatz zur "frühen Bindung"
wo schon zur Compile - Zeit feststeht, wie das Programm abläuft. So ist bei der frühen Bindung gleichsam
alles "fest verdrahtet".
Bei der Polymorphie arbeitet man mit dynamischem Speicher durch die Verwendung von dynamischen Objekten,
das sind mit new erzeugte namenlose Objekte, die über Zeiger auf die Basisklasse realisiert und
addressiert werden. Man schreibt außerdem gleichnamige Methoden, die unterschiedlich arbeiten können
in der Basisklasse und den abgeleiteten Klassen. Die additive Vererbung ist somit polymorphietauglicher
als die ersetzende.
Die Deklarierung von Methoden als virtual bewirkt, dass automatisch die Methode
der richtigen Klasse auf der richtigen Ebene in der Klassenhierarchie genommen wird, und genau dies
entscheidet sich erst zur Laufzeit(runtime). Die eigenen Methoden der abgeleiteten Klasse erreicht man durch
Downcast:
static_cast<Flugdrachen* >(ZRudolf)->fliegen( );
Formal ausgedrückt hieße das:
static_cast<Basisklasse* >(Zeigername)->Methode_der_Unterklasse( );
Über einfaches "Zeigern" erreicht man sowohl die Datenelemente der Unterklasse :
ZRudolf=new Flugdrachen;
ZObj1=ZRudolf;
ZObj1->farbe="gelb";
als auch die Methoden der Basisklasse. Beide Zeiger ZRudolf und ZObj1 sind Zeiger auf die
Basisklasse Jabberwocky.
Abstrakte Basisklassen
Es gibt zwei Möglichkeiten, eine abstrakte Basisklasse zu erstellen
1) Man deklariert den Konstruktor nicht public, sondern protected.
2) Man schreibt eine rein virtuelle Methode in der Basisklasse.
Bsp.: class abstr_Basis
{
public:
virtual void ausgabe( ) = 0;
};
Die Gleichsetzung mit 0 bewirkt, dass die Methode rein virtuell wird.
Eine rein virtuelle Methode der Basisklasse muss in jeder abgeleiteten Klasse neu definiert werden.
Exceptions
Exceptions werden zur Fehlerbehandlung eingesetzt. Bestimmte Ausnahmesituationen können so
mit einer sinnvollen Fehlermeldung abgefangen werden. Steht ein bestimmter Abschnitt des Quellcodes
im Verdacht, Laufzeitfehler zu verursachen, so umschließt man ihn mit einem try - Block:
try{ Verdächtiger Code if( verdächtig) throw Datenelement;}
Darunter steht dann:
catch(Datentyp_des_obigen_Elements) und für alle Fälle auch
catch(...) , beides gefolgt von der Exception - Handling - Routine in geschweiften Klammern.
Man kann auch den gesamten Komplex "Exception - Handling" nennen.
Überladen von Operatoren
Bsp.: Den Index - Operator, [ ], für eine Klasse, X, überladen
als inline Implementierung:
X& operator [ ](int n)
{
if (n>=size){
cout << "out of range";
exit(-1);
}
return p[n]; // p ist ein Zeiger, der ebenfalls in der Klasse deklariert wurde.
}
Glossar zu Templates und Containern
Templates und Container (Prosa)
Weiter zur Windows - Programmierung