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

Урок разбирает двумерные массивы (матрицы): как они объявляются, почему в памяти лежат построчно одним непрерывным блоком и как обходить их вложенными циклами.
Двумерный массив в C — это не «таблица», а один непрерывный блок памяти, разложенный построчно. Понимание этой раскладки объясняет и доступ по индексам, и поведение при передаче в функции.

Матрица объявляется с двумя размерами — строками и столбцами. Доступ к элементу — по двум индексам:

int grid[2][3] = {
    {1, 2, 3},      // строка 0
    {4, 5, 6}       // строка 1
};

printf("%d\n", grid[0][0]);   // 1
printf("%d\n", grid[1][2]);   // 6

grid[0][1] = 99;              // меняем элемент строки 0, столбца 1

Первый индекс — номер строки, второй — столбца. Несмотря на «двумерную» запись, в памяти эти элементы лежат подряд, строка за строкой.

Как работает под капотом

Память линейна, поэтому двумерность — иллюзия. Компилятор раскладывает матрицу построчно (row-major) и вычисляет адрес по формуле:

int grid[2][3] = {{1,2,3},{4,5,6}};

Логически (как мы думаем):     В памяти (как на самом деле):
   столбцы 0  1  2               один непрерывный блок:
строка 0 [1][2][3]               [1][2][3][4][5][6]
строка 1 [4][5][6]                ^строка0^  ^строка1^

Адрес grid[i][j] = база + (i * ЧИСЛО_СТОЛБЦОВ + j) * sizeof(int)

grid[1][2] -> база + (1*3 + 2)*4 = база + 5*4
           -> шестой элемент -> 6

Вот почему число столбцов всегда нужно знать: оно входит в формулу адреса. При передаче двумерного массива в функцию вы обязаны указать размер столбцов: void f(int m[][3], int rows) — иначе компилятор не сможет вычислять адреса строк.

// Обход матрицы вложенными циклами
int grid[2][3] = {{1,2,3},{4,5,6}};
int sum = 0;
for (int i = 0; i < 2; i++) {        // по строкам
    for (int j = 0; j < 3; j++) {    // по столбцам
        sum += grid[i][j];
    }
}
printf("Сумма: %d\n", sum);   // 21

Частые ошибки

  • Перепутанные индексы. grid[столбец][строка] вместо grid[строка][столбец] — обращение не туда.
  • Выход за границы по любому измерению. Как и в одномерном массиве, проверки нет: grid[2][0] при двух строках — чужая память.
  • Пропуск размера столбцов при передаче в функцию. Без него компилятор не вычислит раскладку строк.
  • Перепутанный порядок циклов. Обход по столбцам во внешнем цикле работает, но менее эффективен из-за раскладки в памяти.

Best practices

  • Запомните порядок: первый индекс — строка, второй — столбец. Держите его единообразным во всём коде.
  • Обходите матрицу «строка за строкой» (внешний цикл по строкам) — это совпадает с раскладкой в памяти и работает быстрее.
  • При передаче в функцию всегда указывайте размер столбцов и передавайте число строк отдельным параметром.

Двумерность в Python обычно делают списком списков, но смоделируем именно C-раскладку: один плоский список плюс формула адреса. Так видна суть row-major.

# Эмулируем 2D-массив C как ПЛОСКИЙ блок памяти (row-major)
ROWS, COLS = 2, 3
flat = [1, 2, 3, 4, 5, 6]      # тот же блок, что и в C

def at(i, j):
    # формула адреса: i * COLS + j  (как в C)
    return flat[i * COLS + j]

print("grid[0][0] =", at(0, 0))   # 1
print("grid[1][2] =", at(1, 2))   # 6

total = 0
for i in range(ROWS):
    for j in range(COLS):
        total += at(i, j)
print("Сумма матрицы:", total)    # 21

Та же логика на Python ▶ — формула i * COLS + j — это ровно то, что вычисляет компилятор C. Двумерность создаётся арифметикой над плоской памятью.

Динамические двумерные массивы

Статическая матрица grid[2][3] удобна, но её размеры фиксированы на этапе компиляции. Когда размеры известны только в рантайме, двумерность собирают из динамической памяти, и тут есть два подхода. Первый, простой и быстрый, — один большой плоский блок malloc(rows * cols * sizeof(int)) с доступом по знакомой формуле arr[i * cols + j]: данные лежат подряд, дружелюбны к кэшу, и освобождать нужно один блок. Второй — «массив указателей на строки»: массив int *, каждый элемент которого указывает на отдельно выделенную строку. Он позволяет строки разной длины и привычный синтаксис arr[i][j], но требует несколько malloc и стольких же free, а память разбросана. Выбор между ними — типичное инженерное решение C: простота и скорость против гибкости.

Итоги

Двумерный массив в C — это непрерывный блок, разложенный построчно (row-major). Адрес элемента вычисляется по формуле i * число_столбцов + j, поэтому число столбцов критично и обязательно при передаче в функцию. Доступ — по двум индексам (строка, столбец), обход — вложенными циклами. Границы по-прежнему не проверяются, а обход «строка за строкой» эффективнее из-за раскладки в памяти.

Проверьте себя
1. Как двумерный массив grid[2][3] располагается в памяти?
AКак отдельные несвязанные строки в разных местах
BКак один непрерывный блок, разложенный построчно (row-major)
CКак столбцы один за другим
DДвумерные массивы в C хранятся в куче
2. Почему при передаче двумерного массива в функцию обязательно указывать число столбцов?
AДля красоты сигнатуры
BЧисло столбцов входит в формулу адреса элемента (i * столбцы + j); без него компилятор не вычислит раскладку строк
CИначе массив скопируется целиком
DЭто не обязательно