Wie werden C ++ - Klassenmitglieder initialisiert, wenn ich es nicht explizit mache?

158

Angenommen , ich habe eine Klasse mit eigenem memebers ptr, name, pname, rname, crnameund age. Was passiert, wenn ich sie nicht selbst initialisiere? Hier ist ein Beispiel:

class Example {
    private:
        int *ptr;
        string name;
        string *pname;
        string &rname;
        const string &crname;
        int age;

    public:
        Example() {}
};

Und dann mache ich:

int main() {
    Example ex;
}

Wie werden die Mitglieder in ex initialisiert? Was passiert mit Zeigern? Haben stringund interhalten 0-intialized mit Standardkonstruktoren string()und int()? Was ist mit dem Referenzmitglied? Was ist auch mit const Referenzen?

Was sollte ich sonst noch wissen?

Kennt jemand ein Tutorial, das diese Fälle abdeckt? Vielleicht in einigen Büchern? Ich habe in der Universitätsbibliothek Zugang zu vielen C ++ - Büchern.

Ich würde es gerne lernen, damit ich bessere (fehlerfreie) Programme schreiben kann. Jedes Feedback würde helfen!

bodacydo
quelle
3
Buchempfehlungen finden Sie unter stackoverflow.com/questions/388242/…
Mike Seymour
Mike, nun, ich meine Kapitel aus einem Buch, das es erklärt. Nicht das ganze Buch! :)
bodacydo
Es wäre jedoch wahrscheinlich eine gute Idee, ein ganzes Buch über eine Sprache zu lesen, in der Sie programmieren möchten. Und wenn Sie bereits eines gelesen haben und es dies nicht erklärt hat, dann war es kein sehr gutes Buch.
Tyler McHenry
2
Scott Meyers (ein beliebter Ex-Profi-C ++ - Ratgeber) erklärt in Effective C ++ : "Die Regeln sind kompliziert - meiner Meinung nach zu kompliziert, um sie auswendig zu lernen. Stellen Sie sicher, dass alle Konstruktoren alles im Objekt initialisieren." So in seiner Meinung, der einfachste Weg , um (Versuch) schreiben „fehlerfrei“ Code ist nicht zu versuchen , die Regeln auswendig zu lernen (und in der Tat hat er nicht legt sie in dem Buch heraus), aber explizit initialize alles. Beachten Sie jedoch, dass Sie, selbst wenn Sie diesen Ansatz in Ihrem eigenen Code verwenden, möglicherweise an Projekten arbeiten, die von Personen geschrieben wurden, die dies nicht tun. Daher sind die Regeln möglicherweise immer noch wertvoll.
Kyle Strand
2
@ TylerMcHenry Welche Bücher über C ++ halten Sie für "gut"? Ich habe mehrere Bücher über C ++ gelesen, aber keines von ihnen hat dies vollständig erklärt. Wie in meinem vorherigen Kommentar erwähnt, lehnt Scott Meyers es ausdrücklich ab , die vollständigen Regeln in Effective C ++ bereitzustellen . Ich habe auch Meyers ' Effective Modern C ++ , Dewhursts C ++ Common Knowledge und Stroustrups A Tour of C ++ gelesen . Zu meiner Erinnerung erklärte keiner von ihnen die vollständigen Regeln. Natürlich hätte ich den Standard lesen können, aber ich würde das kaum als "gutes Buch" betrachten! : D Und ich gehe davon aus, dass Stroustrup es wahrscheinlich in der Programmiersprache C ++ erklärt .
Kyle Strand

Antworten:

206

Anstelle einer expliziten Initialisierung funktioniert die Initialisierung von Mitgliedern in Klassen identisch mit der Initialisierung lokaler Variablen in Funktionen.

Für Objekte wird ihr Standardkonstruktor aufgerufen. Zum Beispiel setzt std::stringder Standardkonstruktor eine leere Zeichenfolge. Wenn die Klasse des Objekts keinen Standardkonstruktor hat, handelt es sich um einen Kompilierungsfehler, wenn Sie ihn nicht explizit initialisieren.

Für primitive Typen (Zeiger, Ints usw.) werden sie nicht initialisiert - sie enthalten den beliebigen Junk, der sich zuvor an diesem Speicherort befand.

Für Referenzen (z. B. std::string&) ist es illegal , sie nicht zu initialisieren, und Ihr Compiler wird sich beschweren und sich weigern, solchen Code zu kompilieren. Referenzen müssen immer initialisiert werden.

In Ihrem speziellen Fall, wenn sie nicht explizit initialisiert werden:

    int *ptr;  // Contains junk
    string name;  // Empty string
    string *pname;  // Contains junk
    string &rname;  // Compile error
    const string &crname;  // Compile error
    int age;  // Contains junk
Tyler McHenry
quelle
4
+1. Es ist erwähnenswert, dass nach der strengen Standarddefinition Instanzen primitiver Typen zusammen mit einer Vielzahl anderer Dinge (jeder Speicherbereich) alle als Objekte betrachtet werden .
stinky472
7
"Wenn die Klasse des Objekts keinen Standardkonstruktor hat, ist dies ein Kompilierungsfehler, wenn Sie ihn nicht explizit initialisieren." Das ist falsch ! Wenn eine Klasse keinen Standardkonstruktor hat, erhält sie einen leeren Standardkonstruktor .
Zauberer
19
@wiz Ich denke, er meinte wörtlich "wenn das Objekt keinen Standardkonstruktor hat", wie auch in keinem generierten, was der Fall wäre, wenn die Klasse explizit andere Konstruktoren als den Standardkonstruktor definiert (es wird kein Standard-Ctor generiert). Wenn wir zu pedanatisch werden, werden wir wahrscheinlich mehr als nur Hilfe verwirren, und Tyler macht dies in seiner vorherigen Antwort auf mich deutlich.
stinky472
8
@ wiz-loz Ich würde sagen, foodas hat einen Konstruktor, es ist nur implizit. Aber das ist wirklich ein Argument der Semantik.
Tyler McHenry
4
Ich interpretiere "Standardkonstruktor" als einen Konstruktor, der ohne Argumente aufgerufen werden kann. Dies ist entweder eine, die Sie selbst definieren oder die implizit vom Compiler generiert wird. Das Fehlen bedeutet also weder von Ihnen selbst definiert noch erzeugt. Oder so sehe ich das.
5ound
28

Lassen Sie mich zunächst erklären, was eine Mem-Initialisierer-Liste ist. Eine mem-Initialisierer-Liste ist eine durch Kommas getrennte Liste von mem-Initialisierern , wobei jeder mem-Initialisierer ein Mitgliedsname ist (, gefolgt von einer Ausdrucksliste , gefolgt von a ). Die Ausdrucksliste gibt an, wie das Mitglied aufgebaut ist. Zum Beispiel in

static const char s_str[] = "bodacydo";
class Example
{
private:
    int *ptr;
    string name;
    string *pname;
    string &rname;
    const string &crname;
    int age;

public:
    Example()
        : name(s_str, s_str + 8), rname(name), crname(name), age(-4)
    {
    }
};

Die mem-initializer-Liste des vom Benutzer angegebenen Konstruktors ohne Argumente ist name(s_str, s_str + 8), rname(name), crname(name), age(-4). Diese mem-initializer-Liste bedeutet, dass das nameMember vom Konstruktor initialisiert wird, der std::stringzwei Eingabe-Iteratoren verwendet , das rnameMember mit einem Verweis auf initialisiert wird name, das crnameMember mit einem const-Verweis auf initialisiert namewird und das ageMember mit dem Wert initialisiert wird -4.

Jeder Konstruktor hat seine eigene mem-initializer-Liste , und Mitglieder können nur in einer vorgeschriebenen Reihenfolge initialisiert werden (im Grunde die Reihenfolge, in der die Mitglieder in der Klasse deklariert sind). So sind die Mitglieder Examplenur in der Reihenfolge initialisiert werden: ptr, name, pname, rname, crname, und age.

Wenn Sie keinen Mem-Initialisierer eines Mitglieds angeben , lautet der C ++ - Standard:

Wenn die Entität ein nicht statisches Datenelement ... vom Klassentyp ... ist, wird die Entität standardmäßig initialisiert (8.5). ... Andernfalls wird die Entität nicht initialisiert.

Da namees sich hierbei um ein nicht statisches Datenelement vom Klassentyp handelt, wird es standardmäßig initialisiert, wenn namein der mem-initializer-Liste kein Initialisierer für angegeben wurde . Alle anderen Mitglieder von Examplehaben keinen Klassentyp, daher werden sie nicht initialisiert.

Wenn der Standard besagt, dass sie nicht initialisiert sind, bedeutet dies, dass sie einen beliebigen Wert haben können . Da der obige Code nicht initialisiert wurde pname, kann es sich also um alles handeln.

Beachten Sie, dass Sie noch andere Regeln befolgen müssen, z. B. die Regel, dass Verweise immer initialisiert werden müssen. Es ist ein Compilerfehler, Referenzen nicht zu initialisieren.

Daniel Trebbien
quelle
Dies ist der beste Weg, um Mitglieder zu initialisieren, wenn Sie Deklaration (in .h) und Definition (in .cpp) streng trennen möchten, ohne zu viele Interna anzuzeigen.
Matthieu
11

Sie können Datenelemente auch an dem Punkt initialisieren, an dem Sie sie deklarieren:

class another_example{
public:
    another_example();
    ~another_example();
private:
    int m_iInteger=10;
    double m_dDouble=10.765;
};

Ich benutze dieses Formular so ziemlich ausschließlich, obwohl ich gelesen habe, dass einige Leute es als "schlechte Form" betrachten, vielleicht weil es erst kürzlich eingeführt wurde - ich denke in C ++ 11. Für mich ist es logischer.

Eine weitere nützliche Facette der neuen Regeln ist das Initialisieren von Datenelementen, die selbst Klassen sind. Angenommen, dies CDynamicStringist eine Klasse, die die Zeichenfolgenbehandlung kapselt. Es verfügt über einen Konstruktor, mit dem Sie den Anfangswert angeben können CDynamicString(wchat_t* pstrInitialString). Sie können diese Klasse sehr gut als Datenelement in einer anderen Klasse verwenden - beispielsweise in einer Klasse, die einen Windows-Registrierungswert kapselt, in dem in diesem Fall eine Postanschrift gespeichert ist. Um den Registrierungsschlüsselnamen, in den dies geschrieben wird, fest zu codieren, verwenden Sie geschweifte Klammern:

class Registry_Entry{
public:
    Registry_Entry();
    ~Registry_Entry();
    Commit();//Writes data to registry.
    Retrieve();//Reads data from registry;
private:
    CDynamicString m_cKeyName{L"Postal Address"};
    CDynamicString m_cAddress;
};

Beachten Sie, dass die zweite Zeichenfolgenklasse, die die tatsächliche Postanschrift enthält, keinen Initialisierer hat, sodass der Standardkonstruktor bei der Erstellung aufgerufen wird - möglicherweise wird er automatisch auf eine leere Zeichenfolge gesetzt.


quelle
9

Wenn Ihre Beispielklasse auf dem Stapel instanziiert wird, ist der Inhalt nicht initialisierter Skalarmitglieder zufällig und undefiniert.

Für eine globale Instanz werden nicht initialisierte Skalarmitglieder auf Null gesetzt.

Für Mitglieder, die selbst Instanzen von Klassen sind, werden ihre Standardkonstruktoren aufgerufen, sodass Ihr Zeichenfolgenobjekt initialisiert wird.

  • int *ptr; // nicht initialisierter Zeiger (oder auf Null gesetzt, wenn global)
  • string name; // Konstruktor aufgerufen, mit leerem String initialisiert
  • string *pname; // nicht initialisierter Zeiger (oder auf Null gesetzt, wenn global)
  • string &rname; // Kompilierungsfehler, wenn Sie dies nicht initialisieren können
  • const string &crname; // Kompilierungsfehler, wenn Sie dies nicht initialisieren können
  • int age; // Skalarwert, nicht initialisiert und zufällig (oder auf Null gesetzt, wenn global)
Paul Dixon
quelle
Ich habe experimentiert und es scheint, dass string namees nach dem Initialisieren der Klasse auf dem Stapel leer ist. Sind Sie sich Ihrer Antwort absolut sicher?
Bodacydo
1
Zeichenfolge wird einen Konstruktor haben, der standardmäßig eine leere Zeichenfolge bereitstellt - ich werde meine Antwort klarstellen
Paul Dixon
@bodacydo: Paul ist richtig, aber wenn Sie sich für dieses Verhalten interessieren, tut es nie weh, explizit zu sein. Wirf es in die Initialisiererliste.
Stephen
Vielen Dank für die Klarstellung und Erklärung!
Bodacydo
2
Es ist nicht zufällig! Zufall ist ein zu großes Wort dafür! Wenn Skalarmitglieder zufällig wären, würden wir keine anderen Zufallszahlengeneratoren benötigen. Stellen Sie sich ein Programm vor, das Datenreste analysiert - wie nicht wiederhergestellte Dateien im Speicher - die Daten sind alles andere als zufällig. Es ist nicht einmal undefiniert! Es ist normalerweise schwer zu definieren, da wir normalerweise nicht wissen, was unsere Maschine tut. Wenn diese "zufälligen Daten", die Sie gerade gelöscht haben, das einzige Bild Ihres Vaters sind, kann Ihre Mutter sie sogar als anstößig
empfinden,
5

Nicht initialisierte nicht statische Elemente enthalten zufällige Daten. Tatsächlich haben sie nur den Wert des Speicherorts, dem sie zugewiesen sind.

Natürlich stringkönnte der Konstruktor des Objekts für Objektparameter (wie ) eine Standardinitialisierung durchführen.

In Ihrem Beispiel:

int *ptr; // will point to a random memory location
string name; // empty string (due to string's default costructor)
string *pname; // will point to a random memory location
string &rname; // it would't compile
const string &crname; // it would't compile
int age; // random value
Magier
quelle
2

Bei Mitgliedern mit einem Konstruktor wird der Standardkonstruktor zur Initialisierung aufgerufen.

Sie können sich nicht auf den Inhalt der anderen Typen verlassen.

janm
quelle
0

Wenn es sich auf dem Stapel befindet, ist der Inhalt von nicht initialisierten Mitgliedern, die keinen eigenen Konstruktor haben, zufällig und undefiniert. Selbst wenn es global ist, wäre es eine schlechte Idee, sich darauf zu verlassen, dass sie auf Null gesetzt werden. Ob es sich auf dem Stapel befindet oder nicht, wenn ein Mitglied einen eigenen Konstruktor hat, wird dieser aufgerufen, um es zu initialisieren.

Wenn Sie also den String * pname haben, enthält der Zeiger zufälligen Junk. Für den Zeichenfolgennamen wird jedoch der Standardkonstruktor für die Zeichenfolge aufgerufen, sodass Sie eine leere Zeichenfolge erhalten. Für Ihre Referenztypvariablen bin ich mir nicht sicher, aber es wird wahrscheinlich eine Referenz auf einen zufälligen Speicherblock sein.

George
quelle
0

Es hängt davon ab, wie die Klasse aufgebaut ist

Die Beantwortung dieser Frage erfordert das Verständnis einer riesigen Switch-Case-Anweisung im C ++ - Sprachstandard, die für bloße Sterbliche schwer zu verstehen ist.

Als einfaches Beispiel dafür, wie schwierig die Dinge sind:

main.cpp

#include <cassert>

int main() {
    struct C { int i; };

    // This syntax is called "default initialization"
    C a;
    // i undefined

    // This syntax is called "value initialization"
    C b{};
    assert(b.i == 0);
}

Bei der Standardinitialisierung beginnen Sie mit: https://en.cppreference.com/w/cpp/language/default_initialization. Gehen Sie zum Teil "Die Auswirkungen der Standardinitialisierung sind" und starten Sie die case-Anweisung:

  • "wenn T ein Nicht-POD ist ": nein (die Definition von POD ist an sich eine riesige switch-Anweisung)
  • "wenn T ein Array-Typ ist": nein
  • "sonst wird nichts getan": daher bleibt ein undefinierter Wert übrig

Wenn sich jemand für eine Wertinitialisierung entscheidet, gehen wir zu https://en.cppreference.com/w/cpp/language/value_initialization "Die Auswirkungen der Wertinitialisierung sind" und starten die case-Anweisung:

  • "Wenn T ein Klassentyp ohne Standardkonstruktor oder mit einem vom Benutzer bereitgestellten oder gelöschten Standardkonstruktor ist": nicht der Fall. Sie werden jetzt 20 Minuten damit verbringen, diese Begriffe zu googeln:
    • Wir haben einen implizit definierten Standardkonstruktor (insbesondere, weil kein anderer Konstruktor definiert wurde).
    • es wird nicht vom Benutzer bereitgestellt (implizit definiert)
    • es wird nicht gelöscht ( = delete)
  • "Wenn T ein Klassentyp mit einem Standardkonstruktor ist, der weder vom Benutzer bereitgestellt noch gelöscht wird": Ja
    • "Das Objekt ist nullinitialisiert und wird dann standardinitialisiert, wenn es einen nicht trivialen Standardkonstruktor hat": kein nicht trivialer Konstruktor, nur nullinitialisiert. Die Definition von "Zero-Initialize" ist zumindest einfach und entspricht den Erwartungen: https://en.cppreference.com/w/cpp/language/zero_initialization

Aus diesem Grund empfehle ich dringend, dass Sie sich niemals auf eine "implizite" Nullinitialisierung verlassen. Wenn keine starken Leistungsgründe vorliegen, initialisieren Sie alles explizit, entweder auf dem Konstruktor, falls Sie einen definiert haben, oder verwenden Sie die aggregierte Initialisierung. Ansonsten machen Sie die Dinge für zukünftige Entwickler sehr, sehr riskant.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
quelle