1. Массивы иногда неудобны
Если вы до этого жили на std::array<int, 10>, то вам знакома приятная стабильность: у массива всегда 10 элементов, никто не «переезжает», индексы понятны. Но жизнь, как известно, любит подкидывать сюжеты, где количество данных заранее неизвестно. Например: пользователь вводит оценки, пока не введёт 0; или мы читаем числа из ввода до конца; или собираем список покупок, пока пользователь не скажет «хватит».
С фиксированным массивом тут появляются два типичных неудобства. Первое — нужно заранее выбрать максимальный размер («ну пусть будет 1000… вдруг хватит?»), а это либо риск «не хватит», либо лишняя память «на всякий случай». Второе — даже если вы выбрали большой массив, вам всё равно нужно отдельно хранить «сколько элементов реально записано». И вот это «реально записано» мы сегодня сделаем встроенной частью контейнера.
2. Знакомимся с std::vector<T>
std::vector<T> — это контейнер стандартной библиотеки, который хранит элементы типа T в памяти подряд (как массив), но умеет менять количество элементов во время работы программы. То есть это «массив, который растёт и уменьшается», только без магии: всё честно, под контролем стандартной библиотеки.
Подключается он так же буднично, как iostream:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
std::cout << "ok\n"; // ok
}
С точки зрения модели на сегодня, у vector есть два важных числа.
Табличка, которую полезно держать в голове:
| Термин | Как получить | Человеческий смысл |
|---|---|---|
|
|
сколько элементов реально существует |
|
|
сколько элементов может поместиться без расширения памяти |
Про «память» мы сегодня говорим мягко, без низкоуровневых деталей, но сам факт существования capacity() и обсуждение его «сложности» — часть стандартного интерфейса контейнеров.
3. Создание вектора
Когда вы впервые создаёте std::vector, важно на уровне привычки разделить две мысли: «переменная существует» и «внутри есть элементы». Вектор может существовать и быть пустым — это нормальное состояние, а не ошибка.
Пустой вектор
Пустой вектор — это когда size() == 0.
#include <iostream>
#include <vector>
int main() {
std::vector<int> numbers;
std::cout << numbers.size() << '\n'; // 0
}
Самая частая ошибка новичка здесь — пытаться обратиться к numbers[0] «потому что вектор же есть». Вектор есть, а элемента с индексом 0 — нет. Это как прийти в пустой холодильник и спросить: «А где мой торт под номером 0?»
Инициализация списком {...}
Если вы заранее знаете стартовые данные, можно создать вектор сразу заполненным:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v{10, 20, 30};
std::cout << v.size() << '\n'; // 3
std::cout << v[0] << '\n'; // 10
}
4. Добавление и доступ: push_back и size()
Самый базовый способ «растить» вектор — это метод push_back(value). Он добавляет один элемент в конец, и после этого size() увеличивается на 1. Это действие настолько частое, что в C++ его можно считать эквивалентом фразы «положи в список ещё один элемент».
Мини-пример на 7 строк: добавим два числа и проверим размер.
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
v.push_back(5);
v.push_back(8);
std::cout << v.size() << '\n'; // 2
}
Обратите внимание на важную деталь: push_back именно добавляет в конец. То есть порядок элементов сохраняется: что добавили раньше — то будет левее (с меньшим индексом).
size(): «сколько элементов есть» и какие индексы допустимы
Когда вектор растёт, самая практичная привычка — строить всю логику вокруг size(). Не вокруг «мне кажется, там 10 элементов», не вокруг «я же добавил пару раз», а вокруг реального числа, которое контейнер хранит сам.
Если size() == n, то корректные индексы — от 0 до n - 1. Это абсолютно то же правило, что было у массивов, просто теперь n меняется во время работы программы.
Проверим на простом примере:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
v.push_back(100);
v.push_back(200);
std::cout << "size=" << v.size() << '\n'; // size=2
std::cout << v[0] << ' ' << v[1] << '\n'; // 100 200
}
А теперь ключевой момент дисциплины: если вы пишете цикл, то условие почти всегда выглядит так:
for (std::size_t i = 0; i < v.size(); ++i) {
// ...
}
Не <=, не «до 10», не «до последнего». Только строго i < v.size(). Это как ремень безопасности: кажется скучным, но внезапно спасает жизнь.
5. capacity(): почему у вектора «есть место», но ещё «нет элементов»
Слово capacity звучит как что-то из фитнеса: «ёмкость» — сколько влезет. В случае std::vector это буквально так: capacity() — сколько элементов вектор может хранить прямо сейчас, не расширяя внутреннюю память.
Почему это вообще существует? Потому что выделять память по одному элементу при каждом push_back было бы очень дорого. Поэтому вектор обычно выделяет «с запасом»: например, у вас 3 элемента, а capacity() может быть 4 или 8 — чтобы следующие push_back прошли быстро.
Важно: capacity() почти всегда >= size(). Это один из базовых «инвариантов» (то, что обязано быть правдой).
Посмотрим, как это выглядит на практике — в диагностическом примере:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
v.push_back(1);
std::cout << v.size() << ' ' << v.capacity() << '\n'; // например: 1 1 (или 1 2)
v.push_back(2);
std::cout << v.size() << ' ' << v.capacity() << '\n'; // например: 2 2 (или 2 4)
v.push_back(3);
std::cout << v.size() << ' ' << v.capacity() << '\n'; // например: 3 4
}
Числа capacity() могут отличаться на разных компиляторах и платформах — и это нормально. Важно не «угадывать формулу роста», а понимать смысл: size — это правда про элементы, capacity — это правда про запас места.
6. Инварианты std::vector: что всегда должно быть правдой
Слово «инвариант» звучит как имя древнего дракона из фэнтези, но на практике это просто список обещаний контейнера самому себе. Мы не будем уходить в формальную математику: нам достаточно понимать несколько правил, которые помогают не писать «код-сюрприз».
Первый инвариант — 0 <= size() <= capacity(). То есть элементов не может быть отрицательное количество (спасибо, Кэп), и вектор не может содержать больше элементов, чем у него сейчас выделено места.
Второй инвариант — элементы хранятся подряд (как у массива). Это одна из причин, почему vector такой популярный: он быстро работает с индексами и обычно хорошо дружит с кешем процессора. Иногда это формулируют как «vector — contiguous-контейнер», то есть с непрерывной памятью. Сегодня итераторы мы ещё не используем, но эту метку полезно держать в голове: vector — это «почти массив, но умнее».
Третий инвариант — порядок элементов сохраняется. Если вы добавляете 10, потом 20, потом 30, то индексы будут соответствовать этому порядку: v[0] == 10, v[1] == 20, v[2] == 30. Это то, что делает vector хорошим контейнером «списка» в бытовом смысле.
Наконец, важное практическое следствие из capacity(): иногда при добавлении нового элемента вектор может расширять внутреннее хранилище. На уровне ощущения это «вектор переехал в более просторную квартиру». Вам пока не нужно знать, как именно он переезжает, но нужно помнить: capacity() может измениться в момент push_back. Именно поэтому мы и не строим логику на предположениях о конкретных числах capacity().
Небольшая схема модели (без низкоуровневых деталей, просто «как думать»):
flowchart LR
A["vector: size=3"] -->|"push_back"| B{"есть место? capacity > size"}
B -->|да| C["добавили элемент size=4 capacity не меняется"]
B -->|нет| D["нужно расшириться capacity увеличится потом size=4"]
7. Мини-приложение: «Копилка расходов» на std::vector<int>
Чтобы не превратить std::vector в «теорию про контейнеры», давайте начнём маленькое приложение, которое будет развиваться по мере следующих лекций дня. Сегодня мы сделаем самую базовую часть: собираем числа в вектор через push_back, а затем выводим статистику и наблюдаем size/capacity.
Сюжет простой: пользователь вводит расходы (целые числа), а 0 означает «стоп». Мы складываем всё в std::vector<int>, потому что количество расходов заранее неизвестно.
Заготовка: приветствие и пустой список
#include <iostream>
#include <vector>
int main() {
std::vector<int> expenses;
std::cout << "Enter expenses (0 to stop)\n"; // Enter expenses (0 to stop)
std::cout << "size=" << expenses.size() << '\n'; // size=0
}
Здесь полезно глазами увидеть, что список существует, но пуст.
Чтение чисел и push_back
Добавим цикл. Заметьте, мы пока не усложняем ввод «защитой от плохого ввода» — это отдельная важная тема, но сегодня наша цель именно контейнер.
#include <iostream>
#include <vector>
int main() {
std::vector<int> expenses;
int x = 0;
std::cout << "Enter expenses (0 to stop)\n"; // Enter expenses (0 to stop)
std::cin >> x;
while (x != 0) {
expenses.push_back(x);
std::cin >> x;
}
std::cout << "count=" << expenses.size() << '\n'; // например: count=3
}
Если вы ввели 10 25 7 0, то size() станет 3. То есть size() — это «сколько реально было введено».
Посчитаем сумму обычным циклом
Пока у нас нет алгоритмов STL (они будут позже по курсу), поэтому суммируем простым for. И здесь снова выигрывает size() — цикл не зависит от того, сколько элементов ввели.
#include <cstddef>
#include <iostream>
#include <vector>
int main() {
std::vector<int> expenses{10, 25, 7};
int sum = 0;
for (std::size_t i = 0; i < expenses.size(); ++i) {
sum += expenses[i];
}
std::cout << "sum=" << sum << '\n'; // sum=42
}
Добавим «диагностику памяти»: печатаем capacity()
Это не обязательно для пользователя, но очень полезно вам, чтобы почувствовать «как растёт вектор».
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
for (int i = 1; i <= 8; ++i) {
v.push_back(i);
std::cout << "size=" << v.size()
<< " cap=" << v.capacity() << '\n';
}
}
Запустите и просто посмотрите глазами: size растёт на 1 каждый раз, а cap — скачками. И это нормально, потому что capacity() — про запас.
Соберём текущую версию приложения целиком
Это финальная версия для этой лекции: только то, что мы уже ввели — push_back, size, capacity, индексы, цикл while и один цикл for.
#include <cstddef>
#include <iostream>
#include <vector>
int main() {
std::vector<int> expenses;
std::cout << "Enter expenses (0 to stop)\n"; // Enter expenses (0 to stop)
int x = 0;
std::cin >> x;
while (x != 0) {
expenses.push_back(x);
// Диагностика для нас (не для пользователя):
std::cout << "[debug] size=" << expenses.size()
<< " cap=" << expenses.capacity() << '\n';
std::cin >> x;
}
int sum = 0;
for (std::size_t i = 0; i < expenses.size(); ++i) {
sum += expenses[i];
}
std::cout << "count=" << expenses.size() << '\n'; // например: count=3
std::cout << "sum=" << sum << '\n'; // например: sum=42
}
Да, это пока «сырой прототип». Зато он уже делает главное: хранит неизвестное число значений без заранее заданного размера и не требует «ручного счётчика заполненности», как это было бы с обычным массивом.
8. Типичные ошибки при работе с std::vector на старте
Ошибка №1: путать «вектор существует» и «вектор содержит элементы».
После std::vector<int> v; вектор создан, но v.size() равен нулю. Если сразу полезть читать v[0], вы обращаетесь к элементу, которого нет. Исправляется это привычкой сначала думать в терминах size() и проверять, что элементов действительно достаточно.
Ошибка №2: думать, что «последний индекс — это size()».
У вектора размера n последний индекс — n - 1, и то только если n > 0. Ошибка особенно коварна в циклах: i <= v.size() почти всегда означает выход за границу на последнем шаге. Надёжный шаблон — i < v.size().
Ошибка №3: использовать capacity() как «количество элементов».
capacity() — это про запас памяти, а не про реальное количество элементов. Печатать capacity() можно для диагностики и понимания, но всю логику (циклы, индексацию, проверки) строим на size().
Ошибка №4: ожидать, что capacity() растёт «по формуле», и завязывать на это код.
Иногда хочется написать «если capacity удвоилась — значит…». Не надо. Рост capacity() — деталь реализации, она может отличаться. Единственное, что важно понимать: capacity() может меняться при добавлении элементов, а сам факт существования capacity() и обсуждение его свойств — часть интерфейса контейнера.
Ошибка №5: индексы хранить в int и сравнивать с v.size() бездумно.
v.size() возвращает беззнаковый тип размера. Если вы будете смешивать int и size_t в сравнениях, иногда получите очень странные эффекты (особенно при отрицательных значениях). На уровне привычки для индексов используйте std::size_t, как мы делали в циклах.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ