Массивы и вектор в C++ без боли: короткое руководство
C-массив, std::array и std::vector решают одну задачу по-разному. Разбираем, когда какой брать, чем платишь за динамику и на каких ошибках спотыкаются новички.

Три способа хранить последовательность чисел в C++ выглядят похоже, но ведут себя по-разному под капотом: сырой C-массив int a[10], обёртка фиксированного размера std::array и растущий std::vector. Выбор не косметический: он определяет, где лежат данные (стек или куча), можно ли менять размер и что происходит при выходе за границы. Ниже — конкретика для C++17 и новее, компилятор GCC 13 или Clang 17, стандартная библиотека без сторонних зависимостей.
Три контейнера и их цена
Все три хранят элементы одного типа подряд в памяти. Различия — в размере, месте хранения и в том, кто следит за границами.
C-массив: наследие C
Размер задаётся на этапе компиляции и не меняется. Массив лежит на стеке (если объявлен как локальная переменная), а при передаче в функцию распадается в указатель — теряется информация о длине.
#include <iostream>
void printLength(int arr[]) {
// sizeof здесь вернёт размер указателя (обычно 8), а не массива
std::cout << sizeof(arr) << "\n";
}
int main() {
int numbers[5] = {1, 2, 3, 4, 5};
std::cout << sizeof(numbers) << "\n"; // 20 (5 * sizeof(int))
printLength(numbers); // 8, длина потеряна
}
Именно из-за распада в указатель C-массив опасен: функция не знает, сколько в нём элементов, и легко выйти за границу. Компилятор ничего не проверит.
std::array: тот же размер, но безопаснее
Обёртка над C-массивом фиксированного размера. Лежит там же (на стеке для локальной переменной), но знает свою длину, не распадается в указатель при передаче и умеет отдавать .size().
#include <array>
#include <iostream>
void printLength(const std::array<int, 5>& arr) {
std::cout << arr.size() << "\n"; // 5, длина сохранена
}
int main() {
std::array<int, 5> numbers = {1, 2, 3, 4, 5};
printLength(numbers);
std::cout << numbers.at(2) << "\n"; // 3, at() проверяет границы
}
std::vector: растущий массив
Размер меняется в рантайме. Данные хранятся в куче, вектор владеет этой памятью и освобождает её сам. За гибкость платите разыменованием указателя и периодическим перевыделением памяти при росте.
#include <vector>
#include <iostream>
int main() {
std::vector<int> numbers = {1, 2, 3};
numbers.push_back(4); // размер стал 4
numbers.push_back(5); // размер стал 5
std::cout << numbers.size() << "\n"; // 5
std::cout << numbers.capacity() << "\n"; // >= 5, зависит от реализации
}
Вектор хранит два числа: size() — сколько элементов сейчас, и capacity() — сколько влезет без нового выделения памяти. Когда size упирается в capacity, вектор выделяет новый блок (обычно вдвое больше), копирует туда всё и освобождает старый. Отсюда важное следствие ниже.
Сравнение по существу
| Свойство | C-массив | std::array | std::vector |
|---|---|---|---|
| Размер меняется | Нет | Нет | Да |
| Известен на этапе | Компиляции | Компиляции | Рантайма |
| Где лежат данные | Стек* | Стек* | Куча |
| Знает свою длину | Нет | Да, .size() | Да, .size() |
| Проверка границ | Нет | .at() | .at() |
| Копируется целиком | Вручную | Да, дёшево при малом размере | Да, копирует кучу |
* Для локальной переменной. Если объявить как поле объекта в куче или глобально — место меняется.
Когда что выбирать
- std::vector — выбор по умолчанию. Если заранее не знаете количество элементов или оно меняется — берите его и не думайте.
- std::array — размер известен на этапе компиляции и не меняется: координаты RGB, матрица 4×4, буфер фиксированного размера. Быстрее вектора: нет выделения в куче.
- C-массив — почти никогда в новом коде. Оправдан при взаимодействии с C-библиотеками, которые ждут указатель и длину отдельно.
Правило, которое экономит часы отладки: если сомневаетесь между тремя — беритеstd::vector. Он медленнееarrayна микроуровне, но эта разница почти всегда меньше, чем цена ошибки с ручным управлением памятью.
На чём спотыкаются
Инвалидация итераторов и ссылок при росте вектора
Когда вектор перевыделяет память, все указатели, ссылки и итераторы на его элементы становятся невалидными — они смотрят на освобождённый блок. Классическая ловушка:
#include <vector>
int main() {
std::vector<int> data = {10, 20, 30};
int& first = data[0]; // ссылка на первый элемент
data.push_back(40); // возможно перевыделение памяти
// first теперь может указывать на освобождённую память — UB
return 0;
}
Если знаете итоговый размер заранее — вызовите data.reserve(1000) до вставок. Это выделит память один раз и уберёт перевыделения внутри цикла.
operator[] против at()
Оба дают доступ по индексу, но data[100] при выходе за границу — неопределённое поведение (может упасть, может вернуть мусор), а data.at(100) бросит std::out_of_range. В горячем цикле, где вы уверены в индексе, используют []; там, где индекс приходит извне, — at().
Копирование вектора там, где хватило бы ссылки
Передача вектора в функцию по значению копирует все элементы. Для большого вектора это дорого и обычно не нужно:
// Плохо: копия всего вектора при каждом вызове
long sum(std::vector<int> v);
// Хорошо: только чтение, копии нет
long sum(const std::vector<int>& v);
Хранение размера в int
Метод .size() возвращает беззнаковый тип (size_t). Сравнение со знаковым int в цикле вида for (int i = 0; i < v.size(); ++i) компилятор пометит предупреждением о сравнении знакового с беззнаковым. Проще перебирать диапазоном: for (int x : v), а если нужен индекс — использовать size_t i.
Что делать дальше
Начните с привычки объявлять std::vector вместо C-массива везде, где размер не константа времени компиляции. Дальше стоит посмотреть на std::span (C++20) — невладеющее представление над любым из трёх контейнеров, которое передаётся в функцию без копии и сохраняет длину. Для случаев, где важна каждая наносекунда, померьте разницу между array и vector профилировщиком на своих данных, а не полагайтесь на общие рассуждения о стеке и куче.
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: std::vector — cppreference.com, std::array — cppreference.com
Частые вопросы
Что быстрее — std::array или std::vector?
А если у меня старый компилятор без C++17?
Почему capacity() больше, чем я добавил элементов?
Мой push_back в цикле работает медленно, что делать?
Можно ли вернуть vector из функции без лишнего копирования?
Программа падает при обращении по индексу — как найти причину?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.