C++ Tutorial

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.

1: // Klassendefinition
2: class Window
3: {
4:    int xPos = 0, yPos = 0;         // Member der Klasse init.
5:    int widht = 640, height = 480;
6:    ...
7: public:
8:     // Defaultwerte ueberschreiben
9:     void Init (int x, y, w, h)
10:    {
11:       xPos = x; yPos = y;
12:       width = w; height = h;
13:    }
14: };

Da in der Regel Eigenschaften 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.

1: // Klassendefinition
2: class Window
3: {
4:    ... // Member der Klasse
5: public:
6:    // Definition des ctor
7:    Window()
8:    {
9:       ... // ctor Anweisungen
10:   }
11: };

Aufrufzeitpunkt des Konstruktors

Für lokale Objekte wird der Konstruktor zu dem Zeitpunkt aufgerufen, an dem 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 wir uns dazu einmal die Ausgabe des folgenden Beispiels an.

1: // Klassendefinition
2: class Window
3: {
4:     ... // Member der Klasse
5: public:
6:     // Definition des ctor
7:     Window()
8:     {
9:        std::cout << "ctor von Window\n";
10:       ...
11:    }
12: };
13: // Globales Objekt definieren
14: Window myWin;
15:
16: // main() Funktion
17: int main()
18: {
19:    std::cout << "Beginn main()\n";
20:    ...
21: }

ctor von Window
Beginn main()

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, Initialisiererliste

Konstruktorparameter

Da der Konstruktor im Prinzip eine normale Methode ist, kann er 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 und auto beschrieben.

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".

1: // Klassendefinition
2: class Window
3: {
4:     // Eigenschaften
5:     short xPos = 0, yPos = 0;
6:     unsigned short width = 640, height = 480;
7:     std::string title;
8: public:
9:     // Definition des ctor
10:    Window (unsigned short w, unsigned short h,
11:            std::string_view t)
12:    {
13:       width = w;    // Fenstergrösse lt. Parameter
14:       height = h;
15:       title = t;    // Fenstertitel lt. Parameter
16:    }
17: };
18:
19: // Objektdefinitionen
20: // Führen zum Aufruf des ctor
21: Window myWin{640,480,"Kleines Fenster"};
22: Window yourWin{800,600,"Grosses Fenster"};

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, so wie im Beispiel die Eigenschaften xPos und yPos.

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 auch 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.

1: // Klassendefinition
2: class Window
3: {
4:     // Eigenschaften
5:     short xPos, yPos; // Fenstergröße
6:     unsigned short width, height; // Fensterposition
7:     std::string title; // Fenstertitel
8: public:
9:     // Definition des ctor
10:    Window (unsigned short w, unsigned short h,
11:            std::string_view t):
12:            title{t}, width{w}, height{h}
13:    {
14:       xPos = yPos = 0;
15:    }
16: };
17:
18: // Objektdefinitionen
19: // Führen zum Aufruf des ctor Window
20: Window myWin(640,480,"Kleines Fenster");
21: Window yourWin(800,600,"Grosses Fenster");

Denken Sie stets daran: Wenn eine Klasse Objekte enthält, sollten die Objekte über die Initialisiererliste initialisiert werden.

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. Die Initialisierungen von xPos und yPos erfolgt dann erst bei der Ausführung des Konstruktors.

1: // Klassendefinition
2: class Window
3: {
4:     // Eigenschaften
5:     short xPos, yPos; // Fenstergröße
6:     unsigned short width, height; // Fensterposition
7:     std::string title; // Fenstertitel
8: public:
9:     // Definition des ctor
10:    Window (unsigned short w, unsigned short h,
11:            std::string_view t):
12:            title{t}, width{w}, height{h}
13:    {
14:       xPos = yPos = 0;
15:    }
16: };

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.

1: // Klassendefinition
2: class CAny
3: {
4:    int len;
5:    char *pText;
6:    ...
7: public:
8:    CAny(const char *pT);
9: };
10: // Konstruktordefinition
11: CAny::CAny(const char *pT): pText(pT), len(strlen(pText))
12: {
13:    ...
14: }

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).

1: // Klassendefinition
2: class One
3: {
4: public:
5:    One(const char* text)
6:    {...}
7: };
8: class Two
9: {
10: public:
11:    Two(const char* text, int val)
12:    {...}
13: };
14: // Definition der Objektfelder
15: One myObjects[] {"eins", "zwei"};
16: Two yourObjects[] {{"eins",1},{"zwei",2}};
17: // Expliziter Aufruf des ctors
18: Two moreObjects[] {Two {"drei",3}, Two{"vier",4}};

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.

1: // Klassendefinition
2: class CAny
3: {
4:    ...
5: public:
6:    CAny(int); // ctor mit einem(!) Parameter
7: };
8:  // Objektdefinitionen
9:  CAny first1 = 1;   // Das ist standardmässig auch erlaubt
10: CAny second1{1};   // und das sowieso

Soll die Initialisierung per Zuweisung verhindert werden, ist bei der Deklaration des Konstruktors das Schlüsselwort explicit dem Konstruktornamen voranzustellen.

1: // Ausschließen der Zuweisung
2: // Klassendefinition class CAny
3: class CAny
4: {
5:    ...
6: public:
7:    explicit CAny(int);
8: };
9:  // Objektdefinitionen
10: CAny first2 = 1;    // Das geht jetzt nicht mehr!
11: CAny second2{10};   // Aber das immer

Konstruktoren mit einem Parameter definieren implizit eine Konvertierungsvorschrift. Im ersten Beispiel wird durch den Konstruktor CAny(int) die Konvertierungsvorschrift festgelegt, wie ein int-Wert in ein CAny-Objekt umzuwandeln ist.

Angemerkt sei an dieser Stelle noch, dass eine Klasse mehrere Konstruktoren besitzen kann. Mehr dazu später im Kapitel Überladen des Konstruktors.

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.

1: // Klassendefinition
2: class Window
3: {
4:    ... // Member der Klasse
5: public:
6:    Window();    // ctor
7:    ~Window();   // dtor
8: };

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 die 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.

1: // Klassendefinition
2: class Window
3: {
4: public:
5:    Window() // ctor
6:    {
7:       std::cout << "Window ctor\n";
8:    }
9:    ~Window() // dtor
10:   {
11:   std::cout << "Window dtor\n";
12:   }
13: };
14: // Globales Window-Objekt definieren
15: Window myWin;
16: // main() Funktion
17: int main ()
18: {
19:    std::cout << "Beginn main()\n";
20:    {
21:       // Lokales Window-Objekt definieren
22:      Window localWindow;
23:      ...
24:    }
25:    std::cout << "Ende main()\n";
26: }

Window ctor
Beginn main()
Window ctor
Window dtor
Ende main()
Window dtor

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.


Copyright 2024 © Wolfgang Schröder
E-Mail mit Fragen oder Kommentaren zu dieser Website an: info@cpp-tutor.de
Impressum & Datenschutz