Menu

vector в C++: динамические массивы с std::vector

std::vector - это массив C++ с изменяемым размером, контейнер, к которому стоит обращаться по умолчанию. Узнай, как создавать vector, обращаться к его элементам, наращивать и обходить его, а также об опасностях инвалидации итераторов и выхода за границы.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Почему vector вместо обычного массива

Обычный массив имеет фиксированный размер, зашитый на этапе компиляции, и забывает свою длину в тот момент, когда ты передаёшь его в функцию. std::vector решает обе проблемы: это массив с изменяемым размером, который отслеживает свою собственную длину, растёт по требованию и сам убирает за собой память. В современном C++ vector - контейнер по умолчанию; обращайся к обычному массиву только тогда, когда у тебя есть конкретная причина этого не делать.

Подключи <vector>, затем объяви его, указав тип элемента в угловых скобках:

scores.size() всегда сообщает текущую длину - не нужно отдельного int n, который приходится держать синхронизированным, и никаких трюков с sizeof. {90, 75, 100, 60} - это инициализатор в фигурных скобках; vector сам понимает, что ему нужно четыре ячейки.

Создание и инициализация vector

Есть несколько способов создать его, в зависимости от того, что ты знаешь заранее:

Остерегайся ловушки круглые скобки против фигурных: vector<int> tens(5, 10) создаёт пять копий 10, тогда как vector<int> tens{5, 10} создаёт vector из двух элементов, содержащий 5 и 10. Круглые скобки означают «размер и значение заполнения»; фигурные означают «вот эти конкретные элементы».

Добавление и удаление элементов

Весь смысл vector в том, что он растёт. push_back добавляет в конец, а pop_back удаляет с конца:

back() возвращает последний элемент, а front() - первый; чище, чем v[v.size() - 1] и v[0]. Начиная с C++11 можно также использовать emplace_back(args...), чтобы сконструировать элемент на месте, что позволяет избежать временной копии для более тяжёлых типов.

Частая ошибка новичков - вызов front() или back() для пустого vector. Это неопределённое поведение, а не ошибка - всегда сначала проверяй через if (!v.empty()).

Чтение элементов: [] против at()

Ты индексируешь vector точно так же, как массив, с помощью []. Но [] не проверяет границы - индекс за пределами диапазона - это неопределённое поведение, которое может тихо прочитать мусор или упасть позже в неожиданном месте:

vector<int> v = {1, 2, 3};
cout << v[10];   // НЕОПРЕДЕЛЁННОЕ ПОВЕДЕНИЕ - без проверки, без ошибки

Когда нужна безопасность, используй at(). Он проверяет индекс и бросает std::out_of_range при некорректном доступе, так что ты получаешь понятный отказ вместо повреждения данных:

Практическое правило: используй [] в плотных циклах, где ты уже доказал, что индекс корректен, и at() на границах, где могут просочиться некорректные входные данные.

Обход vector

Самый чистый способ пройтись по vector - это цикл for по диапазону. Бери элементы по const auto&, чтобы читать без копирования, или по auto&, чтобы изменять их на месте:

Если тебе действительно нужен индекс (например, чтобы сравнивать соседей), используй классический счётный цикл - но учти, что size() возвращает беззнаковый тип (size_t). Сравнение знакового int i с ним может вызвать предупреждения компилятора и неожиданное переполнение, поэтому по возможности предпочитай size_t i или цикл по диапазону:

for (size_t i = 0; i < v.size(); i++) {   // size_t, а не int
    cout << v[i];
}

size, capacity и reserve

vector хранит два числа: size() (сколько элементов он содержит) и capacity() (сколько он может вместить, прежде чем ему придётся вырасти). Когда push_back превышает ёмкость, vector выделяет блок побольше, копирует туда каждый элемент и освобождает старый блок. Именно поэтому повторяющиеся push_back дёшевы в амортизированном смысле, но каждое отдельное перевыделение не бесплатно:

Если ты примерно знаешь, сколько элементов добавишь, сначала вызови reserve(), чтобы пропустить повторяющиеся перевыделения. Учти, что reserve() меняет ёмкость, а не размер - в vector по-прежнему ноль элементов, пока ты их не добавишь.

Это перевыделение также источник самого коварного бага vector. Поскольку рост перемещает хранилище, любой указатель, ссылка или итератор, который ты сохранил внутрь vector, становится висячим после push_back, который перевыделяет память:

vector<int> v = {1, 2, 3};
int& first = v[0];     // ссылка внутрь vector
v.push_back(4);        // может перевыделить...
cout << first;         // ВИСЯЧАЯ - может указывать на освобождённую память

То же касается итераторов: не делай push_back или erase, пока итерируешь с сохранённым итератором. Если нужно удалять элементы во время цикла, используй возвращаемое значение erase или идиому erase-remove с std::remove.

Далее: map

vector идеален, когда ты ищешь вещи по позиции - элемент 0, элемент 1 и так далее. Но часто хочется искать вещи по ключу: имени пользователя, идентификатору товара, слову. Именно для этого нужен std::map. Дальше мы разберём map, контейнер «ключ-значение» в C++, включая то, как вставлять, искать и обходить записи, а также ловушку с тем, что [] создаёт значение по умолчанию, на которой спотыкаются почти все.

Часто задаваемые вопросы

Что такое vector в C++?

std::vector - это динамический массив с изменяемым размером из стандартной библиотеки C++. В отличие от обычного массива, он знает свой собственный размер, автоматически растёт, когда ты добавляешь элементы через push_back, и сам освобождает свою память. Подключи <vector> и напиши vector<int> v;, чтобы создать его.

В чём разница между [] и at() у vector в C++?

v[i] не проверяет границы - индекс за пределами диапазона - это неопределённое поведение (падение или тихое повреждение данных). v.at(i) проверяет индекс и бросает std::out_of_range, если он недопустим. Используй [] в горячих циклах, где ты уже проверил индекс, и at(), когда нужен безопасный и удобный для отладки отказ.

Делает ли push_back недействительными указатели и ссылки внутрь vector в C++?

Да, потенциально. Когда у vector заканчивается ёмкость, push_back перевыделяет его хранилище в новый блок, что делает недействительными все указатели, ссылки и итераторы на старые элементы. Не храни ссылку на элемент через вызов push_back и по возможности заранее вызывай reserve(), чтобы избежать неожиданных перевыделений.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ