JavaRush /Курсы /C++ SELF /std::vector:

std::vector: push_back, size, capacity

C++ SELF
12 уровень , 1 лекция
Открыта

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 есть два важных числа.

Табличка, которую полезно держать в голове:

Термин Как получить Человеческий смысл
size()
v.size()
сколько элементов реально существует
capacity()
v.capacity()
сколько элементов может поместиться без расширения памяти

Про «память» мы сегодня говорим мягко, без низкоуровневых деталей, но сам факт существования 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, как мы делали в циклах.

1
Задача
C++ SELF, 12 уровень, 1 лекция
Недоступна
Последний заказ
Последний заказ
1
Задача
C++ SELF, 12 уровень, 1 лекция
Недоступна
Паспорт контейнера
Паспорт контейнера
1
Задача
C++ SELF, 12 уровень, 1 лекция
Недоступна
Лучший результат
Лучший результат
1
Задача
C++ SELF, 12 уровень, 1 лекция
Недоступна
Журнал расширений
Журнал расширений
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Anonymous #2923722 Уровень 17
21 мая 2026
В ходе экспериментов заметил, что выделенная память (capacity) увеличивается в два раза после того, как размер (size) становится больше предыдущего выделенного значения памяти (capacity). Так что у кого память в дефиците имейте в виду). vector<int> size=1 cap=1 size=2 cap=2 size=3 cap=4 size=4 cap=4 size=5 cap=8 size=6 cap=8 size=7 cap=8 size=8 cap=8 size=9 cap=16 size=10 cap=16 size=11 cap=16 size=12 cap=16 size=13 cap=16 size=14 cap=16 size=15 cap=16 size=16 cap=16 size=17 cap=32 size=18 cap=32 size=19 cap=32 size=20 cap=32 size=21 cap=32 size=22 cap=32 size=23 cap=32 size=24 cap=32 size=25 cap=32 size=26 cap=32 size=27 cap=32 size=28 cap=32 size=29 cap=32 size=30 cap=32 size=31 cap=32 size=32 cap=32 size=33 cap=64