C++ Kurs

Konstruktor und Destruktor

Die Themen:

Einleitung
Konstruktordefinition
Aufrufzeitpunkt des Konstruktors
Konstruktorparameter, Initialisiererliste und Klassenkonstanten
Expliziter Konstruktor
Destruktordefinition
Aufrufzeitpunkt des Destruktors
Beispiel und Übung

Einleitung

Um die Eigenschaften einer Klasse zu initialisieren, musste bisher immer eine Memberfunktion (oft mit dem Namen Init()) aufgerufen werden. Da die Initialisierung von Eigenschaften aber eine sehr oft benötigte Funktion ist, stellt C++ hierfür eine besondere Memberfunktion bereit, den Konstruktor. In der englischsprachigen Literatur (und auch im Kurs) finden Sie für den Konstruktor auch die Abkürzung ctor. 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 dann die Eigenschaften entweder per Einzelzuweisung oder per Initialisiererliste (wird gleich noch erläutert) initialisiert werden.


// Klassendefinition
class Win
{
   ...    // Member der Klasse
   ...    // hier folgt gleich der ctor
};
// main() Funktion
int main ()
{
   Win myWin;    // Aufruf des ctor!
   ...
}

Konstruktordefinition

Die zweite Besonderheit betrifft den Namen des Konstruktors. Damit der Konstruktor von anderen Memberfunktionen eindeutig unterschieden werden kann, besitzt er immer den gleichen Namen wie die Klasse. So hat der Konstruktor für die Klasse Window ebenfalls den Namen Window(). Innerhalb des Konstruktors sind alle C++ Anweisungen erlaubt, bis auf eine Ausnahme: ein Konstruktor darf kein neues Objekt seiner eignen Klasse anlegen. Dies würde ansonsten zu einer Endlos-Schleife führen. Objekte anderer Klassen dürfen im Konstruktor jedoch definiert werden.


// Klassendefinition
class Window
{
   ...       // Member der Klasse
 public:
   // Definition des ctor
   Window()
   {
      ...   // ctor Anweisungen
   }
};

Wenn Sie sich die Definition des Konstruktors einmal näher ansehen, so werden Sie eine weitere Besonderheit bemerken. Der Konstruktor besitzt keinen Rückgabewert, auch nicht void! Sollte also bei der Ausführung des Konstruktors etwa ein Fehler auftreten, so können Sie nicht so ohne weiteres einen Fehlerstatus zurückgeben. Später im Kurs werden wir uns aber zwei verschiedene Verfahren ansehen, mit denen Sie feststellen können, ob der Konstruktor richtig und vollständig ausgeführt werden konnte.

Und nun ein ganz wichtiger Hinweis:

Da der Konstruktor bei der Definition eines Objektes immer automatisch aufgerufen wird, darf er in der Regel nicht im private-Bereich der Klasse stehen. Sie könnten ansonsten kein Objekt der Klasse definieren, da der Konstruktor nicht aufgerufen werden kann.

Aufrufzeitpunkt des Konstruktors

Wissen Sie bis jetzt, dass der Konstruktor automatisch bei der Definition eines Objekts aufgerufen wird, so sehen wir uns nun einmal an, wann genau der Konstruktor für lokale und globale Objekte ausgeführt wird.

Für lokale Objekte wird der Konstruktor genau zu dem Zeitpunkt aufgerufen, an dem das lokale Objekt definiert wird. Die Definition eines Objekts reserviert ab nun nicht nur alleine Speicher für dessen Eigenschaften, sondern kann je nach Umfang des Konstruktors die Ausführung von mehr oder weniger Code zur Folge haben.


// Klassendefinition
class Window
{
   ...       // Member der Klasse
 public:
   // Definition des ctor
   Window()
   {
      ...   // ctor Anweisungen
   }
};
// main() Funktion
int main()
{
   ...
   Window myWin;    // Hier wird der ctor ausgeführt!
   ...
}

Für globale Objekte wird deren Konstruktor noch vor dem Eintritt in die main() Funktion ausgeführt. Nur so ist gewährleistet, dass alle globalen Objekte beim Eintritt in main() auch bereits initialisiert sind. Sehen Sie sich dazu einmal die Ausgabe des Beispiels an.


// Klassendefinition
class Window
{
   ...       // Member der Klasse
 public:
   // Definition des ctor
   Window()
   {
      cout << "ctor von Window\n";
      ...
   }
};

// Objektdefinition
Window myWin;

// main() Funktion

int main()
{
   cout << "Beginn main()\n";
   ...
}

ctor von Window
Beginn main()

Beim Testen Ihres Programms müssen Sie jedoch Folgendes beachten: enthält ein Konstruktor einen Fehler, so kann dies dazu führen, dass main() überhaupt nicht mehr aufgerufen wird!

Konstruktorparameter, Initialisiererliste und Klassenkonstanten

Konstruktorparameter

Da der Konstruktor vom Prinzip her eine normale Memberfunktion ist, kann er auch Parameter besitzen. Benötigt ein Konstruktor Parameter, so müssen diese bei der Definition eines Objekts mit angegeben. Dazu werden die Parameter nach dem Objektnamen innerhalb einer Klammer entsprechend aufgelistet. Im nachfolgenden Beispiel erhält der Konstruktor der Klasse Window die Größe des Fensters so wie den Fenstertitel. D.h. das erste Fenster besitzt dann die Größe 640x480 und den Titel Kleines Fenster und das zweite Fenster erhält die Größe 800x600 und den Titel Grosses Fenster.


// Klassendefinition
class Window
{
   // Eigenschaften
   short xPos, yPos;               // Fenstergrösse
   unsigned short width, height;   // Fensterposition
   std::string title;              // Fenstertitel
 public:
   // Definition des ctor
   Window (unsigned short w, unsigned short h, const std::string& t)
   {
      xPos = yPos = 0;             // Fensterposition auf (0,0) setzen
      width = w;                   // Fenstergrösse lt. Parameter
      height = h;
      title = t;                   // Fenstertitel lt. Parameter
   }
};

// Objektdefinitionen
// Führen zum Aufruf des ctor

Window myWin(640,480,"Kleines Fenster");
Window yourWin(800,600,"Grosses Fenster");

// main() Funktion
int main()
{
   ...
}

Die Initialisierung des Objekts wird im Konstruktor durch Zuweisung der entsprechenden Parameter zu den Eigenschaften vorgenommen. Selbstverständlich müssen nicht immer 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 Vorgehensweise, die Eigenschaften per Zuweisung zu initialisieren, ist für einfache Datentypen effizient genug. Enthält aber die Klasse auch Objekte, wie zum Beispiel ein string-Objekt, so wird die Initialisierung per Zuweisung ineffizient. Der Grund hierfür liegt darin, dass bei der Definition des eingeschlossenen string-Objekts diese zunächst mit einem leeren String initialisiert wird. Diesem 'leerem' 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.

Und (fast) selbstverständlich lassen sich diese zwei Schritte unter bestimmten Umständen zusammenfassen. Enthält die Klasse des eingeschlossenen Objekts (im Beispiel ist dies das string-Objekt für den Fenstertitel) einen Konstruktor mit Parametern, so kann das eingeschlossenen Objekt per Initialisiererliste gleich bei seiner Definition initialisiert werden. Eine Initialisiererliste wird bei der Konstruktordefinition durch einen Doppelpunkt nach der Parameterklammer des Konstruktors eingeleitet. Nach dem Doppelpunkt werden die zu initialisierenden Eigenschaften aufgelistet, wobei der Initialwert einer jeden Eigenschaft in Klammern angegeben wird. Diese Initialisierung per Initialisiererliste beschränkt sich aber nicht nur auf eingeschlossene Objekte innerhalb einer Klasse, sondern es können auch einfache Datentypen auf diese Weise initialisiert werden. So wird im Beispiel zunächst das string-Objekt mit dem an den Konstruktor übergebenen Parameter t initialisiert und anschließend die beiden einfachen Datentypen 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.


// Klassendefinition
class Window
{
   // Eigenschaften
   short xPos, yPos;               // Fenstergrösse
   unsigned short width, height;   // Fensterposition
   std::string title;              // Fenstertitel
 public:
   // Definition des ctor
   Window (unsigned short w, unsigned short h, const std::string& t):
         title(t), width(w), height(h)
   {
      xPos = yPos = 0;
   }
};

// Objektdefinitionen
// Führen zum Aufruf des ctor Window

myWin(640,480,"Kleines Fenster");
Window yourWin(800,600,"Grosses Fenster");

// main() Funktion
int main()
{
   ...
}

Aber denken Sie stets daran: falls eine Klasse Objekte enthält, so sollten diese Objekte in der Regel über die Initialisiererliste initialisiert werden.

Initialisiererliste und Klassenkonstanten

Enthält eine Klasse Konstanten, so müssen diese per Initialisiererliste initialisiert werden, da eine Zuweisung an Konstanten ja nicht erlaubt ist. Sie können Klassenkonstanten entweder mit einem Literal oder aber, und hier wird's interessant, mit einem Konstruktorparameter initialisieren. Dadurch ist es möglich, dass Klassenkonstanten für verschiedene Objekt unterschiedliche Werte annehmen können. So erhält die Klassenkonstante CLASS_CONST für das Objekt myObj den Wert 5 und für das Objekt yourObj den Wert 10.


// Klassendefinition
class Any
{
   const int CLASS_CONST;
 public:
   Any(int c): CLASS_CONST(c)
   {}
   ...
};
// Objektdefinitionen
Any myObj(5);
Any yourObj(10);

Reihenfolge der Initialisierungen bei Initialisiererlisten

Wenn Sie eine Initialisiererliste zur Initialisierung von Eigenschaften verwenden, dann sollten Sie allerdings die Reihenfolge kennen, in der die Eigenschaften initialisiert werden. Die Reihenfolge der Initialisierung richtet sich nach der Reihenfolge der Eigenschaften in der Definition der Klasse. In der nachfolgenden Klasse Window wird immer zuerst die Eigenschaft xPos, dann yPos usw. initialisiert, egal in welcher Reihenfolge die Initialisierungen bei der Definition des Konstruktors stehen.


// Klassendefinition
class Window
{
   // Eigenschaften
   short xPos, yPos;
   unsigned short width, height;
   std::string title;
   ...
};

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 Sie sich jetzt aber einmal die Definition des Konstruktors an. Dort wird len mit der Stringlänge des Textes pText initialisiert. Da aber pText erst nach len initialisiert wird, zeigt pText noch auf einen undefinierten Bereich und len erhält damit einen zufälligen Wert. Durch Tauschen der beiden Definitionen würde das Beispiel richtig arbeiten.


// Klassendefinition
class Any
{
   int len;
   char *pText;
   ...
 public:
   Any(const char *pT);
};
// Konstruktordefinition
Any::Any(const char *pT): pText(pT), len(strlen(pText))
{
   ...
}

Objektfelder

Besitzt eine Klasse einen Konstruktor mit Parametern und wollen Sie von dieser Klasse ein Objektfeld anlegen, so müssen Sie die einzelnen Elemente des Objektfelds auf eine etwas andere Weise initialisieren. Wie Sie solche Objektfelder initialisieren, ist nachfolgend dargestellt. Nach dem Namen des Objektfelds folgt der Zuweisungsoperator und dann innerhalb eines {...} Blocks der expliziten Aufruf des Konstruktors für jedes einzelne Feldelement. Beachten Sie dabei aber, dass die Anzahl der Konstruktoraufrufe auch mit der Anzahl der Feldelemente übereinstimmt!


// Klassendefinition

Expliziter Konstruktor

Sehen wir uns noch eine besondere Form der Initialisierung an. Besitzt eine Klasse einen Konstruktor mit genau einem Parameter, so kann die Initialisierung des Objekts auch dessen Definition über eine Zuweisung erfolgen (erstes Beispiel unten). Soll diese Zuweisung bei der Objektdefinition verhindert werden, so ist bei der Deklaration des Konstruktors das Schlüsselwort explict dem Konstruktornamen voranzustellen (zweites Beispiel).


// Klassendefinition
class Any1
{
   ...
 public:
   Any1(int);      // ctor mit einem(!) Parameter
   ...
};
// Objektdefinitionen
Any1 first1 = 1;   // Das wäre standardmässig auch erlaubt
Any1 second1(1);   // und das so wie so

// Ausschließen der Zuweisung
// Klassendefinition class Any2

{
   ...
 public:
   explicit Any2(int);
   ...
};
// Objektdefinitionen
Any2 first2 = 1;    // Das geht nicht mehr!
Any2 second2(1);    // Aber das immer

Solche Konstruktore mit nur einem Parameter verhalten sich prinzipiell gleich wie Konvertierungen! Im ersten Beispiel wird durch den Konstruktor Any(int) die Konvertierungsvorschrift festgelegt, wie ein int-Wert in ein Any-Objekt umgewandelt werden kann.

Beenden wir nun die Betrachtung des Konstruktors und wenden uns seinem Gegenstück zu, dem Destruktor.

Destruktordefinition

Der Destruktor wird in der englischsprachigen Literatur (und im Kurs) oft auch mit dtor abgekürzt.

Auch der Destruktor wird, genauso wie der Konstruktor, automatisch aufgerufen, jetzt doch nicht bei der Definition eines Objekts sondern beim Löschen des Objekts.

Damit der Destruktor von 'normalen' Memberfunktionen unterschieden werden kann, besitzt auch er einen fest vorgegebenen Namen. Der Destruktor hat ebenfalls den gleichen Namen wie die Klasse, nur wird jetzt vor dem Namen noch das Symbol ~ gesetzt (Tilde-Symbol, befindet sich auf der deutschen Tastatur auf der Plus-Taste).

Auch er kann niemals einen Wert zurückliefern und besitzt, im Gegensatz zum Konstruktor, niemals Parameter.


// Klassendefinition
class Window
{
   ...           // Member der Klasse
 public:
   Window();     // ctor
   ~Window();    // dtor
};
// main() Funktion
int main ()
{
   Window myWin;
   ...
}

Der Destruktor darf niemals im private-Bereich der Klasse stehen!

Aufrufzeitpunkt des Destruktors

Sehen wir uns auch hier wieder den Zeitpunkt des Destruktoraufrufs an.

Für lokale Objekte wird der Destruktor zu dem Zeitpunkt aufgerufen, an dem das lokale Objekt gelöscht (gelöscht) wird.

Komplizierter wird die Sache bei globalen Objekten. Hier wird der Destruktor erst nach dem Verlassen von main() aufgerufen. Das folgende Beispiel demonstriert beide Fälle. Sehen Sie sich die Programmausgaben an! 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 wird dann aufgerufen, wenn das Objekt definiert wird. Da die Objekt-Definition innerhalb eines Blocks erfolgt, beschränkt sich die Gültigkeit des Objekts auch 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 jetzt ebenfalls Code ausgeführt werden kann.


// Klassendefinition
class Window
{
   ...           // Member der Klasse
 public:
   Window()      // ctor
   {
      cout << "Window ctor\n";
      ...
   }
   ~Window();    // dtor
   {
      cout << "Window dtor\n";
      ...
   }
};
// Globales Window-Objekt definieren
Window myWin;
// main() Funktion
int main ()
{
   cout << "Beginn main()\n";
   {
      // Lokales Window-Objekt definieren
      Window localWindow;
      ...
   }  
   ...
   cout << "Ende main()\n";
}

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

Auf zwei Besonderheiten des Konstruktors bzw. Destruktors soll noch hingewiesen werden:

  1. Im Gegensatz zu 'normalen' Memberfunktionen kann von einem Konstruktor und Destruktor niemals dessen Adresse bzw. Offset gebildet werden.
  2. Ein Objekt das einen Konstruktor oder Destruktor enthält kann nicht als Element einer Variante (Union) verwendet werden.

Beispiel und Übung

Beispiel:

Die in der Lektion über Klassen in der Übung entwickelte Klasse Stack erhält nun einen Konstruktor und Destruktor.

Der Konstruktor initialisiert den so wichtigen Stackindex stackPtr, so dass die Memberfunktion Init() nun entfallen kann. Dadurch, dass der Konstruktor automatisch aufgerufen wird, kann es jetzt niemals ein Objekt der Klasse Stack geben, dessen Stackindex nicht initialisiert ist!

Im Destruktor werden die beim Löschen des Stackobjekts noch auf dem Stack liegenden Daten ausgegeben, d.h. der Stack wird sozusagen geleert.

Innerhalb der main() Funktion wird ein lokales Stackobjekt definiert, dessen Stack dann komplett mit der bekannten Memberfunktion Push(...) komplett gefüllt wird.

Danach werden über die Memberfunktion Pop(...) 5 Werte vom Stack ausgelesen.

Wird nun das Programm beendet, so wird das Stackobjekt gelöscht und der restliche Inhalt des Stacks ausgegeben.

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
Stack geleert!


// Beispiel zu Konstruktor und Destruktor

// Zuerst Dateien einbinden

#include <iostream>
#include <cstdlib>

using std::cout;
using std::endl;

// Stackgrösse
const int SSIZE = 10;

// Klassendefinition
class Stack
{
   // Die Eigenschaften sind private und damit
   // gegen den direkten Zugriff geschützt

   short stackPtr;
   short values[SSIZE];
 public:
   // Die Memberfunktionen müssen public sein sonst
   // kann mit der Klasse nicht gearbeitet werden

   Stack();
   ~Stack();
   bool Push(short val);
   bool Pop(short& val);
};

// Definition der Memberfunktionen
// Konstruktor

Stack::Stack()
{
   stackPtr = 0;
   cout << "Stack initialisiert!\n";
}
// Destruktor
Stack::~Stack()
{
   cout << "Hole restliche Werte vom Stack:\n";
   while (stackPtr > 0)
   {
      stackPtr--;
      cout << values[stackPtr] << ' ';
   }
   cout << "\nStack geleert!" << endl;
}
// Wert auf Stack ablegen
bool Stack::Push(short val)
{
   bool retval = false;
   // Falls Stack noch nicht voll ist
   if (stackPtr != SSIZE)
   {
      // Neuen Wert ablegen
      values[stackPtr] = val;
      stackPtr++;
      retval = true;
   }
   return retval;
}
// Wert vom Stack holen
bool Stack::Pop(short& val)
{
   bool retval = false;
   // Falls noch Werte auf dem Stack
   if (stackPtr!=0)
   {
      // Wert vom Stack holen
      stackPtr--;
      val = values[stackPtr];
      retval = true;
   }
   return retval;
}

// main() Funktion
int main()
{
   // Stackobjekt definieren
   Stack myStack;
   // Sonstige lokale Variablen definieren
   bool retVal;
   short val;

   // Stack so lange füllen bis er voll ist
   cout << "Schiebe Werte auf Stack:\n";
   do
   {
      val = std::rand() % 100;
      retVal = myStack.Push(val);
      if (retVal)
         cout << val << ' ';
   } while (retVal);

   // 5 Werte vom Stack wieder holen
   cout << "\nLese 5 Werte vom Stack:\n";
   for (int iIndex=0; iIndex<5; iIndex++)
   {
      myStack.Pop(val);
      cout << val << ' ';
   }
   cout << endl;
}

Übung:

Es ist eine Klasse zum Abspeichern von Fensterdaten zu entwickeln. Ein Fenster enthält dabei die bisher bekannten Eigenschaften Größe, Position und Fenstertitel. Hinzu kommt nun noch eine weitere Eigenschaft, der Fensterstil. Dieser Fensterstil soll durch einen enum-Datentyp repräsentiert werden, wobei folgende Fensterstile erlaubt sein sollen:

SYSMENU, MINBOX, MAXBOX und CLOSEBOX

Damit sowohl der Aufruf des Konstruktors wie auch der des Destruktors im Ausgabefenster erscheinen, sollen diese entsprechende Meldungen ausgeben (siehe nachfolgende Ausgabe).

Außer den erwähnten Eigenschaften soll die Klasse noch die Memberfunktionen Paint(), MoveWin(...) und ResizeWin(...) erhalten. Die Funktion der Memberfunktionen MoveWin(...) und ResizeWin(...) ergibt sich aus ihren Namen. Die Memberfunktion Paint() gibt alle Eigenschaften des Fensters wie nachfolgend dargestellt aus.

In der main() Funktion sind dann zwei Fenster zu definieren, wobei das erste Fenster den Standard-Fensterstil SYSMENU erhalten soll und das zweite Fenster die Stile MINBOX und SYSMENU. Geben Sie den Fenstern die Titel Fenster1 und Fenster2, die restlichen Eigenschaften sind beliebig.

Beachten Sie hierbei unbedingt die Fehlerfalle beim Aufruf von Funktionen mit enum-Parametern. Sie können Sie sich die Fehlerfalle hier nochmals ansehen.

Nach dem die beiden Fenster definiert sind, sind deren Eigenschaften auszugeben. Anschließend verschieben Sie das erste Fenster und verändern die Größe des zweiten Fensters. Geben Sie danach nochmals zur Kontrolle die neuen Fensterdaten aus.

Fenster1 erstellt
Fenster2 erstellt
Fenster1
Position: 10, 10
Grösse :300,200
Stil :1

Fenster2
Position: 20, 20
Grösse :600,800
Stil :3

Fenster1
Position: 30, 30
Grösse :300,200
Stil :1

Fenster2
Position: 20, 20
Grösse :640,480
Stil :3

Fenster2 gelöscht
Fenster1 gelöscht

Lösung ansehen!