До сих пор мы с вами в программах объявляли одномерные массивы, то есть, массивы, у которых
была одна размерность, один набор индексов:
И по каждому
индексу получали определенное числовое значение элемента:
Такая
организация подходит для хранения последовательных данных, например, значений
функций в отдельных дискретных отсчетах:
Однако такое
представление данных не всегда бывает удобным. Например, при хранении пикселей
изображений, где есть строки и столбцы, или клеток игрового поля и многих других
подобных многомерных данных.
Здесь напрашивается
два набора индексов: один для строк, другой для столбцов. Это приводит нас к
идее двумерных массивов, которые можно объявлять в программах на языке Си
следующим образом:
<тип
элементов> <имя массива>[число строк][число столбцов];
Например, для
представления игрового поля игры «Крестики-нолики» в программе можно объявить
такой двумерный массив:
Интерпретировать
его можно следующим образом. По первому индексу 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 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 можно вычислять
размеры многомерных массивов. Предположим, имеется следующий двумерный массив:
И нам бы
хотелось узнать, сколько у него строк и столбцов. Если применить операцию 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 будет указывать
на второй массив из трех элементов:
Давайте в этом
убедимся. Выведем значение элемента, на который он ссылается:
После запуска
программы увидим значение 4. Соответственно, если смещать адрес, то будет
получать другие значения этого массива. Например:
увидим значение
2. То есть, указатель p_row позволяет
работать с этим двумерным массивом, как с одномерным.
Но как нам
определить указатель именно на двумерный массив и работать с ним как с
двумерным? Не вдаваясь в длительные разъяснения, сразу приведу синтаксис
объявления таких указателей:
<тип данных>
(* <имя указателя>)[вторая размерность];
В нашем случае
указатель на двумерный массив можно объявить следующим образом:
char (*p_ar)[3] = game_pole;
Обратите
внимание на круглые скобки. Они здесь обязательны, так как приоритет операции []
выше, чем операции *. Если бы мы прописали без круглых скобок:
то это был бы
массив из трех указателей. Но с круглыми скобками получаем уже указатель на
двумерный массив, у которого вторая размерность равна трем.
Пользоваться указателем
p_ar можно также, как
и указателем game_pole. Изначально p_ar ссылается на
первый одномерный массив из трех элементов. Поэтому команда:
вернет второй
элемент первого массива, то есть, число 2. Для перехода к следующей строке,
достаточно увеличить первый индекс, например, так:
прочитаем третье
значение второго массива из трех элементов, то есть, число 6. И так далее.
Надо сказать,
что в практике программирования очень редко прибегают к указателям на двумерные
массивы. Они несколько неудобны, т.к. нам приходится явно указывать вторую
размерность. Гораздо удобнее и универсальнее пользоваться указателями на
одномерные массивы, которые достаточно просто можно воспринимать и как
двумерные и даже как многомерные.