Ряд значений фиксированного размера
Массив — это непрерывный блок памяти, содержащий фиксированное число значений одного типа. Если одиночный int хранит одно число, то массив int хранит много чисел подряд, и к каждому можно обратиться по целочисленному индексу.
Вы объявляете массив, указывая тип элементов, имя и размер в квадратных скобках. Добавьте список в фигурных скобках, чтобы заполнить его:
Индексация начинается с нуля: первый элемент — scores[0], а у массива размера 4 допустимые индексы от 0 до 3. Размер должен быть константой времени компиляции — в стандартном C++ нельзя написать int n = readInput(); int a[n]; (это непереносимое расширение). Когда размер нужно определить во время выполнения, используйте вместо этого vector.
Инициализация массивов
Заполнить массив можно несколькими способами, и есть пара сокращений, о которых стоит знать:
Тонкость, которую стоит усвоить: когда вы задаёте меньше инициализаторов, чем размер, оставшиеся инициализируются значением (нулём для числовых типов), а не остаются мусором. Но массив без единого инициализатора — int e[4];, объявленный как локальная переменная — содержит неопределённые значения, и чтение их до присваивания — это неопределённое поведение.
Перебор массива
Поскольку элементы хранятся непрерывно, массив обходят обычным циклом по индексу. Чтобы не выйти за границы, управляйте циклом по реальной длине массива, а не по жёстко заданному числу:
sizeof(scores) — это общее число байт всего массива; делением на sizeof(scores[0]) (размер одного элемента) получаем количество элементов. Начиная с C++17 есть более чистая запись — std::size(scores), которая читается лучше и откажется компилироваться, если вы случайно передадите ей указатель. Ещё проще, когда нужны только значения: цикл for по диапазону полностью избавляет от вычислений с индексами.
Ловушка выхода за границы
C++ не проверяет границы для arr[i]. Обращение за последний элемент не выбрасывает исключение и не предупреждает — оно читает или пишет в ту память, что там оказалась. Это самая частая ошибка с массивами и классический пример неопределённого поведения:
int a[3] = {1, 2, 3};
a[3] = 99; // OOPS - valid indices are 0..2, not 3
cout << a[5]; // garbage, crash, or corruption - undefined behavior
Ошибка «на единицу» обычно прячется в условии цикла. Написав i <= n вместо i < n, вы делаете один лишний шаг и обращаетесь к arr[n], которого не существует:
for (int i = 0; i <= n; i++) // BUG: when i == n, arr[i] is out of bounds
cout << arr[i];
Решение — дисциплина из предыдущего раздела: используйте i < size, никогда <=, и вычисляйте размер из массива, а не вписывайте заново литерал, который рассинхронизируется, как только вы добавите элемент.
Распад массива: скрытый указатель
Самое коварное поведение массивов в C++ — это распад (decay): когда вы передаёте массив в функцию, он незаметно превращается в указатель на свой первый элемент. Информация о размере теряется, поэтому sizeof внутри функции измеряет указатель, а не массив.
Обратите внимание, что int arr[] и int* arr как параметры функции идентичны — скобки чисто косметические. Поскольку количество теряется, вы должны передавать длину рядом с массивом самостоятельно:
int sum(const int* arr, int n) {
int total = 0;
for (int i = 0; i < n; i++) total += arr[i];
return total;
}
Именно этот шаблон «передай указатель и длину и молись, чтобы они совпали» создаёт то самое трение, которое толкает большую часть кода на C++ к std::array и std::vector, которые несут собственный размер и никогда не распадаются.
Пара слов о многомерных массивах
Скобки можно вкладывать, чтобы получить сетку. Двумерный массив — это на самом деле массив массивов, размещённый в памяти строка за строкой:
Индексируйте его как grid[row][col]. Действуют те же оговорки о границах и распаде — и они становятся хуже, ведь передача двумерного массива в функцию требует явно указать все размерности, кроме первой (void f(int g[][3])). Для всего, что сложнее небольшой фиксированной сетки, vector из vector-ов гораздо менее подвержен ошибкам.
Далее: Vector
Сырые массивы быстры и предсказуемы, но их фиксированный размер, отсутствие защиты границ и распад в указатель делают их неудобными для повседневного кода. Дальше мы познакомимся с std::vector — изменяемым массивом, который растёт по мере необходимости, помнит свой размер и напрямую подключается к алгоритмам STL, давая вам почти всё, что предлагают массивы, но с гораздо меньшим числом способов выстрелить себе в ногу.
Часто задаваемые вопросы
Как объявить и инициализировать массив в C++?
Укажите тип элементов, имя и размер в квадратных скобках, а затем при желании список в фигурных скобках: int scores[4] = {90, 75, 100, 60};. Размер можно опустить, если вы задаёте инициализаторы — int scores[] = {90, 75, 100, 60}; позволяет компилятору посчитать их за вас.
Как узнать длину массива в C++?
Для настоящего массива, который ещё находится в области видимости, используйте std::size(arr) (C++17) или sizeof(arr) / sizeof(arr[0]). Это не работает, когда массив распался в указатель (например, внутри функции, принявшей int arr[]), где sizeof возвращает размер указателя, а не массива.
Что произойдёт при обращении к массиву за его границами в C++?
Это неопределённое поведение. C++ не проверяет границы для arr[i], поэтому чтение или запись за концом могут привести к аварийному завершению, вернуть мусор или незаметно повредить соседнюю память. Всегда держите индекс в диапазоне от 0 до size - 1.