Що ви опануєте
Ви навчитеся простежувати створення й знищення об’єктів, розрізняти область видимості та час життя, пояснювати позичений доступ і динамічне володіння. Ці поняття визначають безпечність посилань, вказівників і контейнерів. Наскрізний приклад — список назв навчальних матеріалів із тимчасовими об’єктами для обробки.
Об’єкт і зберігання
Об’єкт C++ має тип і займає область зберігання. Адреса позначає його місце, значення описує стан, час життя визначає період коректного використання. Область видимості стосується доступності імені в коді. Динамічний об’єкт може пережити блок, де його створили, а посилання може залишатися видимим після знищення об’єкта, на який воно вказує.
Автоматична тривалість зберігання типово реалізується стеком викликів. Динамічне зберігання часто називають купою. Стандарт описує правила тривалості й часу життя, а не обов’язкове фізичне розміщення кожної змінної. Звичайний локальний std::vector може сам бути автоматичним об’єктом і керувати динамічно виділеними елементами.
Створення та знищення
Конструктор ініціалізує об’єкт, деструктор виконується при завершенні його часу життя. Локальні об’єкти знищуються у зворотному порядку завершеної побудови. Якщо виникає виняток і стек розкручується, деструктори вже створених локальних об’єктів виконуються. Аварійне припинення процесу не дає такої загальної гарантії.
#include <iostream>
#include <string>
#include <utility>
struct Trace {
std::string name;
explicit Trace(std::string value) : name(std::move(value)) {
std::cout << "+ " << name << '\n';
}
~Trace() { std::cout << "- " << name << '\n'; }
};
int main() {
Trace outer{"outer"};
{
Trace first{"first"};
Trace second{"second"};
} // second, потім first.
std::cout << "after block\n";
} // outer.
Очікуваний порядок: + outer, + first, + second, - second, - first, after block, - outer. explicit обмежує неявне перетворення до Trace. Список ініціалізації після двокрапки створює поле name. std::move дозволяє вибрати переміщення рядка; сам вираз не виконує передачу ресурсу без відповідної операції типу.
Посилання та вказівник
Посилання T& є іншим способом доступу до об’єкта; після ініціалізації воно не переприв’язується звичайним присвоєнням. Вказівник T* зберігає адресу, може бути nullptr і може отримати іншу адресу. Розіменування *p потребує, щоб p вказував на живий доступний об’єкт. Перевірка на nullptr не виявляє вказівник на вже знищений об’єкт.
Висячий доступ виникає після завершення часу життя цілі. Повернення int& на локальний int залишає викликача з некоректним посиланням. Для малого результату повертайте значення. Позичені std::string_view і std::span також не володіють даними; їх використання потребує гарантії живого джерела. Ця гарантія має бути частиною контракту.
Контейнер та інвалідація
std::vector керує послідовністю елементів у суміжному зберіганні. Зростання місткості може перемістити елементи до іншої області, інвалідувавши вказівники, посилання та ітератори на них. Ітератор — об’єкт для позиціонування й доступу до елементів контейнера. Правила інвалідації залежать від контейнера та операції.
#include <vector>
#include <iostream>
int main() {
std::vector<int> values{10, 20};
int snapshot = values[0]; // Окрема копія значення.
values.push_back(30);
// Отримуємо доступ заново після можливої зміни зберігання.
std::cout << snapshot << " " << values.at(0) << '\n';
}
at перевіряє індекс і може кинути виняток. Значення snapshot незалежне від переміщення елементів. Не зберігайте посилання на values[0] і не використовуйте його після операції, що може перевиділити пам’ять, без перевірки гарантій. reserve може зменшити перевиділення, але його власний виклик теж може інвалідувати доступ.
Динамічне володіння
std::unique_ptr<T> виражає одного власника динамічного об’єкта. std::make_unique<T> створює об’єкт і власника. Деструктор власника звільняє ресурс. Передача через std::move робить попередній unique_ptr порожнім. Для інших типів стан після переміщення визначає їх контракт, тому не узагальнюйте цю властивість на всі об’єкти.
#include <memory>
#include <utility>
#include <iostream>
int main() {
auto owner = std::make_unique<int>(42);
auto next = std::move(owner);
if (!owner && next) std::cout << *next << '\n';
}
Виводиться 42. Ресурс не копіюється. Звичайний локальний int був би достатнім для такого малого прикладу; динамічне володіння тут використано для демонстрації механізму. У проєкті воно потребує причини, наприклад поліморфного часу життя або передачі відповідальності.
Налагодження та професійні практики
AddressSanitizer може виявляти багато помилок доступу до пам’яті у виконаних сценаріях. Для Clang або GCC використайте -fsanitize=address,undefined -g разом із підтримуваними параметрами платформи. Успішний запуск не доводить відсутності всіх висячих доступів: неперевірена гілка може залишатися небезпечною.
Практичний аналіз
Побудуйте таблицю часу життя для Trace з двома вкладеними блоками. Поясніть, які об’єкти існують у кожній точці. Для vector назвіть операцію, яка може змінити зберігання. Чому перевірка вказівника на nullptr не гарантує живого об’єкта? Пояснення має пов’язувати адресу, власника й час життя.
Розбір позиченого доступу
Припустімо, функція створила локальний std::string і повернула вказівник на його символи. Після повернення локальний рядок знищено, тому доступ уже не має гарантії коректності. Значення адреси може залишитися схожим на попереднє, але це не відновлює життя об’єкта. Поведінка при читанні через такий доступ невизначена.
Повернення std::string за значенням задає іншу модель: результат стає окремим об’єктом викликача, а компілятор може застосувати оптимізації копіювання. Контракт не залежить від локальної змінної після виходу. Для std::string_view потрібно прямо вказати, хто зберігає символи й до якого моменту вони існують.
У vector збільшення capacity може перемістити елементи у нове сховище. Посилання, вказівники та ітератори до старих елементів тоді стають недійсними. Навіть reserve не є універсальним дозволом зберігати доступ: потрібно знати, які подальші операції виконуються й чи не перевищать вони зарезервовану місткість. Безпечний підхід — отримувати доступ після структурної зміни, якщо контракт не гарантує стабільності.
Побудуйте таблицю життя: момент створення власника, отримання позиченого доступу, зміна контейнера, останнє використання, знищення. Для кожного доступу поясніть, чи покриває життя власника весь потрібний інтервал. Цей метод працює і для локальних змінних, і для heap-об’єктів. Автоматичний власник звільняє ресурс у визначений момент, але позичені користувачі все одно мають дотримуватися його меж.
Схема процесу
Ініціалізація починає час життя об’єкта.
Самоперевірка
Як обґрунтувати відповідь своїми словами?
Підсумок
Час життя визначає допустимість доступу. Посилання, вказівники й views потребують живого джерела. Контейнери та RAII-власники керують ресурсами, але правила інвалідації й передачі відповідальності потрібно пояснювати явно.