Українська
Розбори та типові помилки
Життя посилань, вкладені об’єкти та невдале створення
Константність не гарантує тривалого життя. Метод, який повертає const std::string&, може бути безпечним для читання, поки живе власник. Якщо власник знищено, посилання стає недійсним, хоча воно константне. Якщо внутрішній контейнер перебудовується, посилання на його елемент також може втратити чинність. Контракт методу читання має описувати не лише право зміни, а й межі використання результату.
Повернення маленького значення, наприклад int balance() const, не пов’язує користувача з внутрішнім сховищем. Повернення копії рядка коштує більше, але теж відокремлює життя результату. Повернення константного посилання може бути виправданим для великих даних, якщо користувач розуміє час життя. У вступних моделях варто обирати простий контракт до оптимізації.
Якщо третє поле не вдалося сконструювати, перше й друге вже були готові та будуть знищені у зворотному порядку. Тіло конструктора зовнішнього об’єкта може ще не виконуватися. Тому небезпечно сподіватися, що зовнішній деструктор виправить будь-яку помилку виділення ресурсу в списку ініціалізації. Власник має бути полем із власним коректним деструктором, наприклад unique_ptr.
Для вкладеного Engine та Car послідовність із прикладу є частиною мовних правил. Для двох незалежних локальних автомобілів у тій самій області видимості пізніше створений знищується раніше. Але не слід перетворювати це на неявну систему залежностей між глобальними об’єктами в різних файлах: їхня ініціалізація має додаткові правила, які розглядаються під час організації багатофайлових програм.
Перевірка інтерфейсу через таблицю контрактів
Перед реалізацією випишіть для кожної операції передумову, результат і стан після відмови. Для рахунку ця таблиця відокремлює дозволений нульовий баланс від недозволеної нульової суми операції.
| Операція | Передумова | Результат або відмова |
|---|---|---|
BankAccount(0) | Баланс у межах | Коректний порожній рахунок |
deposit(0) | Сума має бути додатна | Відмова; баланс незмінний |
withdraw(balance) | Додатний баланс | Нульовий залишок |
withdraw(balance+1) | Не вистачає коштів | Відмова до присвоєння |
balance() const | Об’єкт живий | Копія числа; стан незмінний |
Таблиця виявляє неоднозначності ще до тестів. Наприклад, чи має withdraw(0) бути успіхом без дії, чи помилкою? Обидва контракти можуть бути осмисленими, але довільна зміна відповіді між методами робить API складним. У нашому прикладі нуль відхиляється явно. На захисті потрібно пояснити саме обрані правила, а не лише перерахувати назви модифікаторів доступу.
Межа між конструктором і введенням користувача
Під час читання числа можливі дві різні помилки. Текст може не перетворюватися на число взагалі, або число може порушувати предметне правило. Рядок abc відхиляє шар введення, а число -5 як початковий баланс відхиляє конструктор. Якщо змішати ці обов’язки, клас стане залежним від конкретного термінала та складним для автоматичної перевірки.
Консольний інтерфейс може повторити запит після відмови, але конструктор не повинен сам чекати наступного рядка. Його завдання коротке: або створити готове значення, або повідомити, чому створення неможливе. Той самий конструктор згодом може викликати тест, графічний інтерфейс або читач файла. В усіх випадках предметна перевірка залишається одна.
Подібний поділ стосується форматування. Рахунок повертає баланс, а зовнішній код обирає, чи показати копійки, чи гривні з двома десятковими цифрами. Метод print() може бути зручним у першій вправі, але не повинен бути єдиним способом отримати результат: тоді перевірка числа вимагатиме розбору вже надрукованого тексту.
Для лабораторного проєкту корисно скласти три окремі блоки: читання команди, предметна операція, подання результату. Вони можуть залишатися в одному файлі на початку, проте мають різну відповідальність. Розділення на .h і .cpp лише фізично оформлює інтерфейс; саме по собі воно не забезпечує такого дизайну.
Коли метод доступу повертає зайву свободу
Припустімо, бібліотека зберігає список виданих книг і перевіряє, що їх не більше трьох. Метод books() із результатом std::vector<Book>& дозволить клієнту виконати push_back четвертого елемента без жодної перевірки. Формально поле лишається private, але інваріант уже не захищений публічним інтерфейсом.
Константне посилання забирає безпосередню можливість змінювати контейнер через цей шлях, але залишає питання життя й чинності ітераторів. Копія списку відокремлює результат, проте коштує копіювання. Спеціальна операція hasBook(id) дає лише відповідь, потрібну конкретному клієнту. Вибір залежить від сценарію, а не від автоматичного правила «кожному полю потрібен getter».
Для невеликої навчальної моделі краще спочатку мати прості операції count, contains і report. Якщо виникне справжня потреба обходити елементи, контракт можна розширити свідомо. Це зберігає можливість пізніше замінити vector на іншу структуру без переписування всіх користувачів.
Контрольний розбір помилки ініціалізації
Нехай клас зберігає capacity_, а потім контейнер, розмір якого залежить від місткості. Якщо оголосити контейнер раніше, спроба використати capacity_ для його створення відбудеться до ініціалізації capacity_. Перестановка записів після двокрапки цього не виправляє: порядок задає оголошення полів.
Перший спосіб виправлення – узгодити порядок полів із реальною залежністю. Другий – використовувати вже перевірений параметр конструктора, не покладаючись на ще неготовий член. Якщо перевірка параметра складна, її можна виділити в допоміжну функцію, яка повертає лише допустиме значення або кидає виняток.
У звіті не досить написати «список ініціалізації ефективніший». Потрібно пояснити, що ініціалізація та присвоєння є різними операціями, що const-поле не можна пізніше присвоїти, а поле класового типу до тіла конструктора вже існує. Це семантична відмінність, а не лише потенційна оптимізація.