W jaki sposób członkowie klasy C ++ są inicjowani, jeśli nie zrobię tego jawnie?


158

Załóżmy, że mam klasę z prywatnych memebers ptr, name, pname, rname, crnamei age. Co się stanie, jeśli sam ich nie zainicjuję? Oto przykład:

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

    public:
        Example() {}
};

A potem robię:

int main() {
    Example ex;
}

W jaki sposób członkowie są inicjowani w ex? Co się dzieje ze wskazówkami? Czy stringi intuzyskaj 0-zintializowany z domyślnymi konstruktorami string()i int()? A co z członkiem referencyjnym? A co z referencjami const?

O czym jeszcze powinienem wiedzieć?

Czy ktoś zna tutorial, który omawia te przypadki? Może w jakichś książkach? Mam dostęp w bibliotece uniwersytetu do wielu książek w języku C ++.

Chciałbym się tego nauczyć, żeby móc pisać lepsze (wolne od błędów) programy. Każda opinia byłaby pomocna!


3
Zalecenia dotyczące książek można znaleźć na stronie stackoverflow.com/questions/388242/ ...
Mike Seymour,

Mike, tak, mam na myśli rozdział z jakiejś książki, który to wyjaśnia. Nie cała książka! :)
bodacydo

Jednak prawdopodobnie dobrym pomysłem byłoby przeczytanie całej książki o języku, w którym zamierzasz programować. A jeśli już jedną przeczytałeś i nie wyjaśniła tego, to nie była to zbyt dobra książka.
Tyler McHenry

2
Scott Meyers (popularny guru doradztwa w języku C ++) stwierdza w Effective C ++ : „Zasady są skomplikowane - zbyt skomplikowane, aby były warte zapamiętania, moim zdaniem… upewnij się, że wszyscy konstruktorzy inicjalizują wszystko w obiekcie”. Zatem, jego zdaniem, najłatwiejszym sposobem (próba) napisania „wolnego od błędów” kodu nie jest próba zapamiętania reguł (a właściwie nie przedstawia ich w książce), ale jawne zainicjowanie wszystkiego. Zwróć jednak uwagę, że nawet jeśli zastosujesz to podejście we własnym kodzie, możesz pracować nad projektami napisanymi przez osoby, które tego nie robią, więc reguły mogą nadal być cenne.
Kyle Strand

2
@TylerMcHenry Jakie książki o C ++ uważasz za „dobre”? Przeczytałem kilka książek na temat C ++, ale żadna z nich nie wyjaśniła tego do końca. Jak zauważyłem w moim poprzednim komentarzu, Scott Meyers wyraźnie odmawia podania kompletnych reguł w efektywnym C ++ . Przeczytałem także książkę Meyersa Effective Modern C ++ , Common Knowledge C ++ Dewhursta i A Tour of C ++ Stroustrupa . Pamiętam, że żaden z nich nie wyjaśnił pełnych zasad. Oczywiście mogłem przeczytać standard, ale raczej nie uważałbym tego za „dobrą książkę”! : D I spodziewam się, że Stroustrup prawdopodobnie wyjaśnia to w języku programowania C ++ .
Kyle Strand

Odpowiedzi:


206

Zamiast jawnej inicjalizacji, inicjalizacja elementów członkowskich w klasach działa identycznie jak inicjalizacja zmiennych lokalnych w funkcjach.

W przypadku obiektów wywoływany jest ich domyślny konstruktor. Na przykład for std::string, domyślny konstruktor ustawia go na pusty ciąg. Jeśli klasa obiektu nie ma domyślnego konstruktora, będzie to błąd kompilacji, jeśli nie zostanie ona jawnie zainicjowana.

W przypadku typów pierwotnych (wskaźniki, ints itp.), Nie są one inicjalizowane - zawierają wszelkie arbitralne śmieci, które znajdowały się wcześniej w tej lokalizacji pamięci.

W przypadku odniesień (np. std::string&) Jest to nielegalne nie je zainicjować, a kompilator będzie narzekać i odmówić skompilować takiego kodu. Odwołania muszą być zawsze inicjowane.

Tak więc w twoim konkretnym przypadku, jeśli nie zostały jawnie zainicjowane:

    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

4
+1. Warto zauważyć, że zgodnie ze ścisłą standardową definicją, instancje typów pierwotnych wraz z różnymi innymi rzeczami (dowolny region przechowywania) są uważane za obiekty .
stinky472

7
„Jeśli klasa obiektu nie ma domyślnego konstruktora, będzie to błąd kompilacji, jeśli jej jawnie nie zainicjujesz” to źle ! Jeśli klasa nie ma domyślnego konstruktora, otrzymuje domyślny konstruktor domyślny, który jest pusty.
Wizard

19
@wiz Myślę, że dosłownie miał na myśli „jeśli obiekt nie ma domyślnego konstruktora”, tak jak w przypadku żadnego wygenerowanego konstruktora, co miałoby miejsce, gdyby klasa jawnie definiowała konstruktory inne niż domyślny (nie zostanie wygenerowany żaden domyślny konstruktor). Jeśli staniemy się zbyt pedanatyczni, prawdopodobnie zmylimy więcej niż pomoc, a Tyler dobrze o tym wspomina w swojej wcześniejszej odpowiedzi.
stinky472

8
@ wiz-loz Powiedziałbym, że fooma konstruktora, to po prostu domniemane. Ale to naprawdę argument semantyki.
Tyler McHenry

4
„Domyślny konstruktor” interpretuję jako konstruktor, który można wywołać bez argumentów. Byłby to albo taki, który sam zdefiniujesz, albo niejawnie wygenerowany przez kompilator. A więc jego brak oznacza, że ​​nie jest on zdefiniowany przez Ciebie ani generowany. Albo tak to widzę.
około

28

Najpierw wyjaśnię, czym jest lista inicjalizacji pamięci . Lista inicjalizatorów memów to rozdzielona przecinkami lista inicjalizatorów memów, gdzie każdy inicjator pamięci jest nazwą składową (, po której następuje lista-wyrażeń , po której następuje znak ). Lista wyrażeń to sposób konstruowania elementu członkowskiego. Na przykład w

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)
    {
    }
};

mem-inicjator-lista konstruktora dostarczone przez użytkownika, nie-argumentów name(s_str, s_str + 8), rname(name), crname(name), age(-4). Ten MEM inicjator wykazie oznacza, że nameelement jest inicjowane przez przez std::stringkonstruktora, które ma dwa iteratorami wejściowych The rnameelement jest inicjowane z odniesieniem do nameThe crnameelement jest inicjowany const odniesieniem do name, a ageelement jest inicjalizowany wartością -4.

Każdy konstruktor ma swoją własną listę inicjalizatorów memów , a elementy członkowskie mogą być inicjowane tylko w określonej kolejności (zasadniczo w kolejności, w której elementy członkowskie są zadeklarowane w klasie). W ten sposób członkowie Examplemożna zainicjować tylko w kolejności: ptr, name, pname, rname, crname, i age.

Jeśli nie określisz inicjalizatora pamięci elementu członkowskiego, standard C ++ mówi:

Jeśli jednostka jest niestatycznym składnikiem danych ... typu klasy ..., jednostka jest inicjowana domyślnie (8.5). ... W przeciwnym razie jednostka nie zostanie zainicjowana.

W tym przypadku, ponieważ namejest to niestatyczny element członkowski danych typu klasy, jest inicjowany domyślnie, jeśli nie nameokreślono inicjatora dla w mem-initializer-list . Wszyscy pozostali członkowie Examplenie mają typu klasy, więc nie są inicjowani.

Gdy standard mówi, że nie są one zainicjalizowane, oznacza to, że mogą mieć dowolną wartość. Tak więc, ponieważ powyższy kod nie został zainicjalizowany pname, może to być wszystko.

Zauważ, że nadal musisz przestrzegać innych reguł, takich jak reguła, że ​​odwołania zawsze muszą być inicjowane. Brak inicjalizacji odwołań jest błędem kompilatora.


Jest to najlepszy sposób na zainicjowanie członków, jeśli chcesz ściśle oddzielić deklarację (in .h) i definicję (in .cpp) bez pokazywania zbyt wielu elementów wewnętrznych.
Matthieu

12

Możesz również zainicjować członków danych w momencie ich zadeklarowania:

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

Używam tego formularza prawie wyłącznie, chociaż czytałem, że niektórzy uważają go za „złą formę”, być może dlatego, że został wprowadzony dopiero niedawno - myślę, że w C ++ 11. Dla mnie jest to bardziej logiczne.

Innym użytecznym aspektem nowych reguł jest inicjalizacja elementów członkowskich danych, które same są klasami. Załóżmy na przykład, że CDynamicStringjest to klasa, która hermetyzuje obsługę ciągów. Posiada konstruktor, który pozwala określić jego wartość początkową CDynamicString(wchat_t* pstrInitialString). Możesz bardzo dobrze użyć tej klasy jako elementu członkowskiego danych w innej klasie - powiedzmy, że klasa zawiera wartość rejestru systemu Windows, która w tym przypadku przechowuje adres pocztowy. Aby `` na stałe zakodować '' nazwę klucza rejestru, do którego to zapisuje, użyj nawiasów klamrowych:

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;
};

Zwróć uwagę, że druga klasa ciągu, która przechowuje rzeczywisty adres pocztowy, nie ma inicjatora, więc jej domyślny konstruktor zostanie wywołany podczas tworzenia - być może automatycznie ustawi go na pusty ciąg.


9

Jeśli przykładowa klasa jest tworzona na stosie, zawartość niezainicjowanych elementów skalarnych jest losowa i niezdefiniowana.

W przypadku wystąpienia globalnego niezainicjowane elementy skalarne zostaną wyzerowane.

W przypadku elementów członkowskich, które same są instancjami klas, zostaną wywołane ich domyślne konstruktory, więc obiekt ciągu zostanie zainicjowany.

  • int *ptr; // niezainicjowany wskaźnik (lub wyzerowany, jeśli jest globalny)
  • string name; // wywołany konstruktor, zainicjowany pustym łańcuchem
  • string *pname; // niezainicjowany wskaźnik (lub wyzerowany, jeśli jest globalny)
  • string &rname; // błąd kompilacji, jeśli nie uda się zainicjować tego
  • const string &crname; // błąd kompilacji, jeśli nie uda się zainicjować tego
  • int age; // wartość skalarna, niezainicjowana i losowa (lub zerowana, jeśli jest globalna)

Poeksperymentowałem i wydaje się, że string namepo zainicjowaniu klasy na stosie jest pusta. Czy jesteś absolutnie pewien swojej odpowiedzi?
bodacydo

1
string będzie miał konstruktora, który domyślnie podał pusty ciąg - wyjaśnię moją odpowiedź
Paul Dixon

@bodacydo: Paweł ma rację, ale jeśli zależy ci na tym zachowaniu, wyraźne wypowiedzi nigdy nie zaszkodzą. Wrzuć to na listę inicjatorów.
Stephen

Dzięki za wyjaśnienie i wyjaśnienie!
bodacydo

2
To nie jest przypadkowe! Losowość to za duże słowo na to! Gdyby elementy skalarne były losowe, nie potrzebowalibyśmy żadnych innych generatorów liczb losowych. Wyobraź sobie program, który analizuje „pozostawione” dane - jak przywracanie plików w pamięci - dane nie są przypadkowe. To nawet nie jest nieokreślone! Zwykle jest to trudne do zdefiniowania, ponieważ zwykle nie wiemy, co robi nasza maszyna. Jeśli te "losowe dane", które właśnie cofnąłeś, są jedynym obrazem twojego ojca, twoja matka może nawet uznać to za obraźliwe, jeśli powiesz, że jest przypadkowe ...
slyy2048

5

Niezainicjowane niestatyczne elementy członkowskie będą zawierać losowe dane. W rzeczywistości będą miały tylko wartość lokalizacji pamięci, do której są przypisane.

Oczywiście dla parametrów obiektu (takich jak string) konstruktor obiektu mógłby wykonać domyślną inicjalizację.

W twoim przykładzie:

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

2

Członkowie z konstruktorem będą mieli swój domyślny konstruktor wywołany do inicjalizacji.

Nie możesz polegać na zawartości innych typów.


0

Jeśli znajduje się na stosie, zawartość niezainicjowanych elementów członkowskich, które nie mają własnego konstruktora, będzie losowa i niezdefiniowana. Nawet jeśli jest globalny, byłoby złym pomysłem polegać na ich wyzerowaniu. Niezależnie od tego, czy jest na stosie, czy nie, jeśli element członkowski ma własny konstruktor, zostanie wywołany w celu jego zainicjowania.

Tak więc, jeśli masz ciąg * pname, wskaźnik będzie zawierał losowe śmieci. ale w przypadku nazwy łańcucha zostanie wywołany domyślny konstruktor dla łańcucha, który da ci pusty łańcuch. W przypadku zmiennych typu referencyjnego nie jestem pewien, ale prawdopodobnie będzie to odniesienie do jakiejś losowej porcji pamięci.


0

Zależy to od konstrukcji klasy

Odpowiedzią na to pytanie jest zrozumienie ogromnej instrukcji przełącznika w standardzie języka C ++, która jest trudna do zrozumienia dla zwykłych śmiertelników.

Jako prosty przykład tego, jak trudne są rzeczy:

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);
}

W przypadku domyślnej inicjalizacji należy zacząć od: https://en.cppreference.com/w/cpp/language/default_initialization przechodzimy do części „Efekty domyślnej inicjalizacji są” i rozpoczynamy instrukcję case:

  • „jeśli T jest inny niż POD ”: nie (definicja POD jest sama w sobie ogromną instrukcją przełączającą)
  • „jeśli T jest typem tablicy”: nie
  • „w przeciwnym razie nic nie zostanie zrobione”: dlatego pozostaje z nieokreśloną wartością

Następnie, jeśli ktoś zdecyduje się na inicjalizację wartości, przechodzimy do https://en.cppreference.com/w/cpp/language/value_initialization „Efekty inicjalizacji wartości są” i uruchamiamy instrukcję case:

  • „jeśli T jest typem klasy bez domyślnego konstruktora lub z dostarczonym przez użytkownika lub usuniętym konstruktorem domyślnym”: nie ma to miejsca. Spędzisz teraz 20 minut na szukaniu w Google tych terminów:
    • mamy domyślnie zdefiniowany konstruktor domyślny (w szczególności, ponieważ nie zdefiniowano żadnego innego konstruktora)
    • nie jest dostarczony przez użytkownika (domyślnie zdefiniowany)
    • nie jest usuwany ( = delete)
  • „jeśli T jest typem klasy z domyślnym konstruktorem, który nie jest dostarczony przez użytkownika ani usunięty”: tak
    • „obiekt jest inicjalizowany przez zero, a następnie inicjowany domyślnie, jeśli ma nietrywialny konstruktor domyślny”: brak nietrywialnego konstruktora, tylko inicjalizacja zerowa. Przynajmniej definicja „inicjalizacji zerowej” jest prosta i działa zgodnie z oczekiwaniami: https://en.cppreference.com/w/cpp/language/zero_initialization

Dlatego zdecydowanie zalecam, aby nigdy nie polegać na „niejawnej” inicjalizacji zera. O ile nie ma silnych powodów związanych z wydajnością, jawnie zainicjuj wszystko, albo w konstruktorze, jeśli został zdefiniowany, albo przy użyciu inicjalizacji agregacji. W przeciwnym razie sprawisz, że sytuacja będzie bardzo ryzykowna dla przyszłych programistów.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.