C++ Kurs

Konstruktor und Destruktor

Die Themen:

Um die Eigenschaften eines Objekts zu initialisieren standen bisher zwei Möglichkeiten zur Verfügung: die Initialisierung mit Konstanten per Zuweisung innerhalb der Klasse oder aber der Aufruf einer Memberfunktion, welche die Initialwerte per Parameter erhält.

PgmHeader
// Klassendefinition
class Win
{
   int xPos = 0, yPos = 0;          // Member der Klasse initialisieren
   int widht = 640, height = 480; 
   ...
 public:
   // Defaultwerte ueberschreiben
   Init (int x, y, w, h)
   {
      xPos = x; yPos = y;
      width = w; height = h;
   }
};

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 wird für den Konstruktor häufig auch die Bezeichnung ctor verwendet. 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 Zuweisung oder per Initialisiererliste (wird gleich noch erläutert) initialisiert werden.

PgmHeader
// 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.

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

Und die dritte Besonderheit ist der Rückgabewert des Konstruktors. Ein Konstruktor besitzt keinen Rückgabewert, auch nicht void! Sollte also bei der Ausführung des Konstruktors etwa ein Fehler auftreten, so kann er nicht so ohne weiteres einen Fehlerstatus zurückmelden. Später im Kurs werden wir uns aber zwei verschiedene Verfahren noch ansehen, mit denen festgestellt werden kann, ob der Konstruktor richtig und vollständig ausgeführt werden konnte.

Und nun ein ganz wichtiger Hinweis:

AchtungDa 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

Sehen wir uns 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 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.

PgmHeader
// 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 wir uns dazu einmal die Ausgabe des Beispiels an.

PgmHeader
// 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";
   ...
}
Programmausgabector von Window
Beginn main()
HinweisBeim Testen Ihres Programms sollten Sie jedoch Folgendes beachten: enthält ein Konstruktor einen Fehler, so kann dies dazu führen, dass main() überhaupt nicht mehr ausgeführt 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 Daten, so müssen diese bei der Definition eines Objekts mit angegeben werden. Dazu werden die Daten nach dem Objektnamen innerhalb einer Klammer entsprechend aufgelistet. Im nachfolgenden Beispiel erhält der Konstruktor der Klasse Window die Größe des Fensters sowie den Fenstertitel. D.h. das erste Fenster besitzt danach die Größe 640x480 und den Titel Kleines Fenster und das zweite Fenster die Größe 800x600 und den Titel Grosses Fenster.

PgmHeader
// Klassendefinition
class Window
{
   // Eigenschaften
   short xPos = 0, yPos = 0;         // Fensterposition
   unsigned short width = 640, height = 480;   // Fenstergroesse
   std::string title;                // Fenstertitel
 public:
   // Definition des ctor
   Window (unsigned short w, unsigned short h, const std::string& t)
   {
      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.

Werden die Eigenschaften bei ihrer Definition initialisiert, wie width und height im obigen Beispiel, so dürfen Sie durchaus im Konstruktor mit anderen Werten überschrieben werden.

Initialisiererliste

Die Vorgehensweise, die Eigenschaften per Zuweisung zu initialisieren, ist für einfache Datentypen effizient genug. Enthält die Klasse aber auch Objekte, wie zum Beispiel ein string-Objekt, so wird die Initialisierung per Zuweisung uneffektiv. Der Grund hierfür liegt darin, dass bei der Definition des eingeschlossenen string-Objekts diese zunächst mit seinem Standard-Konstruktor 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.

Und selbstverständlich lassen sich diese zwei Schritte unter bestimmten Umständen zusammenfassen. Enthält die Klasse des eingeschlossenen Objekts einen Konstruktor mit Parametern, so kann das eingeschlossene 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 nachfolgenden 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.

PgmHeader
// 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

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

// main() Funktion
int main()
{
   ...
}
HinweisAber denken Sie stets daran: falls eine Klasse Objekte enthält, so sollten diese Objekte in der Regel über die Initialisiererliste initialisiert werden.

Reihenfolge der Initialisierungen bei Initialisiererlisten

Wird eine Initialisiererliste zur Initialisierung von Eigenschaften verwendet, dann spielt unter Umständen die Reihenfolge, in der die Eigenschaften initialisiert werden, eine wichtige Rolle. Die Reihenfolge der Initialisierung richtet sich dabei immer nach der Reihenfolge der Eigenschaften in der Klassendefinition. In der nachfolgenden Klasse Window wird also immer zuerst die Eigenschaft xPos, dann yPos usw. initialisiert, egal in welcher Reihenfolge die Initialisierungen bei der Definition des Konstruktors stehen.

PgmHeader
// 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 wir uns jetzt aber einmal genau 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 vertauschen der beiden Definitionen der Eigenschaften in der Klasse würde das Beispiel wieder richtig funktionieren.

PgmHeader
// 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 soll von dieser Klasse ein Objektfeld definiert werden, so müssen die Initialwerte für die einzelnen Elemente des Objektfelds bei der Definition des Objektfelds zusätzlich in geschweifte Klammern stehen. Alternativ kann anstelle der zusätzlichen Klammerung für die Initialwerte auch ein expliziter Konstruktoraufruf erfolgen. Noch einfacher wird es, wenn der Konstruktor nur einen Parameter besitzt. Dann können Sie die Initialwerte auch ohne die zusätzlichen Klammern angeben (Klasse One im nachfolgenden Beispiel).

PgmHeader
// Klassendefinition
class One
{
public:
  One(const char* const text)
  {...}
  ....
};
class Two
{
public:
  Two(const char* const text, int val)
  {...}
  ...
};
// Definition der Objektfelder
One myObjects[] {"eins", "zwei"};
Two yourObjects[] {{"eins",1},{"zwei",2}};
// Expliziter Aufruf des ctors
Two someObjects[] {Two("drei",3), Two("vier",4)};

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 bei dessen Definition auch über eine Zuweisung erfolgen.

PgmHeader
// 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

Soll diese Zuweisung bei der Objektdefinition verhindert werden, so ist bei der Deklaration des Konstruktors das Schlüsselwort explict dem Konstruktornamen voranzustellen (zweites Beispiel).

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

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

Konstruktore mit nur einem Parameter legen unter anderem auch eine Konvertierungsvorschrift fest. Im ersten Beispiel oben wird durch den Konstruktor Any(int) die Konvertierungsvorschrift festgelegt, wie ein int-Wert in ein Any-Objekt umgewandelt werden kann.

Angemerkt sei an dieser Stelle noch, dass eine Klasse auch mehrere Konstruktore enthalten kann und, wie im Kapitel über Klassen auch schon erwähnt, in der Regel auch enthält. Mehr dazu später im Kapitel über überladene Konstruktore.

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

Destruktordefinition

Auch der Destruktor wird, genauso wie der Konstruktor, automatisch aufgerufen, jetzt doch nicht bei der Definition eines Objekts sondern beim Löschen des Objekts. Der Destruktor wird in der englischsprachigen Literatur oft auch als dtor bezeichnet.

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

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

PgmHeader
// Klassendefinition
class Window
{
   ...           // Member der Klasse
 public:
   Window();     // ctor
   ~Window();    // dtor
};
// main() Funktion
int main ()
{
   Window myWin;
   ...
}
AchtungDer Destruktor darf niemals im private-Bereich der Klasse stehen!

Aufrufzeitpunkt des Destruktors

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

Für lokale Objekte wird der Destruktor zu dem Zeitpunkt aufgerufen, an dem das lokale Objekt gelöscht (zerstört) wird. In der Regel ist dies die Stelle, an der eine geschweifte Klammer zu steht.

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

PgmHeader
// 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";
}
ProgrammausgabeWindow ctor
Beginn main()
Window ctor
Window dtor
Ende main()
Window dtor

Auf eine Besonderheiten des Konstruktors bzw. Destruktors soll noch hingewiesen werden: im Gegensatz zu 'normalen' Memberfunktionen kann von einem Konstruktor und Destruktor niemals dessen Adresse bzw. Offset gebildet werden.

Beispiel und ÜbungUnd hier geht's zum Beispiel und zur Übung.