Ist es nicht ein Segen, dass man eine eigene neue Klasse auf sooo viele Arten Initialisieren kann?
struct Person {
string name_;
int alter_;
string ort_;
};
Nur 3 Membervariablen, aber 4 tolle Möglichkeiten zum Initialisieren:
Person otto {"Otto", 44, "Aachen"};
Person hans {"Hans", 33};
Person paul {"Otto"};
Person kurt {};
Weil wir die {}
hier verwenden werden alle Member initialisiert — die „überzähligen“ Wert-Initialisiert“. Also immer mit {}
initialisieren, bitteschön, denn Person xxx;
wird *nicht initialisiert. Iih-ba!
Aber ist das überhaupt gut so? Warum sollte man nur einige daten von Person
initialisieren wollen? Gut, ich kann mir Szenarien vorstellen, wo man mit der Wert-initialierung zufrieden ist, aber alle diese Möglichkeiten zulassen? Wenn der Datentyp so gedacht ist, dass man mit 0, 1, 2 oder 3 Parametern initialisieren kann, wenn das Sinn ergibt, dann kann ich auch die entsprechenden Konstruktoren dafür bereitstellen. Das ist dann expliziter, und der Leser der Datenstruktor muss nicht überlegen „ist das überhaupt so gedacht?“.
Initialisierung von Typen mit private Daten
Daher, warum nicht gleich private Daten haben? Das verhindert nämlich auch die Wert-Initialisierung. Hier kommt dann höchstens die Null-Initialisierung ins Spiel. Aber der Name ist egal, wir kennen alle den Effekt:
„Wenn Sie keinen Konstruktor selbst definieren, dann erzeugt der Compiler den Default-Konstruktor.“
Dankeschön. Und was bedeutet das? Man hört, der macht gar nichts. Ja, fast. Der macht (manchmal) nichts, aber vorher, da passiert das Interessante.
Aber sehen wir uns ein Beispiel an.
class Rect {
int area_; // private Daten
public:
int x_, y_;
void set(int x, int y) { x_=x; y_=y; area_=x_*y_; }
int calc() { return area_; }
};
Da Sie von außen nicht mehr direkt auf diese Membervariable zugreifen dürfen, können Sie sie auch nicht mehr einfach mit geschweiften Klammern {...}
befüllen. Sie können nicht Rect r{1,2};
oder Rect s{6,2,3};
— durch das Packen von area_
in den privaten Bereich der Klasse ist Wert-Initialisierung für Rect
unmöglich.
Sie benötigen also auf jeden Fall einen Konstruktor. Wir akzeptieren niemals, dass etwas nach der Definition uninitialisiert herumliegt! Das sieht der Compiler zum Glück auch so und daher generiert in diesen Fällen einen Konstruktor, nämlich den ohne Parameter. Dadurch können (und sollten) Sie zumindest Rect t{};
verwenden.
Und was macht dieser generierte Konstruktor? Effektiv initialisiert er Ihr Objekt mit Nullen, bzw. bei Membervariablen komplexerem Typs einem passenden Äquivalent.
Durch so viel Automatismen muss man erst einmal durchsteigen. Denken Sie auch an die Leser Ihres Programms und liefern Sie auch einen Konstruktor mit, und sei es nur der Konstruktor ohne Parameter. Andererseits, wenn der ohnehin nur tut, was der Compiler auch generieren würde, dann können Sie dessen Erzeugnis mit =default
anfordern — dann haben Sie es wenigstens explizit gemacht und der Leser sieht direkt >>achja, Initialisierung ohne Parameter.<<.
class Rect {
int area_; // private Daten
public:
int x_, y_;
void set(int x, int y) { x_=x; y_=y; area_=x_*y_; }
int calc() { return area_; }
Rect() = default; // Compiler Konstruktor generieren lassen
};
class Pow {
int result_; // private Daten. hält 'base' hoch 'exp'.
public:
int base_, exp_;
void set(int b, int e) { /* ... */ }
int calc() { return result_; }
Pow() : result_{1} {} // base_, exp_ wurden 0, dann muss result_=1 werden.
};
Während Rect() = default;
nur die Null-Initialisierung anfordert, die der Compiler sowieso eingesetzt hätte, macht der Konstruktor Pow()
tatsächlich etwas: Da base_
und exp_
Null-Initialisiert wurden, muss result_
auf 1
gesetzt werden, denn 0^0=1.
Ich denke, Sie machen einen Fehler. Laut 12.1/6 hat ein vom Compiler erzeugter Konstruktor die gleiche Wirkung wie einer benutzergeschriebene Konstruktor ohne Mitglieds-initialisierungsliste. Ein Konstruktor dieses Typs initialisiert keine Mitgliedsdaten, die Basistypen haben. Das bedeutet, dass in diesem Code,
void someFunc()
{
Pow p;
…
}
p.base_ und p.exp_ keine festgelegten Werten haben.
Eine Ausnahme gilt für Objekte, die statische Speicherdauer haben. Dank 3.6.2/2 werden sie null-initialisiert, bevor der Konstruktor ausführt. Also wenn wir
Pow p;
als eine globale Variable haben, können wir uns in diesem Fall darauf verlassen, dass sowohl p.base_ als auch p.exp_ null sind.
Oder irre ich mich?
Das stimmt, `Pow p;` wird teilweise nicht initialisiert sein. Mit `Pow p{};` erhält man jedoch zuvor eine „Zero-Initialization“. Aber Sie haben recht, ich habe das nicht deutlich genug hervorgehoben und muss die Formulierung nochmal überdenken.
Ja, bei gobalen Objekten sehen die Dinge wieder anders aus. Aus dem Kopf würde ich auch sagen, dass die Zero-Initialisiert werden, bevor der (eigene oder compilergenerierte) Konstruktor aufgerufen wird.