Перейти к содержимому

Массивы

C-массивы — это те массивы, с которыми мы познакомились в предыдущей главе. Напомним: вот пример C-массива:

int a[4];

Они поддерживают оператор индексации и представляют собой линейный блок памяти.

Функции, принимающие массивы как аргументы

Заголовок раздела «Функции, принимающие массивы как аргументы»

Передать C-массив в функцию на самом деле нельзя. Можно передать только адрес памяти массива.

Например, определим функцию sum, которая принимает адрес первого элемента некоторого массива и суммирует все элементы по очереди, возвращая сумму как результат. Заметьте: функция должна знать, сколько элементов в этом блоке.

int sum(int* a)
{
int result = 0;
for (size_t i = 0; i < 4; i++)
{
int* pointerToElement = a + i;
int elementAtIndex = *pointerToElement;
result = result + elementAtIndex;
}
return result;
}

Прототип функции может быть и таким:

int sum(int a[]);

это означает то же самое. Я буду придерживаться записи через указатель, поскольку второй вариант мне кажется бессмысленным.

Теперь попробуем вызвать эту функцию:

int main()
{
// Заметьте: длина должна в точности совпадать с той, что используется в функции.
int arr[4]{};
arr[0] = 5;
arr[1] = 10;
// Передаем ей указатель на начало памяти массива.
int result = sum(&arr[0]); // 15
// При передаче переменной-массива неявно передается адрес первого элемента.
// Это также называют «распадом» (decay) массива в указатель.
int result1 = sum(arr); // 15
return 0;
}

Заметьте: если ошибиться с длиной массива, например, сделать массив в main короче, чем ожидает функция, компилятор не выдаст никаких предупреждений. Более того, весьма вероятно, что программа выполнится без проблем, но выдаст мусорный результат.

Представим, что main выглядит так:

int main()
{
int arr[2]{};
sum(arr);
}

Функция sum попытается суммировать память за пределами памяти, отведенной элементам массива, то есть фактически прочитает память, которая лежит дальше на стеке. Следующим за локальной переменной-массивом arr на стеке скорее всего окажется адрес возврата функции sum.

🤓 что именно лежит рядом с массивом (и лежит ли там адрес возврата) — деталь реализации раскладки стека. Стандарт технически вообще не обещает, что память за пределами массива можно читать или писать: это неопределенное поведение. На практике же на любом современном железе со стандартной раскладкой стека за локальными переменными действительно лежат служебные данные кадра, включая адрес возврата.

А теперь представьте, что функция sum пишет в эту память, а не просто читает из нее.

void setZeros(int* a)
{
for (size_t i = 0; i < 4; i++)
{
a[i] = 0;
}
}

При неверном размере это может привести к порче стека (stack corruption) — адрес возврата на стеке может быть перезаписан нулями, и программа упадет, когда функция попытается вернуться.

Порча стека означает вмешательство в память за пределами той, что ожидаемо доступна.

Чтобы этого избежать, всегда передавайте длину вместе с указателем. Полный пример см. в array.cpp.

Чтобы не повторять длину несколько раз, можно либо завести константу с ней, вот так (нормально):

const size_t arrayLength = 4;
int main()
{
int arr[arrayLength]{};
sum(arr, arrayLength);
return 0;
}

Либо использовать define (так делать не надо):

#define ARRAY_LENGTH 4UL
int main()
{
int arr[ARRAY_LENGTH]{};
sum(arr, ARRAY_LENGTH);
return 0;
}

Либо вычислить её во время компиляции в месте вызова с помощью sizeof. sizeof(arr) дает длину всего массива в байтах, и её нужно разделить на размер одного элемента sizeof(arr[0]), чтобы получить число элементов.

int main()
{
int arr[4]{};
sum(arr, sizeof(arr) / sizeof(arr[0]));
return 0;
}

В C++ есть еще один вариант: параметр шаблона (без особых подробностей). По сути, компилятор может понять длину из типа массива.

template<size_t N>
int sum(int (&arr)[N])
{
int result = 0;
for (size_t i = 0; i < N; i++)
{
result += arr[i];
}
return result;
}
int main()
{
int arr[4]{};
sum(arr);
return 0;
}

C++ добавляет несколько более удачных способов работать с линейными блоками памяти фиксированного типа. Один из них — std::array.

std::array можно использовать вместо C-массивов, и у него куча преимуществ:

  • Число элементов зашито в тип и доступно через size();
  • Это настоящий тип, а не особая сущность вроде массивов, то есть его можно передавать в функции без хаков;
  • Он не распадается на указатель;
  • Можно обычным образом получить указатель на блок памяти, если его тип — std::array (работает как обычный указатель), без дополнительного синтаксиса.
    // Указатель на массив из 4 элементов
    std::array<int, 4>* p{ &arr };

Примечание: я даже не объясняю замысловатый дополнительный синтаксис, используемый для C-массивов, потому что не считаю его полезным.

Примечание: запись Type variableName = { } эквивалентна Type variableName{ }.

// Подключите этот заголовок, чтобы использовать std::array.
#include <array>
int main()
{
std::array<int, 4> arr{};
// В памяти это выглядело бы так же, как:
// int arr[4]{};
// Оператор индексации используется как обычно.
arr[0] = 5;
// Можно брать указатели на элементы, как обычно.
int* pointerToElement = &arr[2];
// А поскольку это обычный тип, можно скопировать
// весь массив одной операцией.
// При этом создается еще один блок памяти на 4 int,
// и каждый int копируется из `arr`.
std::array<int, 4> copy = arr;
// И еще немного магии: на самом деле он способен сам понять,
// что вы имели в виду тип `int` и длину,
// если использовать инициализацию фигурными скобками с конкретными значениями.
std::array arr2{1, 2, 3, 4};
// Что эквивалентно
// std::array<int, 4> arr2{1, 2, 3, 4};
// То же произойдет при попытке скопировать:
std::array copy2 = arr2;
// Приятно то, что 4 больше не нужно нигде захардкодивать,
// и не нужен хак с `sizeof`, благодаря `.size()`:
size_t numElementsOfArray = arr.size(); // 4
// Это сработает даже в контексте const (и даже constexpr),
// потому что `.size()` известен из типа во время компиляции.
// Это полезно, когда длину массива нужно использовать
// во время компиляции, например, как размер массива.
constexpr size_t numElementsOfArray2 = arr.size(); // 4
// Можно иметь указатель на `std::array` фиксированного размера,
// чего с C-массивами не сделать (без особого синтаксиса).
std::array<int, 4>* pointerToArray = &arr;
(*pointerToArray)[1] = 9;
int firstElement = arr[1]; // 9
return 0;
}