Konstruktor und Destruktor
Konstruktor
Um die Eigenschaften eines Objekts zu initialisieren, stehen bisher zwei Möglichkeiten zur Verfügung: die Initialisierung mit Konstanten per Zuweisung oder der Aufruf einer Methode, welche die Initialwerte per Parameter erhält.
// Klassendefinition
class Window
{
// Eigenschaften mit Defaultwerten
// per Zuweisung initialisieren
int xPos = 0, yPos = 0;
int width = 640, height = 480;
public:
// Defaultwerte ueberschreiben
void Init (int x, int y, int w, int h)
{
xPos = x; yPos = y;
width = w; height = h;
}
};
int main()
{
Window winObj;
}
Da Eigenschaften in der Regel stets zu initialisieren sind, gibt es hierfür eine spezielle Methode, den Konstruktor. Er wird in der englischsprachigen Literatur häufig als ctor bezeichnet. Der Konstruktor ist für alle Klassentypen (union, struct, class) verfügbar und weist einige Besonderheit auf.
Zum einen wird der Konstruktor automatisch aufgerufen, wenn ein Objekt definiert wird. Innerhalb des Konstruktors können die Eigenschaften entweder per Zuweisung oder per Initialisiererliste (wird gleich erläutert) initialisiert werden.
Die zweite Besonderheit betrifft den Rückgabewert des Konstruktors. Ein Konstruktor besitzt keinen Rückgabewert, auch nicht void! Sollte bei der Ausführung des Konstruktors ein Fehler auftreten, kann der Fehler nicht so ohne Weiteres zurückgemeldet werden. Später werden wir uns zwei verschiedene Verfahren ansehen, mit denen festgestellt werden kann, ob ein Konstruktor fehlerfrei ausgeführt werden konnte.
Und die dritte Besonderheit betrifft den Namen des Konstruktors. Der Konstruktor hat stets den gleichen Namen wie die Klasse.
Innerhalb des Konstruktors sind alle Anweisungen erlaubt, bis auf eine Ausnahme: Ein Konstruktor darf kein Objekt seiner eignen Klasse definieren, da dies zu einer Endlos-Schleife führen würde. Objekte anderer Klassen dürfen im Konstruktor jedoch definiert werden.
// Klassendefinition
class Window
{
// Eigenschaften
int xPos, yPos;
int width, height;
public:
// Konstruktor
Window()
{ // Eigenschaften initialiseren
xPos = 0; yPos = 0;
width = 640; height = 480;
}
};
int main()
{
Window winObj; // Ruft Konstruktor auf!
}
Da der Konstruktor bei der Definition eines Objektes automatisch aufgerufen wird, darf er in der Regel nicht im private-Bereich der Klasse stehen. Ansonsten ist es nicht möglich, ein Objekt der Klasse zu erstellen, da der Konstruktor nicht aufgerufen werden kann.
Aufrufzeitpunkt des Konstruktors
Für lokale Objekte wird der an der Stelle aufgerufen, an der das lokale Objekt definiert wird. Die Definition eines Objekts reserviert also nicht nur Speicher für dessen Eigenschaften, sondern kann je nach Umfang des Konstruktors die Ausführung von mehr oder weniger Code zur Folge haben.
Für globale Objekte wird der Konstruktor vor dem Eintritt in die main() Funktion ausgeführt. Nur so ist sichergestellt, dass alle globalen Objekte beim Eintritt in main() initialisiert sind. Sehen Sie sich dazu einmal die Ausgabe des folgenden Beispiels an.
#include <print>
// Klassendefinition
class Window
{
public:
// Definition des ctor
Window()
{
std::println("ctor von Window");
}
};
// Globales Objekt definieren
Window myWin;
int main()
{
std::println("Beginn main()");
Window localWin;
}
ctor von Window
Beginn main()
ctor von Window
Beim Testen eines Programms sollten Sie jedoch Folgendes beachten: Enthält ein Konstruktor einen Fehler, kann dies dazu führen, dass main() nicht mehr ausgeführt wird!
Konstruktorparameter und Initialisiererliste
Konstruktorparameter
Da der Konstruktor im Prinzip eine normale Methode ist, kann er auch Parameter besitzen. Benötigt ein Konstruktor Daten, sind diese bei der Definition eines Objekts anzugeben. Dazu werden die Daten nach dem Objektnamen innerhalb einer runden oder geschweiften Klammer aufgelistet.
Der Unterschied zwischen der Initialisierung mit einer runden und einer geschweiften Klammer ist im Kapitel Variablen beschrieben.
Die Initialisierung des Objekts erfolgt im Konstruktor durch Zuweisung der Parameter zu den entsprechenden Eigenschaften. Es müssen nicht alle Eigenschaften eines Objekts über Parameter initialisiert werden, sondern können auch auf feste Anfangswerte gesetzt werden.
Im nachfolgenden Beispiel erhält der Konstruktor der Klasse Window die Größe des Fensters sowie den Fenstertitel. D.h., das erste Fenster besitzt die Größe 640x480 und den Titel "Kleines Fenster" und das zweite Fenster die Größe 800x600 und den Titel "Grosses Fenster".
#include <print>
// Klassendefinition
class Window
{
// Eigenschaften
short xPos = 0, yPos = 0;
unsigned short width, height;
std::string title;
public:
// Definition des ctor
Window (unsigned short w, unsigned short h,
std::string_view t)
{
width = w; // Fenstergroesse lt. Parameter
height = h;
title = t; // Fenstertitel lt. Parameter
}
void Draw()
{
std::println("Fenster: {}",title);
}
};
// Objektdefinitionen
// Fuehren zum Aufruf des ctor
Window smallWin{640,480,"Kleines Fenster"};
Window bigWin{800,600,"Grosses Fenster"};
int main()
{
smallWin.Draw();
bigWin.Draw();
}
Fenster: Kleines Fenster
Fenster: Grosses Fenster
Initialisiererliste
Die Eigenschaften per Zuweisung zu initialisieren ist für intrinsische Datentypen effizient genug. Enthält die Klasse aber Objekte, wie zum Beispiel ein string-Objekt, wird die Initialisierung per Zuweisung ineffizient. Der Grund hierfür liegt darin, dass bei der Definition des eingeschlossenen string-Objekts dieses zunächst mit seinem Standardkonstruktor initialisiert wird. Diesem 'leeren' string-Objekt wird dann später, wenn der Konstruktor abgearbeitet wird, per Zuweisung der endgültige String zugewiesen. Das heißt, es werden bei der Initialisierung von eingeschlossenen Objekten per Zuweisung im Konstruktor immer zwei Schritte zur Initialisierung benötigt.
Diese zwei Schritte können unter bestimmten Bedingungen zusammengefasst werden. Enthält die Klasse des eingeschlossenen Objekts einen Konstruktor mit Parametern, kann das eingeschlossene Objekt per Initialisiererliste initialisiert werden. Die Initialisiererliste wird bei der Konstruktordefinition des umschließenden Objekts durch einen Doppelpunkt nach der Parameterklammer des Konstruktors eingeleitet. Nach dem Doppelpunkt werden die zu initialisierenden Eigenschaften aufgelistet, wobei der Initialwert jeder Eigenschaft in Klammer angegeben wird. Diese Initialisierung per Initialisiererliste beschränkt sich nicht nur auf eingeschlossene Objekte, sondern es können ebenfalls intrinsische Daten auf diese Weise initialisiert werden. So wird im nachfolgenden Beispiel zunächst das string-Objekt mit dem an den Konstruktor übergebenen Parameter t initialisiert und anschließend die beiden Eigenschaften width und height mit den Parametern w bzw. h. Der Einsatz einer Initialisiererliste schließt die Initialisierung von weiteren Eigenschaften per Zuweisung nicht aus. Nachfolgend werden die beiden Eigenschaften xPos uns yPos weiterhin per Zuweisung initialisiert.
#include <print>
// Klassendefinition
class Window
{
// Eigenschaften
short xPos, yPos; // Fenstergroeße
unsigned short width, height; // Fensterposition
std::string title; // Fenstertitel
public:
// Definition des ctor
Window (unsigned short w, unsigned short h, std::string_view t):
width{w}, height{h}, title{t}
{
xPos = yPos = 0;
}
void Draw()
{
std::println("Fenster: {}",title);
}
};
// Objektdefinitionen
// Fuehren zum Aufruf des ctor
Window smallWin{640,480,"Kleines Fenster"};
Window bigWin{800,600,"Grosses Fenster"};
int main()
{
smallWin.Draw();
bigWin.Draw();
}
Fenster: Kleines Fenster
Fenster: Grosses Fenster
Reihenfolge der Initialisierungen bei Initialisiererlisten
Die Reihenfolge der Initialisierungen richtet sich nach der Reihenfolge der Eigenschaften in der Klassendefinition. In der nachfolgenden Klasse Window wird also zuerst die Eigenschaft width, dann height und schließlich title initialisiert, unabhängig davon, in welcher Reihenfolge diese in einer Initialisiererliste stehen.
// Klassendefinition class Window
{
// Eigenschaften
short xPos, yPos; // Fensterposition
unsigned short width, height; // Fenstergroesse
std::string title; // Fenstertitel
public:
// Definition des ctor
Window (unsigned short w, unsigned short h, std::string_view t):
title{t}, width{w}, height{h}
{ xPos = yPos = 0; }
};
Zur Veranschaulichung dieses Sachverhalts einmal eine kleine Fehlerfalle. Laut vorheriger Aussage werden im Beispiel unten die Eigenschaften in der Reihenfolge len und dann pText initialisiert. Sehen wir uns die Definition des Konstruktors an. Dort wird len mit der Stringlänge des Textes pText initialisiert. Da pText aber erst nach len initialisiert wird, zeigt pText noch auf einen undefinierten Bereich und len erhält damit einen zufälligen Wert. Durch Vertauschen der beiden Definitionen der Eigenschaften in der Klasse würde das Beispiel das angedachte Ergebnis liefern.
// Klassendefinition
class CAny
{
int len;
char *pText;
...
public:
CAny(const char *pT): pText(pT), len(strlen(pText))
{...}
};
Konstruktor und Objektfelder
Besitzt eine Klasse einen Konstruktor mit Parameter und wird von dieser Klasse ein Objektfeld definiert, sind die Initialwerte für die einzelnen Objekte im Feld bei der Definition des Objektfelds zusätzlich in geschweiften Klammern anzugeben. Alternativ kann anstelle der zusätzlichen Klammerung für die Initialwerte ein expliziter Konstruktoraufruf stehen. Besitzt der Konstruktor nur einen Parameter, können die Initialwerte ohne die zusätzlichen Klammern angeben (Klasse One im nachfolgenden Beispiel).
#include <print>
#include <string>
// Klassendefinition
class One
{
std::string title;
public:
One(const char* text): title{text}
{}
};
class Two
{
std::string title;
int anyVal;
public:
Two(const char* text, int val):
title{text}, anyVal{val}
{}
};
// Definition der Objektfelder
One myObjects[] {"eins", "zwei"};
Two yourObjects[] {
{"eins",1},
{"zwei",2} };
// Expliziter Aufruf des ctors
Two moreObjects[] {
Two {"drei",3},
Two{"vier",4} };
int main()
{ }
Expliziter Konstruktor
Sehen wir uns noch eine besondere Form der Initialisierung an. Besitzt eine Klasse einen Konstruktor mit nur einem Parameter, kann die Initialisierung des Objekts bei dessen Definition auch per Zuweisung erfolgen.
#include <print>
#include <string>
// Klassendefinition
class One
{
std::string title;
public:
One(const char* text): title{text}
{}
};
int main()
{
One obj = "Text";
}
Soll die Initialisierung per Zuweisung verhindert werden, ist bei der Deklaration des Konstruktors das Schlüsselwort explicit dem Konstruktornamen voranzustellen.
#include <print>
#include <string>
// Klassendefinition
class One
{
std::string title;
public:
explicit One(const char* text): title{text}
{}
};
int main()
{
// One obj1 = "Text"; // Das geht nicht mehr
One obj2 {"Text"};
}
Konstruktoren mit einem Parameter definieren implizit eine Konvertierungsvorschrift. Im ersten Beispiel wird durch den Konstruktor One(const char*) die Konvertierungsvorschrift festgelegt, wie ein const char* in ein One-Objekt umzuwandeln ist.
Angemerkt sei an dieser Stelle noch, dass eine Klasse mehrere Konstruktoren besitzen kann. Mehr dazu später im Kapitel Überladen von Funktionen/Methoden.
Destruktor
Der Destruktor wird, genauso wie der Konstruktor, ebenfalls automatisch aufgerufen, jetzt jedoch beim Löschen des Objekts. Der Destruktor wird in der englischsprachigen Literatur auch als dtor bezeichnet.
Der Destruktor besitzt ebenfalls den gleichen Namen wie die Klasse, jedoch wird vor dem Namen das Tilde-Symbol ~ gestellt. Er liefert ebenfalls keinen Wert zurück und hat niemals Parameter.
Aufrufzeitpunkt des Destruktors
Für lokale Objekte wird der Destruktor zu dem Zeitpunkt aufgerufen, an dem das lokale Objekt gelöscht wird. In der Regel ist dies eine Stelle, an der eine geschweifte Klammer zu steht.
Bei globalen Objekten wird der Destruktor erst nach dem Verlassen von main() aufgerufen. Das folgende Beispiel demonstriert beide Fälle.
#include <print>
// Klassendefinition
class Window
{
public:
// Definition des ctor
Window()
{
std::println("ctor von Window");
}
// Definition des dtor
~Window()
{
std::println("dtor von Window");
}
};
// Globales Objekt definieren
Window myWin;
int main()
{
std::println("Beginn main()");
{
Window localWin; // Lokales Window-Objekt
}
std::println("Ende main()");
}
ctor von Window
Beginn main()
ctor von Window
dtor von Window
Ende main()
dtor von Window
Der Konstruktor des myWin-Objekts wird vor dem Eintritt in main() aufgerufen und dessen Destruktor nach dem Verlassen von main(). Der Konstruktor des lokalen localWindow-Objekts dagegen wird erst dann aufgerufen, wenn das Objekt erstellt wird. Da die Objektdefinition innerhalb eines Blocks erfolgt, beschränkt sich die Gültigkeit des Objekts auf diesen Block. Beim Verlassen des Blocks wird das Objekt gelöscht und damit dessen Destruktor aufgerufen. Beachten Sie also, dass beim Schließen eines Blocks ebenfalls Code ausgeführt werden kann.
Übungen
ctor_01:
Implementieren Sie für Klasse Stack aus der Übung klasse_03 (Klassen und Objekte) einen Konstruktor und Destruktor.
Geben Sie im Konstruktor einen Text aus und initialiseren die Eigenschaften des Stackobjekts.
Innerhalb der main() Funktion ist ein lokales Stackobjekt zu definieren und der Stack vollständig über die Methode Push() zu füllen ist.
Danach sind über die Methode Pop() 5 Werte vom Stack auszulesen.
Die restlichen auf dem Stack befindlichen Werte sollen dann beim Beenden des Programms automatisch vom Stack geholt und ausgegeben werden.
ctor: Stack initialisiert!
Schiebe Werte auf Stack:
41 67 34 0 69 24 78 58 62 64
Lese 5 Werte vom Stack:
64 62 58 78 24
Hole restliche Werte vom Stack:
69, 0, 34, 67, 41,
dtor: Stack geleert!
ctor_02:
Es ist eine Klasse für die Darstellung eines Fensters zu entwickeln. Ein Fenster soll die Eigenschaften Größe, Position, Fenstertitel und Fensterstil besitzen. Der Fensterstil ist durch einen enum-Datentyp festzulegen, wobei folgende Fensterstile möglich sind:
SYSMENU, MINBOX, MAXBOX und CLOSEBOX
Damit sowohl der Aufruf des Konstruktors wie auch des Destruktors verfolgt werden kann, sollen diese ihren Fenstertitel ausgeben.
Außer den erwähnten Eigenschaften sind für die Klasse die Methoden Draw(), MoveWin() und ResizeWin() zu implementieren. Die Funktion der Methoden MoveWin() und ResizeWin() ergibt sich aus ihren Namen. Die Methode Draw() soll alle Eigenschaften des Fensters wie nachfolgend dargestellt ausgeben.
In main() sind zwei Fenster zu erstellen, wobei das erste Fenster den Fensterstil SYSMENU erhalten soll und das zweite Fenster den Stile MINBOX.
Sie können zum jetzigen Zeitpunkt noch keine kombinierten Fensterstile an den Konstruktor übergeben, da arithmetische und logische Operationen mit enums noch nicht möglich sind.
Geben Sie den Fenstern die Titel "Fenster1" und "Fenster2". Die restlichen Eigenschaften sind mit beliebigen validen Werten zu initialisieren.
Nachdem die Fenster erstellt sind, sind deren Eigenschaften auszugeben. Anschließend verschieben Sie das erste Fenster und verändern die Größe des zweiten Fensters. Geben Sie danach zur Kontrolle nochmals die neuen Fensterdaten aus.
Fenster1 erstellt
Fenster2 erstellt
Fenster1
Position:10,10 Groesse:300,200
Stil:1
Fenster2
Position:20,20 Groesse:600,800
Stil:2
Fenster1
Position:30,30 Groesse:300,200
Stil:1
Fenster2
Position:20,20 Groesse:640,480
Stil:2
Fenster2 geloescht
Fenster1 geloescht