Многомерный массив - это массив, элементы которого сами являются массивами. int grid[3][4]; - не какой-то особый тип «сетка», а три элемента, каждый из которых является массивом из четырёх значений int, лежащих подряд. Как только это укладывается в голове, остальное следует само: размещение в памяти, арифметика индексов и в остальном необъяснимое правило про передачу в функции.
Двух измерений хватает практически для всего - сетки, таблицы, матрицы, игровые доски, изображения, - поэтому именно с ними эта страница и работает.
Объявление и инициализация
int grid[3][4]; // 3 строки, 4 столбца - 12 значений int
Первое число - сколько строк, второе - сколько столбцов. Инициализаторы можно писать плоским списком или с внутренними фигурными скобками; скобки стоит использовать, потому что они показывают форму.
Форма int e[][3] важна: число строк можно оставить пустым и позволить инициализатору его определить, а вот число столбцов не опционально никогда. Почему - в следующем разделе.
Построчное размещение
C хранит двумерный массив в построчном порядке (row-major): целиком строка 0, затем целиком строка 1 и так далее, одним сплошным блоком памяти. Никакого скрытого массива указателей на строки за кулисами нет.
int grid[3][4] = {{ 1, 2, 3, 4},
{ 5, 6, 7, 8},
{ 9,10,11,12}};
как вы это представляете как это лежит в памяти на самом деле
+----+----+----+----+
| 1 | 2 | 3 | 4 | строка 0 +--+--+--+--+--+--+--+--+--+--+--+--+
+----+----+----+----+ | 1| 2| 3| 4| 5| 6| 7| 8| 9|10|11|12|
| 5 | 6 | 7 | 8 | строка 1 +--+--+--+--+--+--+--+--+--+--+--+--+
+----+----+----+----+ \_ строка 0 _/\_ строка 1 _/\_ стр 2 _/
| 9 | 10 | 11 | 12 | строка 2
+----+----+----+----+ grid[i][j] имеет индекс элемента i*4 + j
Эта формула, i * число_столбцов + j, и есть весь механизм - и именно поэтому компилятор обязан знать число столбцов, чтобы вообще что-либо проиндексировать. Число строк в вычисление не входит никогда.
Размещение можно увидеть напрямую, напечатав адреса:
Адреса растут на sizeof(int) без пропусков, в том числе там, где одна строка заканчивается и начинается следующая. Плоский цикл это доказывает: flat[k] проходит все двенадцать элементов как единый ряд.
У такого размещения есть и следствие для производительности, о котором стоит знать: перебор «строки, затем столбцы» обращается к памяти по порядку, что нравится кэшу процессора. Поменяйте вложенность так, чтобы внутренний цикл шёл вниз по столбцу, - и каждый шаг будет прыгать через целую строку, а на большом массиве это может оказаться в несколько раз медленнее.
Вложенные циклы
Двум измерениям нужны два цикла for: внешний выбирает строку, внутренний проходит по столбцам этой строки.
Называйте счётчики по смыслу (i/row для строк, j/col для столбцов) и соблюдайте порядок - везде grid[row][col]. Половина всех багов с двумерными массивами - это переставленная местами пара индексов.
Размеры через #define здесь тоже не украшение: теперь границы циклов и объявление не смогут разъехаться, когда вы поменяете форму массива.
Передача двумерного массива в функцию
Вот правило, на котором спотыкаются все: параметр функции обязан объявлять число столбцов.
Причина - приведение (decay). Передача grid превращает его в указатель на первый элемент, а элементы у него - строки, поэтому тип получается int (*)[4]: указатель на массив из 4 значений int. Чтобы grid[i][j] вообще что-то означало, компилятор должен знать, какова длина одной строки, и это как раз та самая 4. Число строк в типе действительно отсутствует, поэтому оно и путешествует отдельным аргументом.
Обратите внимание: int (*grid)[COLS] и int grid[][COLS] - один и тот же параметр, записанный двумя способами; скобки обязательны, потому что int *grid[COLS] был бы массивом указателей. Это различие разобрано в указателях и массивах.
Если число столбцов известно только во время выполнения, переменно-модифицируемые параметры из C99 позволяют передать его первым:
void print_any(int rows, int cols, int grid[rows][cols]);
rows и cols должны быть объявлены раньше, чем использующий их параметр-массив. Там, где это недоступно, обычной альтернативой служит плоский одномерный массив с ручной арифметикой индексов:
data[i * cols + j] - в точности то, что компилятор пишет за вас в случае фиксированного размера. Сделать это руками стоит одной строки и работает для любой формы, определяемой во время выполнения.
Пример с матрицами
Умножение матриц собирает всю страницу воедино - три вложенных цикла поверх построчного хранения.
Две детали, которые стоит перенять. Внутренний цикл по k соединяет a[i][k] с b[k][j] - один индекс идёт по строке, другой по столбцу. А транспонирование начинает внутренний цикл с j = i + 1: начав с 0, вы поменяли бы каждую пару дважды и оставили матрицу неизменной.
Три измерения и дальше
Схема расширяется, и правило про параметры функций тоже: каждое измерение, кроме первого, должно быть объявлено.
На практике три измерения - это тот рубеж, где массивы фиксированного размера становятся неудобными, и большинство кода переходит на плоский блок с вычисляемыми индексами или на массив структур, в котором каждая ось названа по смыслу.
Частые ошибки
- Запись
grid[i, j]. Оператор «запятая» вычисляетi, отбрасывает результат и индексирует поj. Это компилируется. Это неверно. Пишитеgrid[i][j]. - Перестановка индексов.
grid[col][row]читает настоящий элемент, но не оттуда, откуда нужно, так что никакой ошибки, которая бы это поймала, не будет. Держите порядок[row][col]везде. - Пропуск размера столбца в параметре.
void f(int grid[][])не компилируется, и это компилятор вас спасает. - Выход за границы. Как и у любого массива, проверки границ здесь нет.
grid[0][5]на сетке[3][4]молча прочитаетgrid[1][1], потому что данные лежат непрерывно, а арифметике всё равно.
Часто задаваемые вопросы
Как объявить двумерный массив в C?
Укажите два размера в квадратных скобках: int grid[3][4]; объявляет 3 строки по 4 столбца - всего 12 значений int. Читайте это как «массив из 3 элементов, каждый из которых - массив из 4 значений int», потому что именно так C его и хранит.
Как двумерный массив хранится в памяти в C?
В построчном порядке (row-major): сначала все элементы строки 0, затем все элементы строки 1 и так далее, одним непрерывным блоком. grid[i][j] лежит со смещением i * число_столбцов + j элементов от начала - вот почему компилятору нужно именно число столбцов.
Как передать двумерный массив в функцию в C?
Параметр обязан объявлять число столбцов: void print(int grid[][4], int rows) или эквивалентно void print(int (*grid)[4], int rows). Число строк можно опустить, потому что массив приводится к указателю на строку, - но без размера столбца компилятор не сможет вычислить, где начинается строка.
Можно ли обнулить весь двумерный массив при инициализации?
Да: int grid[3][4] = {0}; обнуляет каждый элемент, потому что всё, что вы не перечислили, инициализируется нулём. int grid[3][4] = {{1, 2}}; задаёт первые два элемента строки 0, а остальные десять оставляет нулевыми.