C++

Списки ініціалізації та делегуючі конструктори

Member Initializer List — ефективний спосіб ініціалізації полів класу. Обовязкові випадки використання: const-поля, посилання, вкладені обєкти. Порядок ініціалізації та делегуючі конструктори C++11.

Присвоєння 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 = ...; — це не ініціалізація, а модифікація вже створених полів.

Цей підхід працює для простих типів, але має кілька фундаментальних проблем:

⚠️ Неефективність

Для складних типів (об'єктів класів) відбувається подвійна робота:

  1. Спочатку викликається конструктор за замовчуванням поля
  2. Потім виконується присвоєння, що може викликати 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;          // Присвоєння
Правило найкращої практики: Завжди використовуйте списки ініціалізації для ініціалізації полів класу замість присвоєння у тілі конструктора. Це ефективніше, чіткіше за наміром і працює для всіх типів даних.

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;
}
Без MIL це не скомпілюється:
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?Посилання — це псевдонім (alias) для іншого об'єкта. Після створення посилання воно назавжди прив'язане до цього об'єкта і не може бути перенаправлене на інший. Тому посилання повинне бути ініціалізоване під час створення.

Поля-об'єкти класів без конструктора за замовчуванням

Якщо поле — об'єкт класу, у якого немає конструктора за замовчуванням, потрібно явно викликати його конструктор через 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;
}
g++ Car.cpp -o car && ./car
$ ./car
Engine created: 150 HP
Car created: Toyota
Toyota with 150 HP

Зверніть увагу на порядок виводу: спочатку створюється Engine, потім виконується тіло конструктора Car. Це підтверджує, що поля ініціалізуються до виконання тіла конструктора.

Без MIL це не скомпілюється:
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;
}
Комбінація MIL для різних типів:
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), поля ініціалізуються в порядку оголошення в класі:

  1. Спочатку ініціалізується first → викликається first(second * 2), але second ще не ініціалізоване (містить сміття)!
  2. Потім ініціалізується 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)
    {
    }
};
Чому компілятор не ініціалізує в порядку списку?Це пов'язано з порядком виклику деструкторів (destructors), які ми розглянемо у наступній статті. Деструктори викликаються у зворотному порядку до конструкторів. Якби порядок ініціалізації залежав від списку, це б ускладнило гарантування коректної деструкції.

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;
}
Порядок застосування значень:
  1. NSMI — значення за замовчуванням з оголошення поля
  2. MIL — якщо поле згадане у списку ініціалізації, його значення перезаписується
  3. Тіло конструктора — якщо у тілі є присвоєння, воно перезаписує попередні значення

Коли використовувати NSMI vs MIL

class Config
{
private:
    // Значення за замовчуванням для полів,
    // які рідко змінюються
    int timeout = 30;
    int maxRetries = 3;
    bool verbose = false;

public:
    // Конструктор за замовчуванням використовує NSMI
    Config() {}

    // Можна перевизначити окремі поля через MIL
    Config(int customTimeout) : timeout(customTimeout) {}
};
Найкраща практика:
  • Використовуйте 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;
}
g++ Rectangle.cpp -o rect && ./rect
$ ./rect
Rectangle created: 1x1 (Black)
Rectangle created: 10x20 (Black)
Rectangle created: 5x8 (Red)

Синтаксис делегування

Замість ініціалізації полів у списку ініціалізації, викликається інший конструктор:

ClassName(параметри) : ClassName(інші_параметри)
{
    // Опціональне тіло
}
Обмеження делегуючих конструкторів:
  1. Неможливо поєднувати делегування з ініціалізацією полів у тому ж списку:
// ❌ Помилка компіляції!
Rectangle() : Rectangle(1, 1, "Black"), width(5)
{
    // Не можна делегувати І ініціалізувати поля одночасно
}
  1. Тільки один рівень делегування безпосередньо — але делегований конструктор може делегувати далі (утворюючи ланцюжок)
  2. Не можна створювати циклічне делегування (конструктор 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;
}
g++ Person.cpp -o person && ./person
$ ./person
Full constructor: Alice Smith, 30
Full constructor: Bob Jones, 0
Full constructor: Charlie Unknown, 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, посилань, об'єктів без конструктора за замовчуванням
Рекомендації для курсу:
  1. Завжди використовуйте MIL замість присвоєння в тілі конструктора
  2. Додайте NSMI (C++11) для полів, що мають логічні значення за замовчуванням
  3. Використовуйте делегування (C++11) для уникнення дублювання логіки між конструкторами
  4. Дотримуйтесь порядку оголошення полів у списку ініціалізації

Практичне завдання: клас RGBA

Застосуємо отримані знання на практиці, створивши клас для представлення кольору у форматі RGBA (Red, Green, Blue, Alpha).

Резюме та ключові висновки

Списки ініціалізації членів та делегуючі конструктори — потужні механізми, що роблять ініціалізацію класів ефективнішою, безпечнішою та гнучкішою.

📝 Списки ініціалізації (MIL)

  • Синтаксис: після параметрів конструктора через двокрапку :, список поле(значення)
  • Це ініціалізація, а не присвоєння — ефективніше та коректніше
  • Обов'язкові для: const-полів, посилань, об'єктів без конструктора за замовчуванням
  • Порядок: поля ініціалізуються в порядку оголошення в класі, а не в порядку списку

🎯 NSMI (C++11)

  • Синтаксис: значення за замовчуванням прямо в оголошенні int x = 0; або int x{0};
  • Пріоритет: MIL перевизначає NSMI
  • Використання: для полів із логічними значеннями за замовчуванням
  • Зручність: не потрібно ініціалізувати ці поля у кожному конструкторі

🔗 Делегуючі конструктори (C++11)

  • Синтаксис: Constructor(...) : Constructor(інші_параметри) {}
  • Призначення: один конструктор викликає інший, уникаючи дублювання логіки
  • Обмеження: не можна одночасно делегувати і ініціалізувати поля в одному списку
  • Патерн: створіть головний конструктор з усією логікою, інші делегують до нього

🔜 Що далі

У наступній статті ми розглянемо деструктори (destructors) — спеціальні методи, що викликаються при знищенні об'єкта. Ви дізнаєтесь:

  • Навіщо потрібні деструктори та коли вони викликаються
  • Порядок виклику деструкторів (зворотний до конструкторів)
  • Ідіому RAII — один із найважливіших патернів C++
  • Як правильно управляти ресурсами (пам'ять, файли, з'єднання)
Найкраща практика для початківців:
  1. Завжди використовуйте списки ініціалізації, а не присвоєння в тілі
  2. Додавайте NSMI для полів зі стабільними значеннями за замовчуванням
  3. Дотримуйтесь порядку оголошення полів у списку ініціалізації
  4. Не робіть поля взаємозалежними при ініціалізації
  5. Використовуйте делегування для уникнення дублювання між конструкторами
  6. Для uint8_t та інших малих типів конвертуйте у int при виведенні
Copyright © 2026