View Categories

Массивы

Массив - специальный тип данных, который позволяет объединить несколько переменных одного типа под общим именем: тип_данных имя[количество]:

  • Массив можно сделать из любого типа данных (в том числе из функций, объектов...)
  • Массив может быть инициализирован списком значений, указанных в фигурных скобках - {значение, значение}
  • После крайнего значения в списке может стоять запятая, это не будет ошибкой {значение, }
  • Пустые фигурные скобки {} инициализируют все элементы массива значением по умолчанию
  • Количество элементов можно не указывать, если при инициализации передан весь список с нужным количеством
// неинициализированный массив на 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;
}

Объявление и определение #

Объявление и определение для разделения на файлы делается по тем же правилам, что и у обычных переменных. В объявлении размер массива можно не указывать, но если указывать - должен быть таким же, как в определении. У многомерных массивов можно не указывать самую старшую размерность, как при определении:

// === lib.h
extern int arr3[];
extern int arr4[4];
extern int arr2d[][2];

// === lib.cpp
int arr3[3];
int arr4[] = {1, 2, 3, 4};
int arr2d[][2] = {{1, 2}, {3, 4}};

Многомерные массивы #

Синтаксис массивов довольно гибкий и позволяет создавать массивы любой размерности. Многомерный массив - это массив массивов, например 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
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

1 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Прокрутить вверх