1. Чому std::vector перевиділяє памʼять
Коли ви тільки починаєте програмувати, масив здається чимось спокійним: ось int arr[10], ось 10 чисел — і нікуди вони не дінуться. Але std::vector живе значно динамічніше. Він уміє зростати, а отже, інколи потребує більше памʼяті, ніж було виділено спочатку. І саме тут починається найцікавіше.
Уявіть, що vector — це автобус із місцями. size() — скільки пасажирів уже всередині. capacity() — скільки місць є загалом. Поки є вільні місця, новий пасажир (push_back) заходить без проблем. Але коли автобус заповнений, диспетчер надсилає новий, більший автобус, пасажири пересідають, а старий відʼїжджає. Це і є перевиділення памʼяті (reallocation).
Мінідемо: спостерігаємо, як capacity() зростає стрибками
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
for (int i = 0; i < 10; ++i) {
v.push_back(i);
std::cout << "size=" << v.size()
<< " capacity=" << v.capacity() << '\n';
}
}
Тут варто побачити на власні очі: capacity() зазвичай збільшується не на 1, а «порціями». Так зроблено, щоб не «переїжджати» під час кожного push_back.
Чому «переїзд» дорогий
Перевиділення памʼяті зазвичай означає таке: виділити новий шматок памʼяті → перенести елементи → звільнити стару памʼять. Якщо елементів уже багато, переносити весь цей обсяг помітно накладно за часом. І саме тут reserve() стає нашим другом.
2. reserve(n): виділяємо памʼять заздалегідь
Якщо ви заздалегідь знаєте, скільки даних буде додано, наприклад у сценарії «введіть N чисел», то reserve() — це спосіб сказати вектору: «Нам знадобиться щонайменше n місць. Підготуємо їх заздалегідь і не переїжджатимемо кожні 5 хвилин».
Головне: reserve() керує памʼяттю, але не кількістю елементів. Тобто size() після reserve() не змінюється.
Приклад: reserve не змінює size
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
v.reserve(100);
std::cout << v.size() << '\n'; // 0
std::cout << v.capacity() << '\n'; // >= 100
}
Після reserve(100) елементів, як і раніше, немає. Це означає, що звертатися до v[0] усе ще не можна, навіть якщо capacity() велика. Про безпечний доступ поговоримо далі, а зараз варто зафіксувати просту інтуїцію: «місце є» не означає «елемент існує».
Приклад: reserve + push_back — менше «переїздів»
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
v.reserve(10);
for (int i = 0; i < 10; ++i) {
v.push_back(i);
}
std::cout << "size=" << v.size() << '\n'; // size=10
std::cout << "capacity=" << v.capacity() << '\n';
}
Якщо ми заздалегідь знаємо, що додамо 10 елементів, то reserve(10) майже завжди зменшить кількість перевиділень, а іноді й зведе її до нуля.
Коли reserve() особливо корисний
Найчастіше reserve() використовують, коли програма накопичує дані: читає вхідні дані, збирає результати обчислень, зберігає історію дій. Тобто тоді, коли ми багато разів викликаємо push_back.
І так, reserve() — це не «магічна оптимізація заради оптимізації». Це радше підхід: «не змушуй програму робити зайву роботу, якщо вже знаєш план».
3. resize(n): змінюємо кількість елементів
resize(n) звучить схоже на reserve(n), і саме тому новачки так часто плутають ці два виклики. Але сенс у resize інший: resize змінює реальну кількість елементів. Тобто після resize(n) матимемо v.size() == n.
І ось тут зʼявляється важлива практична різниця: після збільшення через resize зʼявляються нові елементи, і вони отримують значення за замовчуванням (для int це 0, для double це 0.0, для std::string — порожній рядок).
Приклад: збільшили resize — отримали нові елементи
#include <iostream>
#include <vector>
int main() {
std::vector<int> v{1, 2, 3};
v.resize(5);
std::cout << v.size() << '\n'; // 5
std::cout << v[3] << ' ' << v[4] << '\n'; // 0 0
}
Так, тут ми використовуємо []. Це нормально для демонстрації, тому що індекси ми обираємо коректно, у межах 0..size()-1. Пізніше ми розберемо, які є альтернативи та як це впливає на безпеку.
Приклад: зменшили resize — елементи з кінця зникли
#include <iostream>
#include <vector>
int main() {
std::vector<int> v{10, 20, 30, 40};
v.resize(2);
std::cout << v.size() << '\n'; // 2
std::cout << v[0] << ' ' << v[1] << '\n'; // 10 20
}
Під час зменшення resize вектор «обрізається». Це зручно, коли ви спочатку виділили місце із запасом за кількістю елементів, а потім зрозуміли, що реально потрібно менше.
Важливе спостереження: resize() теж може призвести до виділення памʼяті
Якщо ви робите resize(1'000'000), а у вектора capacity() була маленька, йому доведеться виділити памʼять під мільйон елементів. Тобто resize може стосуватися не лише «логічного розміру», а й реальної памʼяті.
Тому інколи використовують таку комбінацію: спочатку reserve(n), потім поступово push_back, а інколи — одразу resize(n) і далі заповнюють за індексами.
4. Як обрати між reserve і resize
Коли два слова відрізняються однією літерою, у програміста вмикається режим «ну це майже одне й те саме». Саме в цей момент багато хто й втрачає кілька годин життя. Тож зафіксуємо різницю максимально прямо.
Таблиця: reserve проти resize
| Операція | Що змінює | Чого НЕ змінює | Що фактично зʼявляється |
|---|---|---|---|
|
стає |
не змінюється |
Нових елементів немає |
|
стає |
може зрости (якщо не вистачає) |
Елементи додаються або видаляються |
Якщо вам потрібно запамʼятати одну фразу: reserve — «підготуй місце», resize — «створи елементи».
Практична модель: накопичуємо vs заповнюємо по місцях
Щоб не обирати між reserve і resize на рівні «мені здається…», корисно мислити двома сценаріями.
Перший сценарій — «накопичуємо». Ми не звертаємося до елементів за індексом одразу, а просто додаємо їх один за одним: push_back. Це схоже на те, як ви складаєте чеки в коробку: «ще один чек, ще один». Для такого сценарію reserve — ідеальний варіант.
Другий сценарій — «заповнюємо по місцях». Ми заздалегідь знаємо, що буде n елементів, і хочемо заповнити їх у циклі за індексами. Наприклад, зчитати n чисел і покласти їх на позиції 0..n-1. Тут зручно resize, тому що він створює елементи й робить ці індекси коректними.
Це не «правильний» і не «неправильний» шлях. Це два різні способи працювати, і ви обираєте той, що відповідає вашій задачі.
5. Практичний приклад: мінізастосунок «Облік витрат»
Продовжимо ідею навчального застосунку, який зберігає список чисел. Нехай це будуть денні витрати (у євро).
Зробимо два режими:
- користувач заздалегідь знає, скільки витрат буде введено (N чеків) → робимо reserve(N) і читаємо N чисел через push_back;
- користувач хоче ввести витрати за 7 днів тижня «по комірках» → робимо resize(7) і заповнюємо v[i].
Режим «введіть N витрат»: використовуємо reserve і push_back
Пояснення просте: якщо користувач одразу називає число N, а ми все одно не робимо reserve(N), то ніби добровільно погоджуємося на можливі зайві «переїзди». Компʼютер, звісно, упорається, але навряд чи подякує.
#include <iostream>
#include <vector>
int main() {
std::size_t n = 0;
std::cin >> n;
std::vector<int> expenses;
expenses.reserve(n);
for (std::size_t i = 0; i < n; ++i) {
int x = 0;
std::cin >> x;
expenses.push_back(x);
}
std::cout << "count=" << expenses.size() << '\n'; // count=n
}
Тут важливо, що reserve(n) не додає елементів, і ми справді додаємо їх через push_back. У результаті size() стає n.
Режим «7 днів тижня»: використовуємо resize і заповнення за індексами
Тепер інший сценарій: у нас фіксована кількість «слотів» — 7 днів. Нам не потрібно накопичувати невідому кількість значень. Нам потрібно створити 7 елементів і заповнити кожен.
#include <iostream>
#include <vector>
int main() {
std::vector<int> by_day;
by_day.resize(7);
for (std::size_t day = 0; day < by_day.size(); ++day) {
std::cin >> by_day[day];
}
std::cout << by_day[0] << '\n'; // витрата в день 0 (умовно понеділок)
}
Сенс resize тут дуже предметний: ми створюємо 7 елементів, і тепер by_day[day] — це вже наявний елемент.
6. Корисні нюанси
Як reserve зменшує перевиділення
На цьому етапі легко подумати: «Усе, зрозуміло: reserve швидше». Але давайте будемо трохи обережнішими.
Коли ви багато разів викликаєте push_back без reserve, вектор зростає так: він виділяє памʼять, заповнюється, потім «розуміє», що місця вже немає, і розширюється. Через це частина викликів push_back відбувається звичайно, а частина — уже «з переїздом».
reserve(n) каже: «Одразу підготуй буфер щонайменше під n елементів». Тоді перші n додавань зазвичай відбудуться без розширень. Це особливо помітно на великих обсягах даних.
Головне правило таке: reserve має сенс, коли ви можете більш-менш оцінити розмір. Не обовʼязково точно. Навіть приблизна оцінка часто вже корисна.
Зменшення: що робить resize, і чого не робить reserve
Іноді виникає потреба: «я зібрав багато даних, а потім зрозумів, що частина зайва». Тут логіка така: якщо ви хочете видалити елементи з кінця, resize(smaller) — ваш базовий інструмент. Він зменшить size(), тобто кількість елементів.
Але важливо розуміти: зменшення size() не обовʼязково означає зменшення capacity(). Тобто у вектора може залишитися великий запас уже виділеної памʼяті. Це нормально: стандартна бібліотека не зобовʼязана тут «стискатися», тому що це може бути дорого, а іноді навіть шкідливо, якщо ви потім знову почнете додавати елементи.
До речі, у стандарті є окрема тема про те, де саме у специфікації мають описуватися речі на кшталт reserve і shrink_to_fit (що до чого належить: до «capacity» чи до «modifiers»). Це один із тих моментів, коли добре видно: навіть у стандарті часом доводиться «переставляти полиці» у шафі.
У межах цієї лекції ми не заглиблюємося в «стискання памʼяті» — це окрема тема й окремі нюанси. Нам достатньо простої дисципліни: resize керує кількістю елементів, reserve керує запасом.
7. Мінісхема вибору
Щоб закріпити ідею, корисно тримати в голові просту «блок-схему» рішення.
flowchart TD
A[Потрібно зберігати дані у vector] --> B{Заздалегідь знаю кількість елементів?}
B -- Ні --> C[Накопичую через push_back]
C --> D["Якщо можу оцінити розмір: reserve(приблизний_n)"]
B -- Так --> E{Хочу додавати поступово?}
E -- Так --> F["reserve(n) + push_back"]
E -- Ні, хочу заповнювати за індексами --> G["resize(n) + заповнення v[i]"]
Ця схема не «єдино правильна», але вона допомагає перестати плутати reserve і resize та почати обирати їх усвідомлено.
8. Типові помилки під час роботи з reserve() і resize()
Помилка № 1: вважати, що reserve(n) створює n елементів.
Це найпоширеніша плутанина. Після reserve(n) вектор усе ще може бути порожнім (size() == 0), а спроба звернутися до v[0] — це звернення до неіснуючого елемента. Ментальна модель має бути простою: reserve — «виділив місце в памʼяті», але «пасажири ще не зайшли».
Помилка № 2: використовувати capacity() як справжній розмір даних.
Іноді новачки пишуть цикли до capacity(), бо «раз місце вже є». Але «місце є» не дорівнює «елементи існують». У логіці програми майже завжди варто орієнтуватися на size(). capacity() — це радше технічний параметр, корисний для оптимізації, а не для змісту даних.
Помилка № 3: викликати reserve() у циклі на кожному кроці.
Виглядає це приблизно так: на кожній ітерації ви робите reserve(v.size() + 1), сподіваючись: «нехай завжди вистачає». На практиці це може лише погіршити ситуацію: ви самі провокуєте часті перевиділення, бо щоразу просите новий розмір. reserve зазвичай викликають один раз перед великим набором даних, а не на кожному кроці.
Помилка № 4: плутати сценарії «накопичуємо» і «заповнюємо за індексами».
Якщо ви хочете додавати значення по одному — використовуйте push_back, а reserve нехай лише зменшує кількість «переїздів». Якщо ви хочете заповнювати вектор за індексами — спочатку зробіть resize, щоб елементи зʼявилися, а потім уже заповнюйте. І навпаки: resize заради того, щоб потім усе одно викликати push_back, часто призводить до зайвих нулів наприкінці й до плутанини: «а чому елементи подвоїлися?».
Помилка № 5: очікувати, що зменшення resize() обовʼязково зменшить capacity().
Коли ви зменшили size(), елементи зникли з погляду логіки контейнера. Але памʼять може залишитися виділеною. Це не витік і не баг — просто у вектора є право «залишити більший автобус», раптом за хвилину ви знову повезете натовп пасажирів.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ