Nie jestem skłonny używać list inicjujących członków z moimi konstruktorami ... ale już dawno zapomniałem o przyczynach tego ...
Czy używasz list inicjujących członków w swoich konstruktorach? Jeśli tak, to dlaczego? Jeśli nie, dlaczego nie?
Nie jestem skłonny używać list inicjujących członków z moimi konstruktorami ... ale już dawno zapomniałem o przyczynach tego ...
Czy używasz list inicjujących członków w swoich konstruktorach? Jeśli tak, to dlaczego? Jeśli nie, dlaczego nie?
Odpowiedzi:
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 B
wywoła domyślny konstruktor dla A
, a następnie zainicjuje a.x
na 3. Lepszym sposobem byłoby, aby B
konstruktor bezpośrednio wywołał A
konstruktor 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 A
to 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 const
zmienną 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;
};
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.
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ą.
Inicjalizacja podobiektów, które mają sparametryzowane konstruktory
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.
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.
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.
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.
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.
// 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;
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:
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];
}
};
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, tworzone są Sam_x i Sam_y . Następnie w treści konstruktora definiowane są te zmienne danych elementu.
Przypadków użycia:
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.
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)
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.
Jak wyjaśniono w Wytycznych podstawowych C ++ C.49: Preferowanie inicjalizacji zamiast przypisania w konstruktorach zapobiega niepotrzebnym wywołaniom domyślnych konstruktorów.