Списки ініціалізації та делегуючі конструктори
Присвоєння vs ініціалізація в конструкторах
У попередній статті ми ініціалізували поля класу через операції присвоєння у тілі конструктора:
class Fraction
{
private:
int numerator;
int denominator;
public:
Fraction(int num = 0, int den = 1)
{
// Присвоєння значень після створення полів
numerator = num;
denominator = (den != 0) ? den : 1;
}
};
Що відбувається під час виконання цього конструктора?
Крок 1: Виділення пам'яті для об'єкта
Коли створюється об'єкт Fraction f(3, 5);, компілятор виділяє пам'ять для всіх полів класу: numerator і denominator.
Крок 2: Створення полів (конструювання за замовчуванням)
Поля numerator і denominator вже створені, але мають неініціалізовані значення (сміття з пам'яті). Для фундаментальних типів (int, double) це просто виділення місця; для класів викликався б конструктор за замовчуванням.
Крок 3: Виконання тіла конструктора
Тіло конструктора виконує присвоєння numerator = num; і denominator = ...; — це не ініціалізація, а модифікація вже створених полів.
Цей підхід працює для простих типів, але має кілька фундаментальних проблем:
⚠️ Неефективність
Для складних типів (об'єктів класів) відбувається подвійна робота:
- Спочатку викликається конструктор за замовчуванням поля
- Потім виконується присвоєння, що може викликати
operator=
Це повільніше, ніж одразу ініціалізувати правильним значенням.
❌ Неможливість для const
Константні поля (const) не можна присвоювати після створення:
class Data
{
private:
const int id; // Константне поле
public:
Data(int value)
{
id = value; // ❌ Помилка компіляції!
// Константам заборонено присвоювати значення
}
};
❌ Неможливість для посилань
Посилання (&) повинні бути ініціалізовані під час створення:
class Wrapper
{
private:
int& ref; // Посилання
public:
Wrapper(int& value)
{
ref = value; // ❌ Помилка компіляції!
// Посилання повинні бути ініціалізовані
}
};
❌ Неможливість для об'єктів без конструктора за замовчуванням
Якщо поле — об'єкт класу, що не має конструктора за замовчуванням, присвоєння не спрацює:
class Engine
{
public:
// Немає конструктора за замовчуванням!
Engine(int power) { /* ... */ }
};
class Car
{
private:
Engine engine; // ❌ Як його створити?
public:
Car(int power)
{
// engine вже має бути створений до цього моменту!
}
};
Для вирішення всіх цих проблем C++ надає списки ініціалізації членів (Member Initializer Lists, скорочено MIL).
Списки ініціалізації членів (Member Initializer List)
Список ініціалізації членів — це спеціальний синтаксис, що дозволяє ініціалізувати поля класу до виконання тіла конструктора. Це справжня ініціалізація, а не присвоєння.
Синтаксис
Список ініціалізації розміщується між сигнатурою конструктора та його тілом, починаючи з двокрапки ::
ClassName(параметри) : поле1(значення1), поле2(значення2), поле3(значення3)
{
// Тіло конструктора (опціонально)
}
Перепишемо клас Fraction з використанням MIL:
class Fraction
{
private:
int numerator;
int denominator;
public:
// Використовуємо список ініціалізації після двокрапки
Fraction(int num = 0, int den = 1) : numerator(num), denominator((den != 0) ? den : 1)
{
// Тіло конструктора порожнє — ініціалізація вже відбулася!
}
int getNumerator() const { return numerator; }
int getDenominator() const { return denominator; }
double getValue() const
{
return static_cast<double>(numerator) / denominator;
}
};
- Після параметрів конструктора
(int num = 0, int den = 1)стоїть двокрапка: - Далі йде список полів у форматі
поле(значення), розділених комами - Тіло конструктора
{}порожнє — вся робота вже виконана у списку ініціалізації - Немає крапки з комою після списку ініціалізації — одразу йдуть фігурні дужки тіла
Тепер поля numerator і denominator одразу ініціалізуються правильними значеннями під час створення об'єкта — не створюються із сміттям, а потім змінюються.
Порівняння: присвоєння vs ініціалізація
Fraction(int num, int den)
{
// 1. Поля створені (містять сміття)
// 2. Потім присвоюємо значення
numerator = num;
denominator = den;
}
// Аналог для звичайних змінних:
int numerator; // Оголошення (сміття)
numerator = 5; // Присвоєння
Fraction(int num, int den) : numerator(num), denominator(den)
{
// Поля вже повністю ініціалізовані!
}
// Аналог для звичайних змінних:
int numerator(5); // Пряма ініціалізація
// або
int numerator{5}; // Uniform initialization
Uniform initialization у списках (C++11)
Починаючи з C++11, у списках ініціалізації можна використовувати uniform initialization (фігурні дужки {}):
class Fraction
{
private:
int numerator;
int denominator;
public:
// Uniform initialization — фігурні дужки замість круглих
Fraction(int num = 0, int den = 1) : numerator{num}, denominator{(den != 0) ? den : 1}
{
}
};
Переваги uniform initialization у списках ініціалізації:
🔒 Захист від звужуючих конвертацій
class Temperature
{
private:
int celsius;
public:
Temperature(int c) : celsius(c) {} // ✅ Компілюється
// Temperature(int c) : celsius{c} {} // ✅ Компілюється
// Temperature(double c) : celsius(c) {} // ✅ Компілюється (обрізає дробову частину)
// Temperature(double c) : celsius{c} {} // ❌ Помилка — звужуюча конвертація!
};
🎯 Єдиний синтаксис
Один синтаксис для всіх типів — фундаментальних, класів, масивів:
class Data
{
private:
int value;
string name;
int arr[3];
public:
Data() : value{0}, name{"Unknown"}, arr{1, 2, 3}
{
}
};
() у списках ініціалізації, бо це класичний синтаксис, який працює в усіх версіях C++. У сучасному коді C++11+ також прийнятні фігурні дужки{}.Обов'язкові випадки використання MIL
Для деяких типів полів списки ініціалізації — це єдиний спосіб їх ініціалізації. Розглянемо кожен випадок детально.
Константні поля (const)
Константні поля повинні бути ініціалізовані під час створення і не можуть змінюватися після:
class Student
{
private:
const int id; // Унікальний ID студента — не змінюється після створення
string name;
public:
// ✅ Правильно — ініціалізація через MIL
Student(int studentId, string studentName) : id(studentId), name(studentName)
{
}
int getId() const { return id; }
string getName() const { return name; }
};
int main()
{
Student alice(12345, "Alice");
cout << alice.getId() << ": " << alice.getName() << "\n";
// alice.id = 67890; // ❌ Помилка — id константний
return 0;
}
Student(int studentId, string studentName)
{
id = studentId; // ❌ Помилка компіляції!
// error: cannot assign to variable 'id' with const-qualified type
name = studentName;
}
Поля-посилання (&)
Посилання повинні бути прив'язані до об'єкта під час створення:
class Wrapper
{
private:
int& reference; // Посилання на зовнішню змінну
public:
// ✅ Правильно — ініціалізація через MIL
Wrapper(int& value) : reference(value)
{
}
void print() const
{
cout << "Referenced value: " << reference << "\n";
}
void modify(int newValue)
{
reference = newValue; // Змінюємо значення за посиланням
}
};
int main()
{
int x = 10;
Wrapper wrapper(x);
wrapper.print(); // Вивід: 10
wrapper.modify(20);
cout << "x = " << x << "\n"; // Вивід: x = 20 — змінили через посилання!
return 0;
}
Поля-об'єкти класів без конструктора за замовчуванням
Якщо поле — об'єкт класу, у якого немає конструктора за замовчуванням, потрібно явно викликати його конструктор через MIL:
class Engine
{
private:
int horsepower;
public:
// Немає конструктора за замовчуванням!
Engine(int hp) : horsepower(hp)
{
cout << "Engine created: " << hp << " HP\n";
}
int getHorsepower() const { return horsepower; }
};
class Car
{
private:
Engine engine; // Поле-об'єкт класу Engine
string model;
public:
// ✅ Правильно — викликаємо конструктор Engine через MIL
Car(string carModel, int enginePower) : engine(enginePower), model(carModel)
{
cout << "Car created: " << model << "\n";
}
void printInfo() const
{
cout << model << " with " << engine.getHorsepower() << " HP\n";
}
};
int main()
{
Car myCar("Toyota", 150);
myCar.printInfo();
return 0;
}
Зверніть увагу на порядок виводу: спочатку створюється Engine, потім виконується тіло конструктора Car. Це підтверджує, що поля ініціалізуються до виконання тіла конструктора.
Car(string carModel, int enginePower)
{
// ❌ Помилка! Engine вже має бути створений,
// але у нього немає конструктора за замовчуванням
engine = Engine(enginePower); // Це створення тимчасового об'єкта + присвоєння
model = carModel;
}
engine, бо не знає, який конструктор викликати.Ініціалізація масивів через MIL (C++11)
До C++11 масиви можна було тільки обнулити через список ініціалізації:
class ArrayHolder
{
private:
int arr[5];
public:
// До C++11 — тільки обнулення
ArrayHolder() : arr{}
{
// Масив містить {0, 0, 0, 0, 0}
// Якщо потрібні інші значення — присвоювання в тілі
}
};
Починаючи з C++11, масиви можна повністю ініціалізувати через uniform initialization:
class ArrayHolder
{
private:
int arr[5];
public:
// C++11 — повна ініціалізація масиву
ArrayHolder() : arr{10, 20, 30, 40, 50}
{
// Масив містить {10, 20, 30, 40, 50}
}
void print() const
{
for (int i = 0; i < 5; i++) {
cout << arr[i] << " ";
}
cout << "\n";
}
};
int main()
{
ArrayHolder holder;
holder.print(); // Вивід: 10 20 30 40 50
return 0;
}
class Data
{
private:
const int id; // Константа
int& externalValue; // Посилання
int numbers[3]; // Масив
string name; // Об'єкт класу std::string
public:
Data(int dataId, int& extVal)
: id(dataId), // Ініціалізація константи
externalValue(extVal), // Ініціалізація посилання
numbers{1, 2, 3}, // Ініціалізація масиву
name("Default") // Ініціалізація об'єкта
{
}
};
Порядок ініціалізації полів: критична деталь
Поля класу ініціалізуються не в тому порядку, в якому вони вказані в списку ініціалізації, а в тому порядку, в якому вони оголошені в класі.
Приклад потенційної проблеми
class Dangerous
{
private:
int first;
int second;
public:
// ⚠️ Увага! Порядок у списку ініціалізації: second, потім first
Dangerous(int value) : second(value), first(second * 2)
{
}
void print() const
{
cout << "first = " << first << ", second = " << second << "\n";
}
};
int main()
{
Dangerous obj(5);
obj.print(); // Очікуємо: first = 10, second = 5
// Отримуємо: first = ??? (невизначене значення!), second = 5
return 0;
}
Що пішло не так?
Хоча у списку ініціалізації ми написали second(value), first(second * 2), поля ініціалізуються в порядку оголошення в класі:
- Спочатку ініціалізується
first→ викликаєтьсяfirst(second * 2), алеsecondще не ініціалізоване (містить сміття)! - Потім ініціалізується
second→ викликаєтьсяsecond(value)→second = 5
Результат: first отримує значення на основі неініціалізованого second.
Безпечний підхід
✅ Правило 1: Дотримуйтесь порядку оголошення
Пишіть поля у списку ініціалізації в тому ж порядку, в якому вони оголошені в класі:
class Safe
{
private:
int first; // Оголошено першим
int second; // Оголошено другим
public:
// ✅ Порядок у списку відповідає порядку оголошення
Safe(int value) : first(value), second(first * 2)
{
}
};
Тепер ініціалізація проходить коректно: first = value, потім second = first * 2.
✅ Правило 2: Уникайте взаємозалежностей
Не ініціалізуйте поля таким чином, щоб одне залежало від іншого. Краще використовуйте параметри конструктора або тимчасові змінні:
class Better
{
private:
int first;
int second;
public:
// ✅ Кожне поле залежить тільки від параметрів, а не один від одного
Better(int value) : first(value), second(value * 2)
{
}
};
Non-Static Member Initialization (NSMI, C++11)
Починаючи з C++11, C++ дозволяє встановлювати значення за замовчуванням для полів класу безпосередньо в оголошенні. Це називається Non-Static Member Initialization (NSMI) — ініціалізація нестатичних членів.
Синтаксис
class Point
{
private:
int x = 0; // Значення за замовчуванням для x
int y = 0; // Значення за замовчуванням для y
string label{"Origin"}; // Можна використовувати {}
public:
// Конструктор за замовчуванням — поля вже мають значення
Point()
{
// x = 0, y = 0, label = "Origin" автоматично
}
// Параметризований конструктор
Point(int xCoord, int yCoord) : x(xCoord), y(yCoord)
{
// x і y отримують нові значення через MIL
// label залишається "Origin" (значення за замовчуванням)
}
void print() const
{
cout << label << ": (" << x << ", " << y << ")\n";
}
};
int main()
{
Point origin; // x = 0, y = 0, label = "Origin"
Point custom(10, 20); // x = 10, y = 20, label = "Origin"
origin.print(); // Origin: (0, 0)
custom.print(); // Origin: (10, 20)
return 0;
}
Пріоритет: MIL перевизначає NSMI
Якщо поле має значення за замовчуванням через NSMI, але також ініціалізується через MIL, MIL має вищий пріоритет:
class Counter
{
private:
int count = 100; // NSMI — значення за замовчуванням
public:
Counter()
{
// count = 100 (NSMI)
}
Counter(int initialCount) : count(initialCount)
{
// count = initialCount (MIL перевизначає NSMI)
}
};
int main()
{
Counter c1; // count = 100 (NSMI)
Counter c2(50); // count = 50 (MIL)
return 0;
}
- NSMI — значення за замовчуванням з оголошення поля
- MIL — якщо поле згадане у списку ініціалізації, його значення перезаписується
- Тіло конструктора — якщо у тілі є присвоєння, воно перезаписує попередні значення
Коли використовувати NSMI vs MIL
class Config
{
private:
// Значення за замовчуванням для полів,
// які рідко змінюються
int timeout = 30;
int maxRetries = 3;
bool verbose = false;
public:
// Конструктор за замовчуванням використовує NSMI
Config() {}
// Можна перевизначити окремі поля через MIL
Config(int customTimeout) : timeout(customTimeout) {}
};
class BankAccount
{
private:
const int accountNumber; // Повинно бути унікальним
double balance; // Початковий баланс різний
public:
// Обов'язкова ініціалізація через MIL
BankAccount(int accNum, double initBalance)
: accountNumber(accNum), balance(initBalance)
{
}
};
- Використовуйте NSMI для полів, що мають логічні значення за замовчуванням, які підходять для більшості випадків
- Використовуйте MIL для полів, які повинні ініціалізуватися конкретними значеннями при створенні об'єкта
- Поєднуйте обидва підходи для гнучкості
Делегуючі конструктори (Constructor Delegation, C++11)
Іноді у класі є кілька конструкторів, які виконують схожу логіку ініціалізації. До C++11 доводилося дублювати код або виносити ініціалізацію у допоміжний метод. Делегуючі конструктори (delegating constructors) дозволяють одному конструктору викликати інший конструктор того самого класу.
Проблема дублювання коду
Розглянемо клас Rectangle без делегування:
class Rectangle
{
private:
int width;
int height;
string color;
public:
// Конструктор з трьома параметрами
Rectangle(int w, int h, string c) : width(w), height(h), color(c)
{
// Складна валідація
if (width <= 0) width = 1;
if (height <= 0) height = 1;
cout << "Rectangle created: " << width << "x" << height << " (" << color << ")\n";
}
// Конструктор з двома параметрами — дублювання логіки!
Rectangle(int w, int h) : width(w), height(h), color("Black")
{
// ❌ Потрібно продублювати валідацію
if (width <= 0) width = 1;
if (height <= 0) height = 1;
cout << "Rectangle created: " << width << "x" << height << " (" << color << ")\n";
}
// Конструктор за замовчуванням — ще більше дублювання!
Rectangle() : width(1), height(1), color("Black")
{
// ❌ Знову дублювання
cout << "Rectangle created: " << width << "x" << height << " (" << color << ")\n";
}
};
Усі три конструктори виконують однакову валідацію та вивід повідомлення. Це порушує принцип DRY (Don't Repeat Yourself).
Рішення: делегування конструкторів
Один конструктор може викликати інший через список ініціалізації:
class Rectangle
{
private:
int width;
int height;
string color;
public:
// Головний конструктор — вся логіка тут
Rectangle(int w, int h, string c) : width(w), height(h), color(c)
{
// Валідація та додаткова логіка в одному місці
if (width <= 0) width = 1;
if (height <= 0) height = 1;
cout << "Rectangle created: " << width << "x" << height << " (" << color << ")\n";
}
// Делегуючий конструктор — викликає головний
Rectangle(int w, int h) : Rectangle(w, h, "Black")
{
// Тіло порожнє — вся робота делегована головному конструктору
}
// Ще один делегуючий конструктор
Rectangle() : Rectangle(1, 1, "Black")
{
// Також делегує головному конструктору
}
int getWidth() const { return width; }
int getHeight() const { return height; }
string getColor() const { return color; }
};
int main()
{
Rectangle rect1; // Викликає Rectangle() → Rectangle(1, 1, "Black")
Rectangle rect2(10, 20); // Викликає Rectangle(10, 20) → Rectangle(10, 20, "Black")
Rectangle rect3(5, 8, "Red"); // Викликає Rectangle(5, 8, "Red") напряму
return 0;
}
Синтаксис делегування
Замість ініціалізації полів у списку ініціалізації, викликається інший конструктор:
ClassName(параметри) : ClassName(інші_параметри)
{
// Опціональне тіло
}
- Неможливо поєднувати делегування з ініціалізацією полів у тому ж списку:
// ❌ Помилка компіляції!
Rectangle() : Rectangle(1, 1, "Black"), width(5)
{
// Не можна делегувати І ініціалізувати поля одночасно
}
- Тільки один рівень делегування безпосередньо — але делегований конструктор може делегувати далі (утворюючи ланцюжок)
- Не можна створювати циклічне делегування (конструктор A викликає B, B викликає A)
Ланцюжки делегування
Делегування може утворювати ланцюжки викликів:
class Person
{
private:
string firstName;
string lastName;
int age;
public:
// Головний конструктор
Person(string first, string last, int personAge)
: firstName(first), lastName(last), age(personAge)
{
cout << "Full constructor: " << firstName << " " << lastName << ", " << age << "\n";
}
// Делегує головному з age = 0
Person(string first, string last) : Person(first, last, 0)
{
}
// Делегує попередньому (який делегує головному) — ланцюжок!
Person(string first) : Person(first, "Unknown")
{
}
};
int main()
{
Person p1("Alice", "Smith", 30); // Викликає головний
Person p2("Bob", "Jones"); // Bob, Jones → Bob, Jones, 0
Person p3("Charlie"); // Charlie → Charlie, Unknown → Charlie, Unknown, 0
return 0;
}
Кожен виклик проходить через весь ланцюжок, доки не досягне головного конструктора, де виконується вся ініціалізація.
- Створіть один головний конструктор із усією логікою ініціалізації та валідації
- Усі інші конструктори делегують до цього головного, передаючи різні комбінації параметрів
- Це забезпечує єдине джерело істини для ініціалізації класу
Порівняння трьох підходів ініціалізації
Розглянемо три способи ініціалізації полів класу на одному прикладі:
class Student
{
private:
int id;
string name;
double gpa;
public:
Student(int studentId, string studentName, double studentGpa)
{
// ❌ Присвоєння після створення полів
id = studentId;
name = studentName;
gpa = studentGpa;
}
};
// Проблеми:
// - Неефективно для об'єктів (подвійна робота)
// - Не працює для const, посилань, об'єктів без конструктора за замовчуванням
class Student
{
private:
int id;
string name;
double gpa;
public:
Student(int studentId, string studentName, double studentGpa)
: id(studentId), name(studentName), gpa(studentGpa)
{
// ✅ Поля ініціалізовані ефективно
}
};
// Переваги:
// - Справжня ініціалізація, а не присвоєння
// - Працює для const, посилань, будь-яких типів
// - Єдиний спосіб для багатьох випадків
class Student
{
private:
int id = 0; // NSMI — значення за замовчуванням
string name = "Unknown"; // NSMI
double gpa = 0.0; // NSMI
public:
// Конструктор за замовчуванням використовує NSMI
Student() {}
// Параметризований конструктор перевизначає через MIL
Student(int studentId, string studentName, double studentGpa)
: id(studentId), name(studentName), gpa(studentGpa)
{
}
// Частково перевизначає
Student(int studentId) : id(studentId) {}
};
// Переваги:
// - Гнучкість: значення за замовчуванням + можливість перевизначення
// - Менше дублювання коду
- Завжди використовуйте MIL замість присвоєння в тілі конструктора
- Додайте NSMI (C++11) для полів, що мають логічні значення за замовчуванням
- Використовуйте делегування (C++11) для уникнення дублювання логіки між конструкторами
- Дотримуйтесь порядку оголошення полів у списку ініціалізації
Практичне завдання: клас RGBA
Застосуємо отримані знання на практиці, створивши клас для представлення кольору у форматі RGBA (Red, Green, Blue, Alpha).
Створіть клас RGBA, який представляє колір із чотирма компонентами.
Вимоги:
Поля (приватні):
red(типuint8_t) — червоний канал (0-255), значення за замовчуванням0green(типuint8_t) — зелений канал (0-255), значення за замовчуванням0blue(типuint8_t) — синій канал (0-255), значення за замовчуванням0alpha(типuint8_t) — прозорість (0-255), значення за замовчуванням255
Конструктори (публічні):
- Конструктор за замовчуванням — використовує NSMI
- Конструктор з трьома параметрами (
red,green,blue) —alphaза замовчуванням255 - Конструктор з чотирма параметрами (всі канали)
Методи (публічні):
print()— виводитьr=<red> g=<green> b=<blue> a=<alpha>
Важливо:
- Використовуйте списки ініціалізації для всіх конструкторів
- Використовуйте NSMI (C++11) для значень за замовчуванням
- Можете використати делегування для уникнення дублювання
- Тип
uint8_tзнаходиться у заголовку<cstdint> - При виведенні
uint8_tпотрібно конвертувати уint, бо інакше виведеться як символ
Тестовий код:
int main()
{
RGBA black; // r=0, g=0, b=0, a=255
black.print();
RGBA cyan(0, 135, 135); // r=0, g=135, b=135, a=255
cyan.print();
RGBA red(255, 0, 0, 128); // r=255, g=0, b=0, a=128
red.print();
return 0;
}
Очікуваний вивід:
r=0 g=0 b=0 a=255
r=0 g=135 b=135 a=255
r=255 g=0 b=0 a=128
#include <iostream>
#include <cstdint> // Для uint8_t
using namespace std;
class RGBA
{
private:
// NSMI — значення за замовчуванням для кольорових каналів
uint8_t red = 0;
uint8_t green = 0;
uint8_t blue = 0;
uint8_t alpha = 255;
public:
// Конструктор за замовчуванням — використовує NSMI
RGBA()
{
}
// Конструктор з трьома параметрами — делегує до чотирьохпараметрового
RGBA(uint8_t r, uint8_t g, uint8_t b) : RGBA(r, g, b, 255)
{
}
// Головний конструктор — ініціалізує всі поля через MIL
RGBA(uint8_t r, uint8_t g, uint8_t b, uint8_t a)
: red(r), green(g), blue(b), alpha(a)
{
}
void print() const
{
// Конвертуємо uint8_t у int для коректного виведення
cout << "r=" << static_cast<int>(red)
<< " g=" << static_cast<int>(green)
<< " b=" << static_cast<int>(blue)
<< " a=" << static_cast<int>(alpha) << "\n";
}
};
int main()
{
RGBA black;
black.print();
RGBA cyan(0, 135, 135);
cyan.print();
RGBA red(255, 0, 0, 128);
red.print();
return 0;
}
Альтернативне рішення без делегування:
class RGBA
{
private:
uint8_t red = 0;
uint8_t green = 0;
uint8_t blue = 0;
uint8_t alpha = 255;
public:
// Конструктор за замовчуванням
RGBA() {}
// Конструктор з трьома параметрами — MIL
RGBA(uint8_t r, uint8_t g, uint8_t b) : red(r), green(g), blue(b)
{
// alpha залишається 255 (NSMI)
}
// Конструктор з чотирма параметрами — MIL
RGBA(uint8_t r, uint8_t g, uint8_t b, uint8_t a)
: red(r), green(g), blue(b), alpha(a)
{
}
void print() const
{
cout << "r=" << static_cast<int>(red)
<< " g=" << static_cast<int>(green)
<< " b=" << static_cast<int>(blue)
<< " a=" << static_cast<int>(alpha) << "\n";
}
};
Обидва рішення коректні! Варіант із делегуванням трохи більш DRY (уникає дублювання списку полів), але варіант без делегування простіший для розуміння.
Резюме та ключові висновки
Списки ініціалізації членів та делегуючі конструктори — потужні механізми, що роблять ініціалізацію класів ефективнішою, безпечнішою та гнучкішою.
📝 Списки ініціалізації (MIL)
- Синтаксис: після параметрів конструктора через двокрапку
:, списокполе(значення) - Це ініціалізація, а не присвоєння — ефективніше та коректніше
- Обов'язкові для:
const-полів, посилань, об'єктів без конструктора за замовчуванням - Порядок: поля ініціалізуються в порядку оголошення в класі, а не в порядку списку
🎯 NSMI (C++11)
- Синтаксис: значення за замовчуванням прямо в оголошенні
int x = 0;абоint x{0}; - Пріоритет: MIL перевизначає NSMI
- Використання: для полів із логічними значеннями за замовчуванням
- Зручність: не потрібно ініціалізувати ці поля у кожному конструкторі
🔗 Делегуючі конструктори (C++11)
- Синтаксис:
Constructor(...) : Constructor(інші_параметри) {} - Призначення: один конструктор викликає інший, уникаючи дублювання логіки
- Обмеження: не можна одночасно делегувати і ініціалізувати поля в одному списку
- Патерн: створіть головний конструктор з усією логікою, інші делегують до нього
🔜 Що далі
У наступній статті ми розглянемо деструктори (destructors) — спеціальні методи, що викликаються при знищенні об'єкта. Ви дізнаєтесь:
- Навіщо потрібні деструктори та коли вони викликаються
- Порядок виклику деструкторів (зворотний до конструкторів)
- Ідіому RAII — один із найважливіших патернів C++
- Як правильно управляти ресурсами (пам'ять, файли, з'єднання)
- Завжди використовуйте списки ініціалізації, а не присвоєння в тілі
- Додавайте NSMI для полів зі стабільними значеннями за замовчуванням
- Дотримуйтесь порядку оголошення полів у списку ініціалізації
- Не робіть поля взаємозалежними при ініціалізації
- Використовуйте делегування для уникнення дублювання між конструкторами
- Для
uint8_tта інших малих типів конвертуйте уintпри виведенні
Конструктори: гарантована ініціалізація об'єктів
Як конструктори забезпечують коректний початковий стан класу. Конструктор за замовчуванням, параметризовані конструктори, зменшення кількості конструкторів через параметри за замовчуванням. Uniform initialization та неявний конструктор.
Деструктори та ідіома RAII
Деструктори — спеціальні методи для очищення ресурсів при знищенні обєкта. Автоматичний виклик, порядок виконання, ідіома RAII. Управління динамічною памяттю, файлами та іншими ресурсами через час життя обєктів.