Menu

Двумерные массивы в C: многомерные массивы, размещение в памяти и матрицы

Как объявлять, инициализировать и перебирать двумерные массивы в C, что на самом деле означает построчное размещение в памяти, почему при передаче двумерного массива в функцию обязательно число столбцов, и разбор примера с матрицами.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Многомерный массив - это массив, элементы которого сами являются массивами. 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, а остальные десять оставляет нулевыми.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ