Was ein Konstruktor ist
Auf der vorigen Seite hast du Klassen definiert und ihnen Member-Variablen gegeben. Doch die Member eines frisch erzeugten Objekts enthalten den Müll, der zufällig in diesem Speicher lag, solange du sie nicht setzt. Ein Konstruktor behebt das: Er ist eine spezielle Member-Funktion, die in dem Moment automatisch läuft, in dem ein Objekt erzeugt wird, und seine einzige Aufgabe besteht darin, das Objekt in einem gültigen, vollständig initialisierten Zustand zu hinterlassen.
Ein Konstruktor trägt denselben Namen wie die Klasse und hat keinen Rückgabetyp – nicht einmal void. Du rufst ihn nie direkt auf; der Compiler ruft ihn für dich auf, sobald ein Objekt entsteht.
Der Counter() ohne Parameter heißt Standardkonstruktor – er ist derjenige, der verwendet wird, wenn du ein Objekt ohne Argumente erzeugst.
Parametrisierte Konstruktoren
Ein Konstruktor ohne Argumente ist in Ordnung, aber meist möchtest du ein Objekt mit konkreten Werten erzeugen. Ein parametrisierter Konstruktor nimmt Argumente entgegen und nutzt sie, um die Member zu initialisieren.
Eine Klasse kann mehr als einen Konstruktor haben, solange sich ihre Parameterlisten unterscheiden – das ist das gewöhnliche Überladen von Funktionen, angewendet auf Konstruktoren. Hier lässt sich Point mit oder ohne Koordinaten erzeugen:
Ein häufiger Fallstrick: Point p(); erzeugt kein Objekt – der Compiler liest es als Deklaration einer Funktion namens p, die ein Point zurückgibt. Um den Standardkonstruktor aufzurufen, schreibe Point p; (ohne Klammern) oder Point p{}; mit geschweiften Klammern.
Member-Initialisierungslisten
Bisher haben die Beispiele die Member innerhalb des Konstruktorrumpfs zugewiesen. Das funktioniert bei einfachen Typen, ist aber das falsche Werkzeug. Wenn der Rumpf läuft, ist jeder Member bereits standardkonstruiert; der Rumpf verwirft das dann und überschreibt es per Zuweisung. Eine Member-Initialisierungsliste initialisiert jeden Member direkt, vor dem Rumpf, in einem einzigen Schritt.
Die Syntax ist ein Doppelpunkt nach der Parameterliste, gefolgt von member(value)-Paaren:
Bei einem string-Member vermeidet das außerdem, einen leeren String zu konstruieren und ihn danach zuzuweisen – die Initialisierungsliste baut ihn schon beim ersten Versuch korrekt auf.
Die Initialisierungsliste ist nicht nur eine Optimierung; sie ist in drei Fällen zwingend erforderlich, weil der Rumpf nur zuweisen, aber nicht initialisieren kann:
const-Member – einemconstkann man nach seiner Existenz nichts mehr zuweisen.- Referenz-Member – eine Referenz muss bei ihrer Geburt gebunden werden.
- Member, deren Typ keinen Standardkonstruktor hat.
class Sensor {
const int id; // const-Member
int& slot; // Referenz-Member
public:
Sensor(int sensorId, int& s) : id(sensorId), slot(s) {}
// Der Versuch, id oder slot im Rumpf zu setzen, würde nicht kompilieren.
};
Eine wichtige Feinheit: Member werden in der Reihenfolge initialisiert, in der sie in der Klasse deklariert sind, nicht in der Reihenfolge, in der du sie in der Initialisierungsliste aufführst. Wenn der Initialisierer eines Members einen anderen liest, zählt die Deklarationsreihenfolge – die beiden zu verwechseln ist eine klassische Quelle für die Verwendung eines noch nicht initialisierten Werts.
Standardargumente und delegierende Konstruktoren
Du brauchst nicht immer separate Überladungen. Standardargumente lassen einen einzelnen Konstruktor mehrere Fälle abdecken: Lässt du ein Argument weg, springt der Standardwert ein:
Sei vorsichtig, wenn du einen parametrisierten Konstruktor mit Standardwerten mit einem separaten Point()-Standardkonstruktor kombinierst – der Compiler kann nicht entscheiden, welchen er für Point p; aufrufen soll, und meldet eine Mehrdeutigkeit. Entscheide dich für einen Ansatz.
Wenn du mehrere Konstruktoren hast, die sich eine gemeinsame Einrichtung teilen, lässt ein delegierender Konstruktor (C++11) einen Konstruktor einen anderen aufrufen, statt die Logik zu wiederholen. Du „delegierst“, indem du den anderen Konstruktor in die Initialisierungsliste setzt:
Der Kopierkonstruktor
Wenn du ein Objekt als Kopie eines anderen erzeugst – indem du es als Wert übergibst, es zurückgibst oder Foo b = a; schreibst – läuft der Kopierkonstruktor. Seine Signatur nimmt eine const-Referenz auf denselben Typ entgegen:
ClassName(const ClassName& other);
Wenn du keinen schreibst, generiert der Compiler einen Standard-Kopierkonstruktor, der jeden Member kopiert. Für Klassen, die nur Werte enthalten (ints, strings, vectors), ist das genau richtig, und du solltest keinen eigenen schreiben.
Der große Fallstrick wohnt im nächsten Kapitel über Speicher: Wenn deine Klasse einen rohen Zeiger auf Heap-Speicher besitzt, kopiert der Standard-Kopierkonstruktor den Zeiger, nicht die Daten – zwei Objekte zeigen dann auf denselben Speicher, und beide versuchen, ihn freizugeben. Das ist der Double-Free-Bug. Die Faustregel ist die Rule of Three/Five: Wenn du einen eigenen Destruktor schreibst, brauchst du mit ziemlicher Sicherheit auch einen eigenen Kopierkonstruktor (und Kopierzuweisung). In modernem C++ ist die sauberere Lösung, einen std::vector oder einen Smart Pointer zu halten, sodass die vom Compiler generierte Kopie einfach funktioniert.
Beachte außerdem, dass es zwingend ist, den Parameter per Referenz zu nehmen, nicht optional: Ein Kopierkonstruktor, der sein Argument als Wert nähme, müsste das Argument kopieren, um sich selbst aufzurufen – eine Endlosrekursion, die nicht einmal kompiliert.
Als Nächstes: Destruktoren
Ein Konstruktor baut ein Objekt auf; ein Destruktor baut es ab. Wenn ein Objekt seinen Gültigkeitsbereich verlässt oder gelöscht wird, läuft sein Destruktor automatisch – der perfekte Ort, um Dateien, Netzwerkverbindungen oder den Heap-Speicher freizugeben, den das Objekt hielt. Die nächste Seite behandelt, wie Destruktoren funktionieren, wann genau sie ausgelöst werden und wie sie sich mit Konstruktoren zusammentun, um C++ sein mächtiges RAII-Muster zu geben.
Häufig gestellte Fragen
Was ist ein Konstruktor in C++?
Ein Konstruktor ist eine spezielle Member-Funktion mit demselben Namen wie die Klasse und ohne Rückgabetyp. Er läuft automatisch, wenn ein Objekt erzeugt wird, und seine Aufgabe ist es, das Objekt in einen gültigen, vollständig initialisierten Zustand zu bringen, bevor anderer Code es verwendet.
Was ist der Unterschied zwischen einem Standard- und einem parametrisierten Konstruktor?
Ein Standardkonstruktor nimmt keine Argumente entgegen und wird verwendet, wenn du ein Objekt ohne Werte erzeugst (Point p;). Ein parametrisierter Konstruktor nimmt Argumente, sodass der Aufrufer das Objekt mit konkreten Werten initialisieren kann (Point p(3, 4);). Eine Klasse kann beide haben, weil Konstruktoren über ihre Parameterlisten überladen werden.
Warum sollte ich in C++ eine Member-Initialisierungsliste verwenden?
Eine Member-Initialisierungsliste (: name(n), age(a)) initialisiert die Member direkt, bevor der Konstruktorrumpf läuft. Sie ist für const-Member, Referenzen und Member ohne Standardkonstruktor zwingend erforderlich und vermeidet das verschwenderische Erst-Standard-konstruieren-dann-zuweisen, das passiert, wenn man im Rumpf zuweist.