1. Вступ
Коли ви лише починаєте програмувати, здається, що змінна — це просто «коробочка зі значенням». Але дуже швидко зʼясовується: у будь-якої «коробочки» є ще один параметр — скільки часу вона взагалі існує. Це критично важливо, бо програма може спробувати прочитати або змінити те, чого вже немає. Або, навпаки, випадково зберігати щось надто довго й отримувати «дивні ефекти, повʼязані з памʼяттю».
Сьогодні ми введемо зручну навчальну класифікацію, яка допомагає передбачати поведінку коду без містики й ворожіння на кавовій гущі: storage duration. Вона не про те, «де саме це лежить у памʼяті на рівні ОС», а про те, «коли створюється і коли зникає» (у спрощеній моделі). І так, це саме той випадок, коли знання про те, «як живуть змінні», економить години налагодження.
2. Терміни: scope, lifetime і storage duration
Давайте акуратно розкладемо терміни, бо новачки дуже часто змішують їх в один «суп зі слів», а потім цей суп починає мститися.
Scope (область видимості) — відповідає на запитання: де я можу звернутися до імені? Наприклад, імʼя x видно всередині блока { ... }, але не видно зовні. Це про доступність імені.
Lifetime (час життя) — відповідає на запитання: коли обʼєкт існує як обʼєкт? Тобто коли ним можна законно користуватися.
Storage duration (тривалість зберігання) — це «типова категорія» часу життя, тобто груба класифікація: обʼєкт живе до кінця блока, до кінця програми або доти, доки хтось не звільнить чи не втратить виділену памʼять.
У стандарті C++ ви зустрінете формулювання на кшталт «object with automatic storage duration» (обʼєкт з автоматичною тривалістю зберігання) — це не просто розмовна фраза, а справді усталений термін.
Сьогодні нам вистачить трьох видів storage duration: automatic, static, dynamic.
Шпаргалка: storage duration в одній таблиці
Перш ніж заглиблюватися в тему, корисно побачити всю картину одним поглядом. Нижче — навчальна шпаргалка. Її не потрібно завчати як вірш, але до неї корисно час від часу повертатися.
| Вид storage duration | Де зазвичай оголошується | Коли створюється | Коли знищується | Типовий приклад |
|---|---|---|---|---|
| automatic | усередині функції / усередині {} | під час входу в блок | під час виходу з блока | усередині main() |
| static | глобально або static локально | один раз за програму | під час завершення програми | |
| dynamic | «живе окремо від стека» (у навчальній практиці — усередині контейнерів) | коли контейнеру потрібна памʼять | коли контейнер звільняє памʼять (зазвичай у своєму деструкторі) | елементи , буфер |
Важливо: фраза «dynamic storage duration» стосується саме обʼєктів, а не «купи як місця». Це тонкість формулювань, але її корисно тримати в голові: ми говоримо про обʼєкти та їхнє життя.
3. Automatic storage duration: локальні змінні до }
З автоматичною тривалістю зберігання ви вже стикаєтеся щодня, навіть якщо не знали, як це називається. Майже будь-яка змінна, яку ви створюєте всередині функції, — це automatic. Вона зʼявляється під час входу в блок коду й зникає під час виходу з нього. Жодної магії — просто дисципліна: «зайшли — створили, вийшли — знищили».
Перевага automatic-змінних у тому, що вони дешеві й передбачувані. Ви майже завжди можете зрозуміти, де закінчується їхнє життя: на найближчій закривальній }. Це базовий будівельний матеріал для локальних обчислень: щось порахувати, перевірити, розібрати, порівняти, тимчасово зібрати рядок.
Приклад: змінна створюється заново під час кожного виклику функції
#include <iostream>
void demo() {
int x = 0; // automatic
++x;
std::cout << x << '\n'; // 1
}
int main() {
demo();
demo();
}
Зверніть увагу: програма виведе 1 і 1, а не 1 і 2. Бо x щоразу створюється заново. demo() закінчилася — x зник. Потім demo() знову почалася — і зʼявився новий x.
Приклад: вкладений блок «скорочує життя змінної»
#include <iostream>
#include <string>
int main() {
std::cout << "start\n"; // start
{
std::string tmp = "temporary"; // automatic
std::cout << tmp << '\n'; // temporary
} // tmp знищується тут
std::cout << "end\n"; // end
}
Практичний сенс тут простий: інколи зручно створювати «важкі» обʼєкти (наприклад, великий std::string або std::vector) якомога пізніше й знищувати якомога раніше — просто поміщаючи їх в окремий блок. Це не «мікрооптимізація», а спосіб зробити код яснішим: видно, де обʼєкт потрібен, а де вже ні.
4. Static storage duration: обʼєкт живе всю програму
Тепер — дуже корисний і водночас дуже підступний інструмент. Static storage duration означає, що обʼєкт існує майже весь час роботи програми. Найпростіший приклад — глобальна змінна. Але в реальному коді частіше трапляється інший варіант: локальна static змінна всередині функції.
І ось тут головне: scope і lifetime — не одне й те саме. Імʼя може бути видимим лише всередині функції (scope локальний), але сам обʼєкт водночас живе всю програму (storage duration static).
Приклад: лічильник викликів функції
#include <iostream>
void hits() {
static int counter = 0; // static storage duration
++counter;
std::cout << counter << '\n';
}
int main() {
hits(); // 1
hits(); // 2
hits(); // 3
}
counter створюється один раз, і його значення не «скидається» між викликами. Саме тому static часто використовують як «памʼять функції».
У стандарті C++ ви зустрінете термін «object with static storage duration» у такому ж «офіційному стилі», як і для automatic.
Коли це зручно
Уявіть, що ви робите генератор унікальних id для задач. Вам хочеться, щоб число «наступний id» не обнулялося щоразу, коли ви додаєте задачу. Тут static виглядає логічно: це «довгоживучий лічильник», який водночас приховано всередині функції й не виставлено глобально назовні.
Але водночас static — як часник: загалом корисний, але якщо переборщити, усім навколо стає складно. Надмірне використання static створює приховані залежності, робить поведінку програми менш очевидною, а тестувати такі функції стає важче, бо в них є памʼять, яка не скидається автоматично між тестами.
5. Dynamic storage duration: динамічна памʼять через контейнери
Слово «dynamic» новачки часто сприймають як щось страшне, де обовʼязково будуть new/delete і нічні кошмари. Гарна новина: сьогодні ми не займаємося ручним керуванням памʼяттю. Ми використовуємо зріліший підхід: динамічна памʼять приходить до нас через стандартні контейнери, а вони самі за неї відповідають.
Ідея проста: стек (automatic) зручний, коли розмір даних наперед невеликий і відомий. Але рядки, списки, введення користувача — усе це має розмір, який стає відомим лише під час виконання. Тому std::string і std::vector зазвичай використовують динамічну памʼять для своїх внутрішніх даних.
Ключовий момент: сам обʼєкт контейнера може бути automatic (наприклад, std::vector<int> v; усередині main()), але його елементи можуть зберігатися в динамічній памʼяті.
Приклад: std::vector зростає — і йому потрібна динаміка
#include <iostream>
#include <vector>
int main() {
std::vector<int> v; // v — automatic-обʼєкт
std::cout << v.size() << '\n'; // 0
v.push_back(10); // елементи зазвичай у dynamic-памʼяті
std::cout << v.size() << '\n'; // 1
}
Ми не бачимо «купу на власні очі», але бачимо ефект: вектор може збільшуватися в міру потреби.
Приклад: size і capacity — про «скільки є» і «скільки виділено»
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
v.reserve(10);
std::cout << v.size() << '\n'; // 0
std::cout << v.capacity() << '\n'; // 10
}
reserve(10) не додає 10 елементів. Цей виклик каже: «виділи місце, щоб потім зростати без зайвих переїздів». Це прямий місток до теми dynamic storage duration: памʼять під елементи виділяється окремо, і контейнер керує цим ресурсом.
Важлива плутанина: dynamic не означає «живе довго»
Тут варто спеціально зробити паузу, бо це один із головних «мозкових багів» у новачків.
Коли ви чуєте «динамічна памʼять», може здатися: «О, отже, воно житиме довго». Насправді ні. Динамічна памʼять — це не про «довго», а про те, що вона не привʼязана до блоку {} й може мати змінний розмір».
Наприклад, std::vector<int> v; усередині функції може жити рівно до }. І коли v знищиться, він звільнить свою динамічну памʼять (усередині себе). Тобто dynamic-памʼять може використовуватися дуже недовго — просто тому, що потрібен змінний розмір.
І навпаки, static змінна може жити довго, але при цьому взагалі не мати стосунку до динамічної памʼяті.
Ця думка потрібна не для філософії, а для того, щоб правильно відповідати на запитання «хто відповідає за дані» і «чому після виходу з функції дані зникли».
Інтуїтивна схема: «офіс» і блок-схема
Щоб закріпити це інтуїтивно, уявімо програму як невеликий офіс:
- automatic — це «папірець на столі співробітника». Поки співробітник на місці (поки виконується блок), папірець існує. Пішов зі зміни (вийшли з блока) — папірець викинули.
- static — це «інформація на стенді компанії». Вона висить увесь робочий день (тобто всю програму).
- dynamic — це «склад». Туди можна приносити коробки різного розміру, але складом хтось має керувати (у нашому випадку — std::vector/std::string).
Можна навіть намалювати просту блок-схему:
flowchart TD
A["Де оголосили обʼєкт?"] --> B{"Усередині блоку `{}`?"}
B -->|Так| C["Зазвичай automatic (живе до `}`)"]
B -->|Ні| D{"Глобально / `static`?"}
D -->|Так| E["static (живе до кінця програми)"]
D -->|Ні| F["dynamic (у навчальній практиці: памʼять усередині контейнерів)"]
Ця схема — не «формальна істина всесвіту», а зручний компас: вона допомагає швидко прикинути, чого очікувати від змінної.
6. Практичний приклад: міні-застосунок TaskBook
Щоб не залишати тему у вакуумі, давайте продовжимо нашу навчальну лінію: маленький консольний застосунок, у якому ми зберігаємо список задач. Ми не робимо повноцінний CLI-парсер (це буде значно пізніше), а просто покажемо, як storage duration проявляється у звичайному коді.
Модель задачі: automatic-обʼєкт і динаміка всередині std::string
Сама структура Task — звичайна модель даних. Коли ми створюємо змінну Task t; усередині функції, це automatic-обʼєкт. Але std::string усередині нього може використовувати динамічну памʼять для тексту.
#include <string>
struct Task {
int id = 0;
std::string title;
};
Тут важливо звикнути до думки про «листковий пиріг»: Task як обʼєкт живе за своїми правилами, а його поля (наприклад, рядок) можуть мати внутрішні ресурси й керувати ними.
Генератор id: static локальна змінна
Тепер нам потрібно видавати унікальні id. Найпростіша навчальна реалізація — функція, яка памʼятає лічильник між викликами. Це класичний випадок static storage duration.
int generate_task_id() {
static int next_id = 1; // живе всю програму
return next_id++; // 1, потім 2, потім 3...
}
Імʼя next_id видно лише всередині generate_task_id(), але живе воно до кінця програми. Це чудова ілюстрація того, що scope і storage duration — різні «осі».
Створення задачі: automatic-обʼєкт, повертаємо за значенням
Функція створює локальний Task, заповнює його й повертає. Для нас зараз важливо не те, «як саме оптимізується повернення», а те, що локальний обʼєкт t автоматичний і зникне під час виходу з функції, а назовні вже піде результат (як значення).
#include <string>
Task make_task(const std::string& title) {
Task t;
t.id = generate_task_id();
t.title = title;
return t;
}
Так, тут використовується const std::string& як параметр — ми вже вміємо так робити, щоб не копіювати рядок без потреби (це з попередніх тем про параметри). Сьогодні ми не заглиблюємося в правила посилань — просто використовуємо це як знайомий інструмент.
Сховище задач: std::vector<Task> і динамічна памʼять для елементів
Тепер створимо список задач. Змінна tasks може бути локальною в main(), отже, сама вона automatic. Але памʼять під елементи всередині вектора — динамічна.
#include <vector>
int main() {
std::vector<Task> tasks; // tasks — automatic-обʼєкт
tasks.push_back(make_task("Read C++ book"));
// ... далі додаватимемо й друкуватимемо
}
І ось це — центральна ідея дня: «локальна змінна» і «динамічна памʼять» чудово співіснують в одному коді без new/delete. Контейнер — власник памʼяті, ви — власник контейнера.
Друк задач: локальні змінні в циклі теж automatic
Зробімо маленьку функцію друку. Її локальні змінні (const Task& t у range-for, тимчасові рядки форматування тощо) — автоматичні.
#include <iostream>
#include <vector>
void print_tasks(const std::vector<Task>& tasks) {
for (const Task& t : tasks) {
std::cout << t.id << ": " << t.title << '\n';
}
}
Якщо ви бачите for (...) { ... }, то майже напевно всередині живуть automatic-змінні, які зникнуть, коли цикл закінчиться.
7. Типові помилки
Помилка № 1: плутати видимість імені (scope) і час життя обʼєкта.
Новачки часто міркують так: «Імʼя не видно — отже, обʼєкта немає». Це неправильно. Імʼя може бути невидимим, але обʼєкт усе ще може жити (наприклад, статична змінна в іншій функції або обʼєкт, до якого хтось зберігає доступ через інші механізми). І навпаки: імʼя може бути видимим, але обʼєкт уже не можна використовувати (це ми детально розберемо пізніше, коли зʼявляться вказівники й посилання). Поки що тримайте просте правило: scope відповідає за доступ до імені, а storage duration і lifetime — за існування обʼєкта.
Помилка № 2: очікувати, що локальна змінна «запамʼятає значення» між викликами функції.
Якщо змінна automatic, вона створюється заново під час кожного входу в блок. Тому код на кшталт «інкрементуємо локальний лічильник і чекаємо, що він зростатиме» дає дивовижно однакові результати. Якщо вам справді потрібна памʼять між викликами — це привід розглянути static і водночас подумати, чи не створюєте ви прихований стан, який потім буде важко тестувати й пояснювати.
Помилка № 3: вважати, що std::vector «увесь лежить у купі».
У вектора є обʼєкт-обгортка (розмір, capacity, вказівник на буфер тощо) і є буфер елементів. Обʼєкт std::vector може бути automatic (локальна змінна), а от елементи — у динамічній памʼяті. Нерозуміння цієї «двошаровості» часто призводить до дивних очікувань: «чому вектор знищився, і все зникло, хоча там була динаміка?». Бо динамічною памʼяттю володів вектор, і його lifetime закінчився.
Помилка № 4: використовувати static як «універсальну милицю», бо «так працює».
static справді може «полагодити» ситуацію, коли вам потрібно зберегти значення. Але він так само може тихо перетворити функцію на обʼєкт із прихованим станом. Потім ви додасте тести, другий сценарій використання, паралельні запуски — і виявите, що «воно дивно поводиться». Правильна звичка: перш ніж писати static, словами сформулювати, чому обʼєкт має жити всю програму і хто відповідатиме за коректність цього довгоживучого стану.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ