Gdy typ klasy jest agregacją, możemy użyć inicjalizacji agregacji do bezpośredniej inicjalizacji typu klasy:
struct Foo // Foo is an aggregate
{
int x {};
int y {};
};
int main()
{
Foo foo { 6, 7 }; // uses aggregate initialization
return 0;
}Inicjalizacja agregacji polega na inicjalizacji elementów (elementy są inicjowane w kolejności, w jakiej są zdefiniowane). Zatem kiedy w powyższym przykładzie zostanie utworzona instancja foo , foo.x jest inicjowany do 6, I foo.y jest inicjowany do 7.
Powiązana treść
Omawiamy definicję agregacji i inicjalizacji agregacji na lekcji 13.8 -- Inicjowanie agregatu Struct.
Jednak gdy tylko uczynimy jakiekolwiek zmienne składowe prywatnymi (aby ukryć nasze dane), nasz typ klasy nie jest już agregatem (ponieważ agregaty nie mogą mieć prywatnych elementów). A to oznacza, że nie możemy już używać inicjalizacji agregowanej:
class Foo // Foo is not an aggregate (has private members)
{
int m_x {};
int m_y {};
};
int main()
{
Foo foo { 6, 7 }; // compile error: can not use aggregate initialization
return 0;
}Zakaz inicjowania typów klas zawierających elementy prywatne poprzez inicjalizację agregowaną ma sens z wielu powodów:
- Inicjalizacja agregowana wymaga wiedzy na temat implementacji klasy (ponieważ musisz wiedzieć, jakie są jej elementy i w jakiej kolejności zostały zdefiniowane), czego celowo staramy się unikać, ukrywając nasze elementy danych.
- Jeśli nasza klasa gdyby istniał jakiś niezmiennik, polegalibyśmy na tym, że użytkownik zainicjalizuje klasę w sposób, który zachowa niezmiennik.
Jak zatem zainicjować klasę ze zmiennymi prywatnymi? Komunikat o błędzie podany przez kompilator dla poprzedniego przykładu stanowi wskazówkę: „błąd: brak odpowiedniego konstruktora do inicjalizacji 'Foo'”
Musimy potrzebować pasującego konstruktora. Ale co to do cholery jest?
Konstruktorzy
A constructor to specjalna funkcja składowa, która jest wywoływana automatycznie po utworzeniu obiektu typu klasy niezagregowanej.
Gdy zdefiniowany jest obiekt typu klasy niezagregowanej, kompilator sprawdza, czy może znaleźć dostępny konstruktor pasujący do wartości inicjujących dostarczonych przez obiekt wywołujący (jeśli istnieje).
- Jeśli dostępny jest dostępny konstruktor zostanie znaleziony pasujący konstruktor, przydzielona zostanie pamięć dla obiektu, a następnie wywołana zostanie funkcja konstruktora.
- Jeśli nie zostanie znaleziony żaden dostępny pasujący konstruktor, zostanie wygenerowany błąd kompilacji.
Kluczowa informacja
Wielu nowych programistów nie jest zdezorientowanych co do tego, czy konstruktory tworzą obiekty, czy nie. Tak się nie dzieje — kompilator ustawia alokację pamięci dla obiektu przed wywołaniem konstruktora. Następnie na niezainicjowanym obiekcie wywoływany jest konstruktor.
Jeśli jednak dla zestawu inicjatorów nie zostanie znaleziony pasujący konstruktor, kompilator wystąpi błąd. Zatem chociaż konstruktory nie tworzą obiektów, brak odpowiedniego konstruktora uniemożliwi utworzenie obiektu.
Oprócz określenia sposobu utworzenia obiektu, konstruktory zazwyczaj wykonują dwie funkcje:
- Zazwyczaj dokonują inicjalizacji dowolnych zmiennych składowych (poprzez listę inicjalizacji składowych)
- Mogą wykonywać inne funkcje konfiguracyjne (poprzez instrukcje w treści konstruktora). Może to obejmować takie rzeczy, jak sprawdzanie wartości inicjalizacji błędów, otwieranie pliku lub bazy danych itp...
Gdy konstruktor zakończy wykonywanie, mówimy, że obiekt został „skonstruowany” i obiekt powinien teraz znajdować się w spójnym, użytecznym stanie.
Pamiętaj, że agregaty nie mogą mieć konstruktorów -- więc jeśli dodasz konstruktor do agregatu, nie będzie on już dłużej agregat.
Nazywanie konstruktorów
W odróżnieniu od normalnych funkcji składowych, konstruktory mają określone zasady dotyczące nadawania im nazw:
- Konstruktory muszą mieć taką samą nazwę jak klasa (z tą samą wielkością liter). W przypadku klas szablonowych ta nazwa wyklucza parametry szablonu.
- Konstruktory nie mają typu zwracanego (nawet
void).
Ponieważ konstruktory są zazwyczaj częścią interfejsu Twojej klasy, zwykle są publiczne.
Przykład podstawowego konstruktora
Dodajmy podstawowy konstruktor do naszego powyższego przykładu:
#include <iostream>
class Foo
{
private:
int m_x {};
int m_y {};
public:
Foo(int x, int y) // here's our constructor function that takes two initializers
{
std::cout << "Foo(" << x << ", " << y << ") constructed\n";
}
void print() const
{
std::cout << "Foo(" << m_x << ", " << m_y << ")\n";
}
};
int main()
{
Foo foo{ 6, 7 }; // calls Foo(int, int) constructor
foo.print();
return 0;
}Ten program się teraz skompiluje i wygeneruje wynik:
Foo(6, 7) constructed Foo(0, 0)
Gdy kompilator zobaczy definicję Foo foo{ 6, 7 }, szuka pasującego Foo konstruktora, który zaakceptuje dwa int argumentami. Foo(int, int) jest dopasowanie, więc kompilator pozwoli definicja.
W czasie wykonywania, gdy tworzona jest instancja foo , przydzielana jest pamięć dla foo , a następnie wywoływany jest konstruktor Foo(int, int) z parametrem x inicjalizowanym na 6 i parametrem y inicjalizowanym na 7. Następnie wykonuje się treść funkcji konstruktora i wypisuje Foo(6, 7) constructed.
Kiedy wywołamy funkcję składową print() , zauważysz, że elementy m_x i m_y mają wartość 0. Dzieje się tak, ponieważ chociaż nasza Foo(int, int) funkcja konstruktora została wywołana, w rzeczywistości nie zainicjowała ona elementów. Pokażemy, jak to zrobić w następnej lekcji.
Powiązana treść
Omawiamy różnice między używaniem kopiowania, inicjacji bezpośredniej i inicjalizacji listy do inicjowania obiektów za pomocą konstruktora na lekcji 14.15 — Inicjalizacja klasy i elizja kopiowania.
Niejawna konwersja argumentów przez konstruktora
W lekcji 10.1 -- Niejawna konwersja typów Zauważyliśmy, że kompilator wykona niejawną konwersję argumentów w wywołaniu funkcji (jeśli to konieczne) w celu dopasowania definicji funkcji, gdy parametry są różne type:
void foo(int, int)
{
}
int main()
{
foo('a', true); // will match foo(int, int)
return 0;
}Nie inaczej jest w przypadku konstruktorów: konstruktor Foo(int, int) dopasuje każde wywołanie, którego argumenty można w sposób dorozumiany konwertować na int:
class Foo
{
public:
Foo(int x, int y)
{
}
};
int main()
{
Foo foo{ 'a', true }; // will match Foo(int, int) constructor
return 0;
}Konstruktory nie powinny być const
Konstruktor musi mieć możliwość zainicjowania konstruowanego obiektu - dlatego konstruktor nie może być stały.
#include <iostream>
class Something
{
private:
int m_x{};
public:
Something() // constructors must be non-const
{
m_x = 5; // okay to modify members in non-const constructor
}
int getX() const { return m_x; } // const
};
int main()
{
const Something s{}; // const object, implicitly invokes (non-const) constructor
std::cout << s.getX(); // prints 5
return 0;
}Zwykle funkcja składowa niebędąca stałą nie można wywołać obiektu stałego. Jednak standard C++ wyraźnie stwierdza (per class.ctor.general#5), że const nie ma zastosowania do obiektu w budowie i zaczyna obowiązywać dopiero po zakończeniu konstruktora.
Konstruktory a setery
Konstruktory są zaprojektowane tak, aby inicjować cały obiekt w momencie jego utworzenia. Settery służą do przypisania wartości pojedynczemu elementowi istniejącego obiektu.

