Dlaczego wolę używać listy inicjowania członków?

Odpowiedzi:

278

Dla członków klasy POD nie ma znaczenia, to tylko kwestia stylu. W przypadku członków klasy, które są klasami, unika to niepotrzebnego wywołania domyślnego konstruktora. Rozważać:

class A
{
public:
    A() { x = 0; }
    A(int x_) { x = x_; }
    int x;
};

class B
{
public:
    B()
    {
        a.x = 3;
    }
private:
    A a;
};

W tym przypadku konstruktor Bwywoła domyślny konstruktor dla A, a następnie zainicjuje a.xna 3. Lepszym sposobem byłoby, aby Bkonstruktor bezpośrednio wywołał Akonstruktor na liście inicjalizatora:

B()
  : a(3)
{
}

Ten nazwałbym tylko A„s A(int)konstruktora, a nie jego konstruktora domyślnego. W tym przykładzie różnica jest nieistotna, ale wyobraź sobie, że Ato domyślny konstruktor zrobił więcej, na przykład przydzielając pamięć lub otwierając pliki. Nie chciałbyś tego robić niepotrzebnie.

Ponadto, jeśli klasa nie ma domyślnego konstruktora lub masz constzmienną składową, musisz użyć listy inicjalizującej:

class A
{
public:
    A(int x_) { x = x_; }
    int x;
};

class B
{
public:
    B() : a(3), y(2)  // 'a' and 'y' MUST be initialized in an initializer list;
    {                 // it is an error not to do so
    }
private:
    A a;
    const int y;
};
Adam Rosenfield
źródło
5
obowiązkowa jest także ważna sprawa referencyjna
4pie0
5
Dlaczego nie użyć „a (3);” lub „a = A (3);” w treści domyślnego konstruktora B?
Siergiej
1
Czy możesz wyjaśnić, co masz na myśli mówiąc POD?
Jonas Stein,
2
@JonasStein POD jest dobrze zdefiniowanym zestawem reguł dotyczących prostych struktur danych (a nie kompletnych klas). Przeczytaj FAQ, aby uzyskać więcej informacji: stackoverflow.com/questions/146452/what-are-pod-types-in-c
monkey0506
2
@Sergey, domyślny konstruktor A nadal będzie wywoływany.
Vassilis
44

Poza wymienionymi powyżej przyczynami wydajności, jeśli twoja klasa przechowuje odniesienia do obiektów przekazanych jako parametry konstruktora lub twoja klasa ma zmienne const, nie masz innego wyboru, jak tylko użycie list inicjalizujących.

Naveen
źródło
7
To samo dotyczy stałych członków.
Richard Corden
tak, nie można użyć przypisania do zmodyfikowania zmiennych const, dlatego należy je zainicjować.
Hareen Laks
23
  1. Inicjalizacja klasy podstawowej

Jednym z ważnych powodów używania listy inicjalizacyjnej konstruktora, o której nie wspomniano w odpowiedziach tutaj, jest inicjalizacja klasy podstawowej.

Zgodnie z kolejnością budowy klasa podstawowa powinna zostać zbudowana przed klasą potomną. Bez listy inicjalizacyjnej konstruktora jest to możliwe, jeśli klasa podstawowa ma domyślny konstruktor, który zostanie wywołany tuż przed wejściem do konstruktora klasy potomnej.

Ale jeśli twoja klasa podstawowa ma tylko sparametryzowany konstruktor, musisz użyć listy inicjalizującej konstruktora, aby upewnić się, że twoja klasa podstawowa została zainicjowana przed klasą potomną.

  1. Inicjalizacja podobiektów, które mają sparametryzowane konstruktory

  2. Wydajność

Korzystając z listy inicjalizacyjnej konstruktora, inicjujesz członków danych dokładnie do stanu, którego potrzebujesz w kodzie, zamiast najpierw inicjować ich do stanu domyślnego, a następnie zmieniać ich stan na potrzebny w kodzie.

  1. Inicjowanie elementów stałych danych stałych

Jeśli elementy stałej danych w postaci niestatycznej w twojej klasie mają domyślne konstruktory i nie używasz listy inicjalizującej konstruktora, nie będziesz w stanie zainicjować ich do zamierzonego stanu, ponieważ zostaną one zainicjowane do stanu domyślnego.

  1. Inicjalizacja członków danych referencyjnych

Członkowie danych referencyjnych muszą zostać zainicjalizowani, gdy kompilator wchodzi do konstruktora, ponieważ referencje nie mogą być po prostu deklarowane i inicjowane później. Jest to możliwe tylko z listą inicjatora konstruktora.

yuvi
źródło
10

Oprócz problemów z wydajnością jest jeszcze jedna bardzo ważna, którą nazwałbym utrzymywalnością i rozszerzalnością kodu.

Jeśli T jest POD i zaczynasz preferować listę inicjującą, to jeśli raz T zmieni się na typ inny niż POD, nie będziesz musiał nic zmieniać wokół inicjalizacji, aby uniknąć niepotrzebnych wywołań konstruktora, ponieważ jest już zoptymalizowany.

Jeśli typ T ma domyślny konstruktor i co najmniej jeden konstruktor zdefiniowany przez użytkownika i jeden raz zdecydujesz się usunąć lub ukryć domyślny konstruktor, to jeśli użyto listy inicjalizacyjnej, nie musisz aktualizować kodu, jeśli konstruktory zdefiniowane przez użytkownika, ponieważ są już poprawnie wdrożone.

To samo z stałymi elementami lub elementami odniesienia, powiedzmy początkowo T jest zdefiniowane następująco:

struct T
{
    T() { a = 5; }
private:
    int a;
};

Następnie decydujesz się zakwalifikować jako const, jeśli użyjesz listy inicjalizacyjnej od początku, to była to zmiana w jednej linii, ale mając T zdefiniowaną jak wyżej, musisz również wykopać definicję konstruktora, aby usunąć przypisanie:

struct T
{
    T() : a(5) {} // 2. that requires changes here too
private:
    const int a; // 1. one line change
};

Nie jest tajemnicą, że konserwacja jest znacznie łatwiejsza i mniej podatna na błędy, jeśli kod nie został napisany przez „małpę kodu”, ale przez inżyniera, który podejmuje decyzje w oparciu o głębsze przemyślenia na temat tego, co robi.

mloskot
źródło
5

Przed uruchomieniem treści konstruktora wywoływane są wszystkie konstruktory dla jego klasy nadrzędnej, a następnie dla jej pól. Domyślnie wywoływane są konstruktory bez argumentów. Listy inicjujące pozwalają wybrać, który konstruktor ma być wywoływany i jakie argumenty otrzymuje ten konstruktor.

Jeśli masz odwołanie lub pole stałej, lub jeśli jedna z używanych klas nie ma domyślnego konstruktora, musisz użyć listy inicjalizacji.

Jamal Zafar
źródło
2
// Without Initializer List
class MyClass {
    Type variable;
public:
    MyClass(Type a) {  // Assume that Type is an already
                     // declared class and it has appropriate 
                     // constructors and operators
        variable = a;
    }
};

Tutaj kompilator wykonuje następujące kroki, aby utworzyć obiekt typu MyClass
1. Konstruktor typu jest wywoływany jako pierwszy dla „a”.
2. Operator przypisania „Typu” jest wywoływany w treści konstruktora MyClass () w celu przypisania

variable = a;
  1. A potem w końcu destruktor „Typu” jest nazywany „a”, ponieważ wychodzi poza zakres.

    Teraz rozważ ten sam kod z konstruktorem MyClass () z listą inicjującą

    // With Initializer List
     class MyClass {
    Type variable;
    public:
    MyClass(Type a):variable(a) {   // Assume that Type is an already
                     // declared class and it has appropriate
                     // constructors and operators
    }
    };

    W przypadku listy inicjalizującej kompilator wykonuje następujące kroki:

    1. Wywoływany jest konstruktor klasy „Type” w celu inicjalizacji: zmienna (a). Argumenty na liście inicjalizacyjnej służą do bezpośredniego kopiowania konstrukcji „zmiennej”.
    2. Niszczyciel „Typu” jest nazywany „a”, ponieważ wykracza poza zakres.
Rahul Singh
źródło
2
Ten fragment kodu może rozwiązać pytanie, ale wyjaśnienie kodu naprawdę pomaga poprawić jakość posta. Pamiętaj, że w przyszłości odpowiadasz na pytanie dla czytelników, a ci ludzie mogą nie znać przyczyn Twojej sugestii kodu. Staraj się również nie tłoczyć kodu objaśniającymi komentarzami, co zmniejsza czytelność zarówno kodu, jak i objaśnień! meta.stackexchange.com/q/114762/308249
davejal
2
Napisz własne porozumienie lub po prostu udostępnij link do oryginalnego źródła (tutaj, geeksforgeeks.com) zamiast kopiować i wklejać.
yuvi,
1

Wystarczy dodać dodatkowe informacje, aby wykazać, jak dużą różnicę może sprawić lista inicjująca członków . W leetcode 303 Zapytanie o sumę zakresu - niezmienne, https://leetcode.com/problems/range-sum-query-immutable/ , gdzie musisz zbudować i zainicjować, aby wyzerować wektor o określonym rozmiarze. Oto dwie różne implementacje i porównanie prędkości.

Bez listy inicjującej członków , uzyskanie AC kosztowało mnie około 212 ms .

class NumArray {
public:
vector<int> preSum;
NumArray(vector<int> nums) {
    preSum = vector<int>(nums.size()+1, 0);
    int ps = 0;
    for (int i = 0; i < nums.size(); i++)
    {
        ps += nums[i];
        preSum[i+1] = ps;
    }
}

int sumRange(int i, int j) {
    return preSum[j+1] - preSum[i];
}
};

Teraz przy użyciu listy inicjującej członka czas uzyskania AC wynosi około 108 ms . W tym prostym przykładzie oczywiste jest, że lista inicjująca członków jest znacznie bardziej wydajna . Cały pomiar pochodzi z czasu pracy z LC.

class NumArray {
public:
vector<int> preSum;
NumArray(vector<int> nums) : preSum(nums.size()+1, 0) { 
    int ps = 0;
    for (int i = 0; i < nums.size(); i++)
    {
        ps += nums[i];
        preSum[i+1] = ps;
    }
}

int sumRange(int i, int j) {
    return preSum[j+1] - preSum[i];
}
};
Yi Wang
źródło
0

Składnia:

  class Sample
  {
     public:
         int Sam_x;
         int Sam_y;

     Sample(): Sam_x(1), Sam_y(2)     /* Classname: Initialization List */
     {
           // Constructor body
     }
  };

Konieczność listy inicjalizacji:

 class Sample
 {
     public:
         int Sam_x;
         int Sam_y;

     Sample()     */* Object and variables are created - i.e.:declaration of variables */*
     { // Constructor body starts 

         Sam_x = 1;      */* Defining a value to the variable */* 
         Sam_y = 2;

     } // Constructor body ends
  };

w powyższym programie, gdy konstruktor klasy jest wykonywany, tworzoneSam_x i Sam_y . Następnie w treści konstruktora definiowane są te zmienne danych elementu.

Przypadków użycia:

  1. Zmienne stałe i odniesienia w klasie

W C zmienne należy zdefiniować podczas tworzenia. w ten sam sposób w C ++ musimy zainicjować zmienną Const i Reference podczas tworzenia obiektu za pomocą listy Inicjalizacja. jeśli wykonamy inicjalizację po utworzeniu obiektu (wewnątrz ciała konstruktora), otrzymamy błąd czasu kompilacji.

  1. Obiekty członkowskie klasy Sample1 (podstawowej), które nie mają domyślnego konstruktora

     class Sample1 
     {
         int i;
         public:
         Sample1 (int temp)
         {
            i = temp;
         }
     };
    
      // Class Sample2 contains object of Sample1 
     class Sample2
     {
      Sample1  a;
      public:
      Sample2 (int x): a(x)      /* Initializer list must be used */
      {
    
      }
     };

Podczas tworzenia obiektu dla klasy pochodnej, która wewnętrznie wywoła konstruktor klasy pochodnej i wywołuje konstruktor klasy bazowej (domyślnie). jeśli klasa podstawowa nie ma domyślnego konstruktora, użytkownik otrzyma błąd czasu kompilacji. Aby tego uniknąć, musimy mieć jedno z nich

 1. Default constructor of Sample1 class
 2. Initialization list in Sample2 class which will call the parametric constructor of Sample1 class (as per above program)
  1. Nazwa parametru konstruktora klasy i element danych klasy są takie same:

     class Sample3 {
        int i;         /* Member variable name : i */  
        public:
        Sample3 (int i)    /* Local variable name : i */ 
        {
            i = i;
            print(i);   /* Local variable: Prints the correct value which we passed in constructor */
        }
        int getI() const 
        { 
             print(i);    /*global variable: Garbage value is assigned to i. the expected value should be which we passed in constructor*/
             return i; 
        }
     };

Jak wszyscy wiemy, zmienna lokalna ma najwyższy priorytet niż zmienna globalna, jeśli obie zmienne mają tę samą nazwę. W takim przypadku program bierze pod uwagę wartość „i” {zmienną zarówno po lewej, jak i po prawej stronie. tj .: i = i} jako zmienna lokalna w konstruktorze Sample3 () i zmienna elementu klasy (i) została zastąpiona. Aby tego uniknąć, musimy użyć jednego z nich

  1. Initialization list 
  2. this operator.
Eswaran Pandi
źródło