Массив - специальный тип данных, который позволяет объединить несколько переменных одного типа под общим именем: тип_данных имя[количество]
:
- Массив можно сделать из любого типа данных (в том числе из функций, объектов...)
- Массив может быть инициализирован списком значений, указанных в фигурных скобках -
{значение, значение}
- После крайнего значения в списке может стоять запятая, это не будет ошибкой
{значение, }
- Пустые фигурные скобки
{}
инициализируют все элементы массива значением по умолчанию - Количество элементов можно не указывать, если при инициализации передан весь список с нужным количеством
// неинициализированный массив на 10 int
int arr1[10];
// инициализированный нулями массив на 10 int
int arr2[10] = {};
// массив на 10 int с частичной инициализацией, остальные элементы 0
int arr3[10] = {1, 2, 3};
// массив на 3 элемента int, инициализирован
int arr4[] = {1, 2, 3, };
Массив располагается в памяти так, как записан - данные идут друг за другом по порядку, от первого элемента в сторону увеличения адреса.
Доступ к элементам #
Для доступа используется оператор квадратные скобки []
, в которых указывается индекс элемента. Запись имя[индекс]
по сути является переменной типа массива, её можно читать, писать, делать на неё ссылку и указатель:
- Индекс начинается с
0
, максимальный индекс -длина_массива - 1
- Процессор не контролирует доступ по некорректному индексу
Запись за границей массива сломает программу
int arr[5]; // массив на 5 int
arr[0] = 123; // запись в элемент 0
int val = arr[1]; // чтение элемента 1
int& ref = arr[2]; // ссылка на элемент под индексом 2
ref = 456; // запишет 456 в arr[2]
int* ptr = &arr[3]; // указатель на элемент под индексом 3
*ptr = 789; // запишет 789 в arr[3]
В C/C++ доступ работает также если поменять имя массива и индекс местами - arr[2] = 5
равносильно 2[arr] = 5
. Если где увидите - не пугайтесь
Размер массива #
Вес массива в байтах внутри программы можно измерить при помощи оператора sizeof
. Если требуется получить длину массива в количестве элементов - нужно разделить размер на вес одного элемента:
long arr[5];
sizeof(arr); // 20 байт
sizeof(arr) / sizeof(arr[0]); // 5 штук
Тип данных #
Массив является специальным типом данных тип[количество]
, то есть массив int arr[10]
- это по сути переменная arr
типа int[10]
. В то же время, мы не можем создать копию массива вот таким образом:
int arr[3] = {1, 2, 3};
int arr_c[3] = arr; // ошибка, массив нельзя присвоить к массиву по значению
Но можем сделать ссылку на массив:
int arr[3] = {1, 2, 3};
int (&arr_r)[3] = arr; // ссылка на массив типа int[3]
arr_r[1] = 123; // изменили элемент arr[1] по ссылке на массив
Указатель #
Одновременно с этим имя массива автоматически преобразуется к указателю на первый элемент массива (с индексом 0) типа массива. То есть в массиве int arr[10]
имя arr
также имеет тип int*
и хранит адрес элемента массива с индексом 0 - можно получить доступ к элементам массива по имени как по указателю:
int arr[10];
*arr = 123; // равносильно arr[0] = 123
*(arr + 2) = 456; // равносильно arr[2] = 456
int* ptr = arr; // указатель на массив
ptr[3] = 789; // равносильно arr[3] = 789
Таким образом квадратные скобки - просто ещё один синтаксис для работы с указателями: операция ptr[n]
равносильна *(ptr + n)
и для массивов, и для указателей
Во время преобразования имени массива к указателю теряется длина массива, так как указатель не хранит размер данных - оператор sizeof
ожидаемо вернёт размер указателя на текущей платформе:
short arr[10];
sizeof(arr); // 20 байт
short* ptr = arr;
sizeof(ptr); // 2-4 байта, вес указателя
Передача в функцию #
Передать массив в функцию в C++ можно несколькими способами - по массиву и по указателю, все способы позволяют изменять исходный массив в функции, т.е. не создают в памяти копию массива. Здесь же рассмотрим вариант с шаблоном - речь о них пойдёт в следующих уроках:
// по указателю на первый элемент
void byPtr(short* arr) {
sizeof(arr); // 2-4 байта, размер массива "потерян"
arr[0] = 111;
}
// как массив произвольной длины (по сути - указатель)
void byArr(short arr[]) {
sizeof(arr); // 2-4 байта, размер массива "потерян"
arr[1] = 222;
}
// как массив с указанием размера
void byArrN(short arr[10]) {
sizeof(arr); // 20 байт
arr[2] = 333;
}
// по ссылке с указанием размера
void byRef(short (&arr)[10]) {
sizeof(arr); // 20 байт
arr[3] = 444;
}
// по ссылке через шаблон
template <typename T>
void byT(T& arr) {
// здесь T превращается в short (&arr)[10]
sizeof(arr); // 20 байт
arr[4] = 555;
}
short arr[10];
byPtr(arr);
byArr(arr);
byArrN(arr);
byRef(arr);
byT(arr);
// arr == {111, 222, 333, 444, 555, ...}
Чтобы внутри функции нельзя было изменить исходный массив - достаточно добавить слово const
слева к параметрам, например const short (&arr)[10]
, const short* arr
.
Передача по указателю более универсальна - позволяет передать массив любого размера. Правда размер придется передать отдельным параметром, если он нужен - такой код можно часто встретить в библиотеках:
// принимаем массив и его размер
void foo(int* arr, size_t size) {
}
int arr[10];
foo(arr, sizeof(arr));
Передача через шаблон по ссылке ещё более универсальна - позволяет передать массив любой длины с сохранением длины - внутри функции она корректно вычисляется.
Массив неизвестной длины #
В примерах выше рассмотрены случаи, в которых длина массива известна на этапе компиляции - то есть задана числом. Длина массива также может быть неизвестна на этапе компиляции и получаться откуда-то из программы, например:
int len;
// откуда то получаем len...
int arr[len];
sizeof(len); // отработает корректно!
Такие массивы можно передать в функцию только по указателю тип*
или по массиву произвольной длины тип[]
.
Массив как список значений #
Из примеров выше видно, что перед отправкой массива в функцию этот массив нужно было создать. Допустим в библиотеке есть функция, которая принимает массив в качестве "настроек", то есть не будет его менять. В таком случае, если эти настройки заранее известны, можно передать массив в функцию напрямую числами - {1, 2, 3}
, но нужно подсказать компилятору тип данных:
void foo1(const int* arr, size_t len) {}
void foo2(const int arr[4]) {}
int main() {
foo1( (const int[]){1, 2, 3, 4}, 4 );
foo2( (const int[]){1, 2, 3, 4} );
}
Массивы и циклы #
Цикл for #
Массивы часто используются в паре с циклом for
- для прохода по всему массиву. Например присвоим элементам массива значение их индексов:
int arr[10] = {};
// arr == {0, 0, 0, 0, ...}
for (int i = 0; i < 10; i++) {
arr[i] = i;
}
// arr == {0, 1, 2, 3, ...}
Цикл for each #
В C++ есть специальный вариант цикла for
для итерируемых сущностей, например для массивов известной длины. Он позволяет пробежаться по всем элементам массива, не создавая цикл-счётчик:
int arr[5] = {11, 22, 33, 44, 55};
for (int val : vals) {
// здесь val принимает значения 11, 22, 33...
}
В примере выше мы получили доступ к элементам массива по значению, то есть это их копии, а int val
- просто локальная переменная. Для изменения элементов массива можно войти в цикл по ссылке:
int arr[5];
for (int& val : vals) {
val = 0; // обнуляем каждый элемент массива
}
Если в цикле нужен текущий индекс - его можно создать отдельно. Даже в таком случае запись получится более лаконичной, чем с использованием обычного цикла for
:
int arr[5];
int i = 0;
for (int& val : vals) {
// здесь i - текущий индекс, а val - теукщий элемент
//val = i; // например присвоить элементам их индексы
++i;
}
Многомерные массивы #
Синтаксис массивов довольно гибкий и позволяет создавать массивы любой размерности. Многомерный массив - это массив массивов, например int arr[2][3]
- массив, состоящий из двух массивов по 3 элемента int
в каждом, итого 2*3=6
элементов. Иерархия идёт слева направо, то есть самое правое число будет отражать количество элементов, а числа слева от него - количество вложенных массивов. int arr[3][4][5]
- три массива, каждый из них содержит 4 массива по 5 элементов в каждом. Давайте инициализируем массив и посмотрим, как данные хранятся в памяти:
short arr2d[2][4] = {
{0, 1, 2, 3},
{4, 5, 6, 7},
};
Если рассматривать его как таблицу, то получилось две строки по четыре столбца. Доступ к элементам осуществляется в том же порядке: сначала строка, потом столбец. Например arr2d[0][2]
это число 2
из таблицы выше, а arr2d[1][1]
- 5
.
При инициализации многомерных массивов нужно обязательно указывать размерности, необязательной является только самая левая - компилятор посчитает и подставит её сам. В примере выше можно было написать short arr2d[][4]
- две "строки" компилятор посчитает сам.
Доступ #
Так как у нас имеется массив массивов, то обращение к первому индексу вернёт тип "массив", то есть из примера выше arr2d[1]
- это вторая по счёту строка в массиве, обычный одномерный массив с типом short[4]
. Можно работать с ним, как с обычным массивом:
arr2d[1][3] = 10; // заменили 7 на 10 в массиве выше
short* row1 = arr2d[1]; // указатель на первую строку двухмерного массива
void foo(short (&arr)[4]) {}
foo(arr2d[0]); // корректный код
Размер #
Оператор sizeof
честно посчитает вес всего массива - sizeof(arr2d) == 16
. А если передать ему вложенный массив - посчитает и её sizeof(arr2d[0]) == 8
.
Как хранится #
Кстати, а как такой массив хранится в памяти? А вот прямо так и хранится, по порядку слева направо и сверху вниз. Вот такой одномерный массив
short arr1d[] = {
0, 1, 2, 3,
4, 5, 6, 7,
};
Является его полным аналогом в памяти! Можно даже преобразовать его к указателю и обратиться напрямую:
short* p1 = arr1d;
short* p2 = (short*)arr2d; // нужно привести тип вручную, иначе ошибка
// p1[0] == p2[0] == 0
// p1[5] == p2[5] == 5
Связь с одномерным #
Индексацию двумерных и одномерных массивов можно связать простой формулой:
arr2d[строка][столбец] == arr1d[(строка * ширина) + столбец]
Это очень часто используется на практике, когда данные изначально представлены в виде одномерного массива, а нам нужно обратиться к нему, как к двухмерному известной ширины (количества столбцов).
Циклы #
Двух- и более мерные массивы точно так же можно использовать в циклах. Например пробежимся по массиву из примеров выше:
for (int y = 0; y < 2; y++) {
for (int x = 0; x < 4; x++) {
arr2d[y][x]; // перебор ячеек по очереди, значения 0, 1, 2, 3...
}
}
Цикл for each также будет работать, но нужно быть внимательнее с типами:
for (short (&row)[4] : arr2d) {
// здесь row (строка) - одномерный массив длиной 4
for (short &col : row) {
// здесь col (столбец) - элемент массива
// col принимает значения по порядку: 0, 1, 2, 3...
}
}
Передача в функцию #
Передать двумерный массив в функцию можно опять же несколькими способами:
// по ссылке с указанием размеров
void byRef(short (&arr)[2][4]) {
sizeof(arr); // 16
sizeof(arr); // 8
}
// как массив с указанием размеров
void byArrN(short arr[2][4]) {
sizeof(arr); // 2 - длина потеряна
sizeof(arr); // 8
}
// как массив массивов short[4] произвольного размера
void byArr(short arr[][4]) {
sizeof(arr); // 2 - длина потеряна
sizeof(arr); // 8
}
// как указатель на одномерные массивы длиной 4
// это полный аналог записи short arr[][4] выше
void byArrPtr(short (*arr)[4]) {
}
// по указателю как одномерный массив
void byPtr(short* arr) {
// размерности потеряны
sizeof(arr); // 2 - длина потеряна
sizeof(arr); // 2 - длина потеряна
}
// по ссылке через шаблон
template <typename T>
void byT(T& arr) {
// здесь T превратится в (short (&arr)[2][4])
sizeof(arr); // 16
sizeof(arr); // 8
}
byRef(arr2d);
byArrN(arr2d);
byArr(arr2d);
byPtr((short*)arr2d);
byT(arr2d);
Массив указателей #
Указатель - тоже тип данных, можно сделать массив указателей. По сути двухмерный массив из примеров выше - уже массив указателей, как мы видели из примеров.
int a, b, c;
int* arr[3] = {&a, &b, &c}; // массив указателей
// равносильные варианты записи:
*arr[0] = 1; // запись в a
*(arr[1]) = 2; // запись в b
arr[2][0] = 3; // запись в c
// здесь a == 1, b == 2, c == 3
Массив массивов #
Можно создать двумерный массив ещё одним способом - буквально как массив указателей на массивы:
int row0[] = {0, 1, 2, 3}; // строка 0
int row1[] = {4, 5, 6, 7}; // строка 1
int* arr[2] = {row0, row1}; // массив указателей
// доступ будет таким же, как у двумерного массива
arr[0][2]; // == 2
arr[1][3]; // == 7
Массив функций #
Указатели на функции можно также хранить в массиве:
// без параметров
void f1() {}
void f2() {}
void f3() {}
void (*arr[3])() = {f1, f2, f3};
// arr[0] = f1;
// вызов
arr[0]();
arr[1]();
arr[2]();
// с параметрами
void f1(int a) {}
void f2(int a) {}
void f3(int a) {}
void (*arr[3])(int a) = {f1, f2, f3};
// arr[0] = f1;
// вызов с аргументами
arr[0](11);
arr[1](22);
arr[2](33);
Функции для массивов #
Массив - очень примитивный тип данных, у которого можно обращаться только к самим элементам. Синтаксис языка не позволяет например сравнить два массива, или пересоздать-инициализировать-присвоить что-то к массиву после его инициализации:
int arr1[] = {1, 2, 3};
int arr2[] = {4, 5, 6};
arr = {1, 2, 3}; // ошибка - повторно инициализировать
arr1 = arr2 // ошибка - присвоить
arr1 == arr2; // ошибка - сравнить
Поэтому в стандартной библиотеке существует набор функций для работы с массивами, точнее для просто данных по какому-то адресу в памяти.
Для использования функций нужно подключить библиотеку <memory.h>
Эти функции обычно принимают void*
- указатель на любые данные, а также размер памяти в байтах:
memset(arr, val, len)
- заполнить массивarr
размеромlen
значениемval
в байтахmemcmp(arr1, arr2, len)
- сравнить массивыarr1
иarr2
на длинуlen
байт. Вернёт0
, если массивы одинаковыеmemcpy(dest, src, len)
- скопировать массивsrc
в массивdest
на длинуlen
байт
И некоторые другие, смотрите список в этом уроке.
int arr1[] = {1, 2, 3};
int arr2[] = {4, 5, 6};
memcmp(arr1, arr2, sizeof(arr1)); // не равно 0, массивы разные
memset(arr1, 0, sizeof(arr1)); // заполнить массив arr1 нулями
memcpy(arr1, arr2, sizeof(arr1)); // скопировать массив arr2 в arr1
memcmp(arr1, arr2, sizeof(arr1)); // == 0, массивы теперь одинаковые
Важно помнить, что работа ведётся в байтах. То есть например не получится заполнить массив отличного от uint8_t
типа конкретными значениями через memset
- эти значения пишутся в каждый байт, а не в каждый элемент. Если элемент состоит из нескольких байт - получится уже другое значение:
uint16_t arr16[3];
uint8_t arr8[3];
memset(arr16, 12, sizeof(arr16));
memset(arr8, 12, sizeof(arr8));
// arr16[0] == arr16[1] == arr16[2] == 3084
// arr8[0] == arr8[1] == arr8[2] == 12