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 имеет смысл, когда вы можете reasonably угадать размер. Не обязательно идеально. Даже приблизительный размер часто уже полезен.
Уменьшение: что делает 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(), элементы исчезли с точки зрения логики контейнера. Но память может остаться выделенной. Это не утечка и не баг — просто у вектора есть право «оставить автобус побольше», вдруг вы через минуту снова повезёте толпу пассажиров.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ