HOWTO · C++

Leere Konstruktoren in C++

Erfahren Sie, was ein Konstruktor mit leerem Rumpf bedeutet, wie er sich vom Standardkonstruktor unterscheidet und wie Sie Member sicher initialisieren.

Auf dieser Seite

Ein „leerer Konstruktor“ bezeichnet meist einen selbst geschriebenen Konstruktor mit leerem Rumpf. Das ist eine verbreitete Beschreibung, aber keine eigene Kategorie in der Sprache C++. Entscheidend ist, ob eine Klasse einen Standardkonstruktor besitzt: einen Konstruktor, der ohne Argumente aufgerufen werden kann. Dieser Konstruktor kann Parameter mit Standardargumenten haben. „Standardkonstruktor“ bedeutet daher nicht zwingend, dass der Konstruktor mit einer leeren Parameterliste deklariert wurde.

Ein Konstruktor mit leerem Rumpf sorgt nicht automatisch dafür, dass jedes Member einen sinnvollen Wert erhält. Die Regeln für die Member-Initialisierung hängen davon ab, wie das Objekt erzeugt wird und wie die Member in der Klasse deklariert sind. Verwenden Sie einen In-Class-Member-Initializer oder eine Member-Initialisierungsliste, wenn die Klasse einen verlässlichen Anfangszustand benötigt.

Leere Konstruktoren und Standardkonstruktoren

Ein Standardkonstruktor ist jeder Konstruktor, der ohne Argumente aufgerufen werden kann. Zum Beispiel lässt sich Gauge(int level = 1) ohne Argument aufrufen und ist daher ein Standardkonstruktor. Ein Konstruktor mit leerem Rumpf wie Gauge() {} ist ebenfalls ein Standardkonstruktor. Ein Konstruktor kann jedoch nützliche Initialisierungen in seinem Rumpf oder in seiner Member-Initialisierungsliste vornehmen.

C++ kann einen Standardkonstruktor implizit deklarieren, wenn eine Klasse keine benutzerdeklarierten Konstruktoren hat. Sobald Sie einen anderen Konstruktor deklarieren, stellt der Compiler nicht zusätzlich einen impliziten Konstruktor ohne Argumente bereit. Sollen Objekte weiterhin ohne Argumente erzeugt werden können, deklarieren Sie diese Überladung ausdrücklich oder fordern Sie, sofern passend, mit = default die vom Compiler erzeugte Form an. Ein standardmäßig definierter Konstruktor und ein Konstruktor mit leerem Rumpf sind unterschiedliche Deklarationen: Mit ersterem soll der Compiler die Operation erzeugen, während letzterer eine selbst geschriebene Funktion ist, deren Rumpf nichts tut.

Der zugehörige Leitfaden zu Standardkonstruktoren behandelt die weiter gefassten Regeln für diese Konstruktorkategorie. Hier geht es um den leeren Rumpf und darum, was er für die Member eines Objekts bedeutet.

Einen leeren Konstruktor definieren

Ein Konstruktor ohne Argumente trägt denselben Namen wie seine Klasse und hat keinen Rückgabetyp. Das folgende Programm weist value einen In-Class-Anfangswert zu. Beim Erzeugen von meter wird der Konstruktor aufgerufen; die letzte Zeile schreibt das Member auf die Standardausgabe. Der In-Class-Initializer wird beim Erzeugen des Objekts und vor dem Konstruktor-Rumpf ausgeführt. Dieses vollständige Programm verwendet ausschließlich Funktionen der Standardbibliothek von C++ und lässt sich eigenständig kompilieren. Die beobachtete Ausgabe folgt.

Führen Sie dieses Beispiel aus:

Der Code und die angezeigten Bezeichnungen oder Ausgaben in diesem Abschnitt bleiben auf Englisch.

#include <iostream>

struct Meter {
    int value{7};
    Meter() {}
};

int main() {
    Meter meter;
    std::cout << meter.value << '\n';
}
7

Die Deklaration Meter() {} hat einen leeren Rumpf; value{7} initialisiert das Member, bevor dieser Rumpf ausgeführt wird. Beim Erzeugen von meter wird der Konstruktor ohne Argumente aufgerufen, und das Programm gibt den Wert des Members aus. Das Beispiel verdeutlicht, dass der Konstruktor und die Quelle der Member-Initialisierung unterschiedliche Rollen haben.

Sie können Member auch in einer Member-Initialisierungsliste initialisieren, zum Beispiel mit Meter() : value(7) {}. Das ist für Member erforderlich, denen nach der Konstruktion kein Wert zugewiesen werden kann, etwa Referenz-Member und const-Daten-Member. Außerdem ist es häufig der klarste Ort, um Invarianten festzulegen. Die Member werden in der Reihenfolge ihrer Deklarationen in der Klasse initialisiert, unabhängig davon, in welcher Reihenfolge ihre Namen in der Liste stehen.

Was mit Membern ohne Initialisierer geschieht

Ein leerer Konstruktor-Rumpf initialisiert keine skalaren Member. Hat ein Skalar wie int count; keinen In-Class-Initializer und steht er auch nicht in der Member-Initialisierungsliste, bleibt er bei der Default-Initialisierung eines Objekts ohne initialisierten Wert. Ihn zu lesen, bevor ihm ein Wert zugewiesen wurde, ist nicht sicher. Verlassen Sie sich nicht auf einen späteren Setter, es sei denn, die Klasse verhindert jeden Zugriff, bevor dieser Setter aufgerufen wurde.

Hat ein ausgelassenes Member einen Default-Member-Initializer, wird dieser verwendet. Andernfalls wird das Member entsprechend seinem Typ initialisiert; ein Member vom Klassentyp wird durch Auswahl eines Standardkonstruktors default-initialisiert. Das bedeutet nicht, dass alle solchen Member anwendungsspezifische Werte erhalten. Prüfen Sie für jedes Member den Typ und dessen Initialisierungsanforderungen, statt anzunehmen, ein leerer Rumpf erzeuge ein vollständig initialisiertes Objekt.

Default-Initialisierung und Wertinitialisierung sind unterschiedliche Initialisierungsarten. Bei einem Klassenobjekt ruft die Default-Initialisierung den ausgewählten Standardkonstruktor auf. Bei der Wertinitialisierung kann das Objekt zunächst auf null initialisiert werden, wenn der ausgewählte Standardkonstruktor nicht benutzerdefiniert ist; anschließend erfolgt die Default-Initialisierung. Ein selbst geschriebener Konstruktor mit {} ist benutzerdefiniert. Die Wertinitialisierung macht daher ein ansonsten nicht initialisiertes skalares Member dieser Klasse nicht sicher. Diese Regeln lassen sich leicht falsch verstehen. Ein In-Class-Initializer oder eine Member-Initialisierungsliste drückt den gewünschten Zustand zuverlässiger aus.

Zum Beispiel erhält count in struct Sample { int count{}; Sample() {} }; durch seinen In-Class-Initializer den Wert null. Dagegen wird count in struct Sample { int count; Sample() {} }; nicht allein dadurch initialisiert, dass der Konstruktor existiert. Die geschweiften Klammern eines leeren Funktionsrumpfs sind keine Member-Initialisierer.

Wann ein leerer Konstruktor sinnvoll ist

Ein Konstruktor mit leerem Rumpf ist sinnvoll, wenn die Klasse während der Konstruktion nichts zu tun hat und jedes Member bereits eine geeignete Initialisierungsregel besitzt. Er kann auch einen bewusst vorgesehenen öffentlichen Konstruktionsweg für ein Framework oder einen Container bereitstellen, der eine Default-Konstruktion voraussetzt. In beiden Fällen sollte die Klasse unmittelbar nach der Konstruktion gültig sein, statt darauf zu vertrauen, dass Aufrufer an eine bestimmte Abfolge von Setter-Aufrufen denken.

Bevorzugen Sie In-Class-Member-Initializer für einfache Standardwerte, die mehrere Konstruktoren gemeinsam verwenden. Nutzen Sie eine Member-Initialisierungsliste, wenn der Anfangswert von Konstruktorargumenten abhängt, wenn const- oder Referenz-Member initialisiert werden oder wenn ein Member-Typ einen bestimmten Konstruktor benötigt. Erfordert die Einrichtung Validierung, das Beschaffen von Ressourcen oder das Herstellen einer Klasseninvariante, erledigen Sie diese Arbeit in einem passenden Konstruktor, statt dessen Rumpf leer zu lassen.

Je nach verwendeter Operation können Sammlungen und Arrays voraussetzen, dass ihre Elemente default-konstruierbar sind. Ein ausdrücklicher Konstruktor ohne Argumente kann diese Anforderung erfüllen, macht eine aufgeschobene Initialisierung aber nicht von selbst sicher. Überlegen Sie, ob eine Factory, Konstruktorargumente oder eine Containeroperation, die Elemente direkt an Ort und Stelle konstruiert, den erforderlichen Zustand besser vermittelt.

Häufige Fehler und sicherere Alternativen

Setzen Sie „leerer Rumpf“ nicht mit „alle Member erhalten Standardwerte“ gleich. Ein Skalar ohne Initialisierer kann bei der Default-Initialisierung einen unbestimmten Wert behalten. Geben Sie ihm einen In-Class-Initializer, initialisieren Sie ihn in der Member-Initialisierungsliste oder verlangen Sie den Wert über Konstruktorparameter.

Nehmen Sie nicht an, dass nach der Deklaration eines Konstruktors mit Parametern auch ein Konstruktor ohne Argumente verfügbar ist. Werden beide Formen benötigt, deklarieren Sie beide oder definieren Sie die parameterlose Form ausdrücklich mit = default, sofern ihr erzeugtes Verhalten geeignet ist. Welche Konstruktoren verfügbar sind, hängt von den Deklarationen und den Sprachregeln ab, nicht von der Reihenfolge, in der sie in der Klasse stehen.

Vermeiden Sie schließlich, ein Objekt in einem unvollständigen Zustand zu erzeugen, nur um es später mit Settern zu vervollständigen. Benötigt die Klasse bestimmte Daten, übergeben Sie diese dem Konstruktor und stellen Sie dort die Invariante her. Behalten Sie einen Konstruktor mit leerem Rumpf nur dann bei, wenn sein wirkungsloser Rumpf beabsichtigt ist und die Member-Deklarationen selbst einen gültigen Anfangszustand bereitstellen.