Menu

Konstruktory w C++: jak poprawnie inicjalizować obiekty

Konstruktor to specjalna metoda, która uruchamia się przy tworzeniu obiektu. Poznaj konstruktory domyślne, parametryczne i kopiujące, listy inicjalizacyjne oraz sposoby na to, by obiekty nie zostawały zainicjalizowane w połowie.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Czym jest konstruktor

Na poprzedniej stronie powstały klasy z polami. Pola świeżo utworzonego obiektu zawierają jednak śmieci, które akurat były w tej pamięci, dopóki ich nie ustawisz. Konstruktor to naprawia: to specjalna metoda, która uruchamia się automatycznie w chwili utworzenia obiektu, a jej jedynym zadaniem jest zostawienie obiektu w poprawnym, w pełni zainicjalizowanym stanie.

Konstruktor ma tę samą nazwę co klasa i nie ma typu zwracanego, nawet void. Nigdy nie wywołujesz go bezpośrednio; kompilator robi to za ciebie za każdym razem, gdy obiekt zaczyna istnieć.

Counter() bez parametrów to konstruktor domyślny: używany wtedy, gdy tworzysz obiekt bez przekazywania argumentów.

Konstruktory parametryczne

Konstruktor bez argumentów jest w porządku, ale zwykle chcesz utworzyć obiekt z konkretnymi wartościami. Konstruktor parametryczny przyjmuje argumenty i używa ich do inicjalizacji pól.

Klasa może mieć więcej niż jeden konstruktor, o ile ich listy parametrów się różnią: to zwykłe przeciążanie funkcji zastosowane do konstruktorów. Tutaj Point można utworzyć ze współrzędnymi albo bez nich:

Częsta pułapka: Point p(); nie tworzy obiektu. Kompilator odczytuje to jako deklarację funkcji o nazwie p, która zwraca Point. Aby wywołać konstruktor domyślny, napisz Point p; (bez nawiasów) albo Point p{}; z klamrami.

Listy inicjalizacyjne

Dotąd przykłady przypisywały wartości polom wewnątrz ciała konstruktora. Przy prostych typach to działa, ale to złe narzędzie. Zanim wykona się ciało, każde pole zostało już skonstruowane domyślnie; ciało wyrzuca ten wynik i przypisuje nową wartość. Lista inicjalizacyjna inicjalizuje każde pole bezpośrednio, przed ciałem, w jednym kroku.

Składnia to dwukropek po liście parametrów, a potem pary member(value):

W przypadku pola typu string pozwala to też uniknąć tworzenia pustego napisu, a potem przypisywania: lista inicjalizacyjna buduje go poprawnie za pierwszym razem.

Lista inicjalizacyjna to nie tylko optymalizacja; w trzech przypadkach jest obowiązkowa, bo ciało może tylko przypisywać, a nie inicjalizować:

  • Pola const: nie można przypisać do const, gdy już istnieje.
  • Pola będące referencjami: referencja musi zostać związana w chwili narodzin.
  • Pola, których typ nie ma konstruktora domyślnego.
class Sensor {
    const int id;        // pole const
    int& slot;           // pole będące referencją

public:
    Sensor(int sensorId, int& s) : id(sensorId), slot(s) {}
    // Próba ustawienia id lub slot w ciele by się nie skompilowała.
};

Jedna subtelność: pola są inicjalizowane w kolejności ich deklaracji w klasie, a nie w kolejności na liście inicjalizacyjnej. Jeśli inicjalizator jednego pola odczytuje inne, liczy się kolejność deklaracji; niezgodność obu kolejności to klasyczne źródło użycia jeszcze niezainicjalizowanej wartości.

Argumenty domyślne i konstruktory delegujące

Nie zawsze potrzebujesz osobnych przeciążeń. Argumenty domyślne pozwalają jednemu konstruktorowi obsłużyć kilka przypadków: pomiń argument, a jego miejsce zajmie wartość domyślna:

Uważaj przy łączeniu konstruktora parametrycznego z wartościami domyślnymi z osobnym konstruktorem domyślnym Point(): kompilator nie będzie wiedział, który wywołać dla Point p;, i zgłosi niejednoznaczność. Wybierz jedno podejście.

Gdy masz kilka konstruktorów o wspólnej konfiguracji, konstruktor delegujący (C++11) pozwala jednemu konstruktorowi wywołać inny zamiast powtarzać logikę. "Delegujesz", umieszczając drugi konstruktor na liście inicjalizacyjnej:

Konstruktor kopiujący

Gdy tworzysz obiekt jako kopię innego (przekazując go przez wartość, zwracając go albo pisząc Foo b = a;), uruchamia się konstruktor kopiujący. Jego sygnatura przyjmuje referencję const do tego samego typu:

ClassName(const ClassName& other);

Jeśli go nie napiszesz, kompilator wygeneruje domyślny konstruktor kopiujący, który kopiuje każde pole. Dla klas przechowujących same wartości (liczby, napisy, wektory) to dokładnie to, czego trzeba, i nie należy pisać własnego.

Wielka pułapka czeka w następnym rozdziale o pamięci: jeśli twoja klasa posiada surowy wskaźnik na pamięć na stercie, domyślny konstruktor kopiujący kopiuje wskaźnik, a nie dane. Dwa obiekty wskazują wtedy tę samą pamięć i oba spróbują ją zwolnić. To błąd podwójnego zwolnienia (double free). Praktyczna zasada to reguła trzech/pięciu: jeśli piszesz własny destruktor, niemal na pewno potrzebujesz też własnego konstruktora kopiującego (i przypisania kopiującego). We współczesnym C++ czystszym rozwiązaniem jest trzymanie std::vector albo inteligentnego wskaźnika, dzięki czemu kopia wygenerowana przez kompilator po prostu działa.

Zauważ też, że przyjmowanie parametru przez referencję jest wymagane, a nie opcjonalne: konstruktor kopiujący przyjmujący argument przez wartość musiałby skopiować argument, żeby wywołać samego siebie. To nieskończona rekurencja, która nawet się nie skompiluje.

Dalej: destruktory

Konstruktor przygotowuje obiekt; destruktor go rozbiera. Gdy obiekt wychodzi z zasięgu lub zostaje usunięty, jego destruktor uruchamia się automatycznie. To idealne miejsce na zwolnienie plików, połączeń sieciowych albo pamięci na stercie, które obiekt trzymał. Następna strona pokazuje, jak działają destruktory, kiedy dokładnie się uruchamiają i jak w parze z konstruktorami tworzą potężny wzorzec RAII w C++.

Najczęściej zadawane pytania

Czym jest konstruktor w C++?

Konstruktor to specjalna metoda o tej samej nazwie co klasa i bez typu zwracanego. Uruchamia się automatycznie przy tworzeniu obiektu, a jej zadaniem jest doprowadzenie obiektu do poprawnego, w pełni zainicjalizowanego stanu, zanim użyje go jakikolwiek inny kod.

Czym różni się konstruktor domyślny od parametrycznego?

Konstruktor domyślny nie przyjmuje argumentów i jest używany, gdy tworzysz obiekt bez podawania wartości (Point p;). Konstruktor parametryczny przyjmuje argumenty, dzięki czemu wywołujący może zainicjalizować obiekt konkretnymi wartościami (Point p(3, 4);). Klasa może mieć oba, bo konstruktory przeciąża się listami parametrów.

Dlaczego warto używać listy inicjalizacyjnej w C++?

Lista inicjalizacyjna (: name(n), age(a)) inicjalizuje pola bezpośrednio, zanim wykona się ciało konstruktora. Jest wymagana dla pól const, referencji i pól bez konstruktora domyślnego, a do tego unika marnotrawnego schematu "najpierw skonstruuj domyślnie, potem przypisz", który występuje przy przypisaniu w ciele.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ