Массивы
О C-массивах
Заголовок раздела «О C-массивах»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;}std::array
Заголовок раздела «std::array»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;}