Двумерные и многомерные массивы. Указатели на двумерные массивы

Практический курс по C/C++: https://stepik.org/course/193691

До сих пор мы с вами в программах объявляли одномерные массивы, то есть, массивы, у которых была одна размерность, один набор индексов:

short digits[10];

И по каждому индексу получали определенное числовое значение элемента:

short x = digits[2];

Такая организация подходит для хранения последовательных данных, например, значений функций в отдельных дискретных отсчетах:

Однако такое представление данных не всегда бывает удобным. Например, при хранении пикселей изображений, где есть строки и столбцы, или клеток игрового поля и многих других подобных многомерных данных.

Здесь напрашивается два набора индексов: один для строк, другой для столбцов. Это приводит нас к идее двумерных массивов, которые можно объявлять в программах на языке Си следующим образом:

<тип элементов> <имя массива>[число строк][число столбцов];

Например, для представления игрового поля игры «Крестики-нолики» в программе можно объявить такой двумерный массив:

char game_pole[3][3];

Интерпретировать его можно следующим образом. По первому индексу game_pole[i] мы получаем одномерные массивы, описывающие строки игрового поля. А второй индекс game_pole[i][j] перебирает элементы соответствующего одномерного массива:

В памяти устройства такой двумерный массив организован так же, как и одномерный, только друг за другом следуют не конкретные числовые значения, а соответствующие одномерные массивы:

И отсюда хорошо видно, что двумерный массив – это всего лишь некоторая абстракция. На уровне машинных кодов – это все тот же одномерный массив. Поэтому, строго говоря, любой одномерный массив относительно просто может быть интерпретирован как двумерный. Например, это же игровое поле в программе мы могли бы описать и так:

#define N  3
char pole_2[N * N];

Получили бы те же 9 байт в непрерывной области памяти, а доступ к элементам можно было бы организовать также по двум индексам i и j следующим образом:

char x = pole_2[i * N + j];

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

char game_pole[3][3];

При обращении к его отдельным элементам по двум индексам:

char y = game_pole[i][j];

внутри выполняются аналогичные вычисления индекса одномерного массива. Хотя, все же определение двумерного и одномерного массивов на уровне языка Си – это разные сущности и воспринимать их как одно и то же было бы неверно. Единство возникает только на уровне представления данных в памяти и не более того.

Инициализация двумерных массивов

Подтверждение этого можно увидеть в особенностях работы инициализаторов двумерных массивов. Начальные значения игрового поля game_pole можно задать следующим образом:

char game_pole[3][3] = {1, 2, 3, 4};

В этом случае массив game_pole инициализатором будет рассматриваться как одномерный и в первые четыре элемента записаны значения 1, 2, 3, 4:

Давайте выведем элементы массива game_pole в консоль и убедимся, что данные организованы именно так:

#include <stdio.h>
 
int main(void) 
{
         char game_pole[3][3] = {1, 2, 3, 4};
 
         for(int i = 0;i < 3;++i) {
                   for(int j = 0; j < 3; ++j)
                            printf("%d ", game_pole[i][j]);
                   printf("\n");
         }
 
         return 0;
}

Обратите внимание, вложенные циклы очень удобны для перебора элементов двумерных массивов. В этом качестве они очень часто используются в программах.

После выполнения программы увидим:

1 2 3
4 0 0
0 0 0

Однако инициализатор можно прописать конкретно и для двумерных массивов следующим образом:

char game_pole[3][3] = {{1, 2}, {3, 4}};

Тогда в первую строку (первый одномерный массив) будут занесены числа 1 и 2, а во вторую строку – числа 3 и 4:

После выполнения программы увидим результат:

1 2 0
3 4 0
0 0 0

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

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

double ar_3D[3][4][5];
short ar_4D[5][2][10][3];

Их инициализация выполняется аналогично инициализации двумерных массивов, а для доступа к отдельным элементам нужно указывать уже три или четыре индекса:

double val = ar_3D[1][0][1];
short res = ar_4D[0][1][5][2];

Разумеется, в памяти они по-прежнему представлены в виде непрерывной последовательности ячеек, следующих друг за другом.

Вообще, в практике программирования обычно используют одномерные и реже двумерные массивы. Массивы больших размерностей встречаются крайне редко. И это не случайно. Большое количество индексов может легко запутать программиста при чтении программы. Поэтому всегда лучше продумывать логику работы так, чтобы избегать использования многомерных массивов.

Операция sizeof с многомерными массивами

В заключение этого занятия посмотрим, как с помощью операции sizeof можно вычислять размеры многомерных массивов. Предположим, имеется следующий двумерный массив:

int game_pole[5][3];

И нам бы хотелось узнать, сколько у него строк и столбцов. Если применить операцию sizeof ко всему массиву, то получим число байт, которые он занимает в памяти:

size_t bytes = sizeof(game_pole);

В данном случае переменная bytes будет принимать значение 60:

5 ∙ 3 ∙ 4 = 60 байт.

Если же эту операцию применить к первому индексу:

size_t bytes_row = sizeof(game_pole[0]);

то получим значение 12:

3 ∙ 4 = 12 байт.

Следовательно, чтобы получить число строк нужно величину bytes разделить на bytes_row. Это можно записать в виде следующей операции:

size_t rows = sizeof(game_pole) / sizeof(game_pole[0]);

Ну а для подсчета числа столбцов добавляем в это выражение второй индекс:

size_t cols = sizeof(game_pole[0]) / sizeof(game_pole[0][0]);

Вот так, относительно просто и очевидно можно вычислять размерности многомерных массивов.

Указатели на двумерные массивы

И, последнее, что отмечу на этом занятии – это способ объявления указателей на двумерные и вообще на многомерные массивы. Как мы с вами уже знаем, имя массива – это указатель, который хранит адрес начала области памяти, в которой располагаются элементы массива. Причем, например, двумерный массив:

char game_pole[3][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9};

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

char *p_row = game_pole[1];

В итоге, p_row будет указывать на второй массив из трех элементов:

Давайте в этом убедимся. Выведем значение элемента, на который он ссылается:

printf("%d\n", *p_row);

После запуска программы увидим значение 4. Соответственно, если смещать адрес, то будет получать другие значения этого массива. Например:

printf("%d\n", *(p_row-2));

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

Но как нам определить указатель именно на двумерный массив и работать с ним как с двумерным? Не вдаваясь в длительные разъяснения, сразу приведу синтаксис объявления таких указателей:

<тип данных> (* <имя указателя>)[вторая размерность];

В нашем случае указатель на двумерный массив можно объявить следующим образом:

char (*p_ar)[3] = game_pole;

Обратите внимание на круглые скобки. Они здесь обязательны, так как приоритет операции [] выше, чем операции *. Если бы мы прописали без круглых скобок:

char *ptr[3];

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

Пользоваться указателем p_ar можно также, как и указателем game_pole. Изначально p_ar ссылается на первый одномерный массив из трех элементов. Поэтому команда:

char x = p_ar[0][1];

вернет второй элемент первого массива, то есть, число 2. Для перехода к следующей строке, достаточно увеличить первый индекс, например, так:

char x = p_ar[1][2];

прочитаем третье значение второго массива из трех элементов, то есть, число 6. И так далее.

Надо сказать, что в практике программирования очень редко прибегают к указателям на двумерные массивы. Они несколько неудобны, т.к. нам приходится явно указывать вторую размерность. Гораздо удобнее и универсальнее пользоваться указателями на одномерные массивы, которые достаточно просто можно воспринимать и как двумерные и даже как многомерные.

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме