1. Синтаксис 2D‑массива и базовые операции
Когда вы впервые видите двумерный массив, мозг обычно делает две вещи: пытается представить Excel и вспоминает школьную алгебру с матрицами (иногда с лёгкой дрожью). Но в программировании 2D‑массив — это не «страшная математика», а просто удобная форма хранения данных, которые естественно выглядят как таблица: строки и столбцы, клетки, координаты.
Например, это может быть таблица оценок (студенты × контрольные), расписание (дни × пары), маленькое игровое поле (3×3), или «температура по городам и дням недели». В таких задачах один индекс “i” уже не отражает реальность — хочется сказать: “строка r, столбец c”, и получить значение в этой клетке.
Объявление T m[R][C]
Запись int m[R][C] на первом знакомстве выглядит как заклинание, особенно если вы ещё не привыкли к квадратным скобкам. Хорошая новость: это заклинание переводится на человеческий. m — это массив из R элементов, и каждый из этих R элементов — ещё один массив из C элементов типа int. То есть это буквально «массив массивов».
Обычно размеры делают константами времени компиляции. На нашем уровне это означает: используем constexpr std::size_t, чтобы случайно не перепутать числа по коду и не словить «магические константы» в пяти местах.
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t R = 2;
constexpr std::size_t C = 3;
int m[R][C] = {}; // все элементы 0
std::cout << "Rows=" << R << " Cols=" << C << '\n'; // Rows=2 Cols=3
}
Обратите внимание: это фиксированный двумерный массив. Он создаётся целиком и сразу, и размеры R и C не меняются по ходу программы (по крайней мере в рамках этой темы).
Индексация m[r][c]
С индексацией в 2D‑массиве главное — не пытаться «угадывать» и не надеяться на удачу. Правило простое: m[r][c] читается как «в массиве m взять строку r, а в этой строке взять столбец c». То есть первый индекс выбирает строку, второй — элемент внутри строки. И всё — никакой магии, только дисциплина.
Границы такие же честные, как и в одномерном массиве. Для R строк допустимы r = 0..R-1, для C столбцов допустимы c = 0..C-1. Любой выход за границы — это UB (непредсказуемое поведение), то есть «может работать, может не работать, может сделать вид, что работает, а потом внезапно сломать вам вечер».
Полезно один раз увидеть индексы как таблицу:
|
0 | 1 | 2 |
|---|---|---|---|
| 0 | |
|
|
| 1 | |
|
|
И маленький пример «взяли конкретную клетку»:
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t R = 2, C = 3;
int m[R][C] = {{1, 2, 3}, {4, 5, 6}};
std::cout << m[1][2] << '\n'; // 6
}
Обход: вложенные циклы
Когда появляется два индекса, появляются и два цикла. Это звучит очевидно, но именно здесь рождаются самые популярные ошибки: перепутали, какой цикл за что отвечает, или поставили неверную границу. Надёжный шаблон такой: внешний цикл идёт по строкам, внутренний — по столбцам. Так чтение кода совпадает с тем, как мы обычно читаем таблицы: строка за строкой.
Почувствуйте, как хорошо это «ложится на язык»: “для каждой строки r, для каждого столбца c”. Это почти русский язык, только с точками с запятой.
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t R = 2, C = 3;
int m[R][C] = {{1, 2, 3}, {4, 5, 6}};
for (std::size_t r = 0; r < R; ++r) {
for (std::size_t c = 0; c < C; ++c) {
std::cout << m[r][c] << ' ';
}
std::cout << '\n';
}
// 1 2 3
// 4 5 6
}
Заметьте, мы используем std::size_t для индексов — это дружит с размерами и меньше провоцирует странные сравнения.
Инициализация: фигурные скобки «в два этажа»
Инициализация двумерного массива — это тот момент, когда фигурные скобки начинают выглядеть как матрёшка. Но идея очень простая: внешние скобки задают строки, внутренние — значения в строке. Если вы видите {{...}, {...}, {...}}, это почти всегда означает «три строки», и в каждой строке — свой набор значений.
Можно инициализировать полностью, частично или сделать «нулевую» инициализацию. Частичная инициализация полезна, когда вы задаёте только несколько элементов, а остальные хотите гарантированно получить нулями (а не «мусором»).
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t R = 3, C = 3;
int grid[R][C] = {
{1, 2}, // третий элемент станет 0
{3}, // второй и третий станут 0
{} // вся строка нули
};
std::cout << grid[0][2] << ' ' << grid[2][1] << '\n'; // 0 0
}
Если вам нужен предсказуемый старт (почти всегда нужен), то = {} или просто {} — это ваш маленький щит от хаоса.
3. Layout и связь с плоским массивом
Как 2D‑массив лежит в памяти
Слово layout звучит так, будто мы сейчас будем верстать сайт (и спорить, где поставить кнопку). Но тут смысл другой: как данные физически уложены в памяти. Для двумерного массива важно знать хотя бы одну идею: элементы каждой строки лежат подряд, и потом идёт следующая строка. Это называется размещением «по строкам» (row-major order) — так принято в C/C++.
Почему это полезно знать? Во‑первых, чтобы не путаться, почему формула «плоского индекса» выглядит именно так. Во‑вторых, чтобы понимать, почему обычно обход «строка‑за‑строкой» считается естественным. Мы не будем уходить в оптимизацию, но модель «строки подряд» пригодится уже сейчас, когда мы будем связывать двумерную индексацию с одномерной.
Представим 2×3:
m[0][0] m[0][1] m[0][2] m[1][0] m[1][1] m[1][2]
То есть сначала три элемента нулевой строки, потом три элемента первой строки.
Проверка на пальцах через sizeof тоже даёт подсказки:
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t R = 2, C = 3;
int m[R][C] = {};
std::cout << sizeof(m) << '\n'; // размер всего 2D-массива в байтах
std::cout << sizeof(m[0]) << '\n'; // размер одной строки (C элементов) в байтах
}
Если у вас int по 4 байта, то sizeof(m[0]) будет 3 * 4 = 12, а sizeof(m) будет 2 * 12 = 24. Важная мысль: строка m[0] — это тоже массив (из C элементов).
Формула r * C + c
Теперь тот самый трюк, который часто объясняет 2D‑индексацию лучше любых слов. Если строки лежат подряд, то клетка (r, c) в плоском массиве длины R*C будет иметь индекс r * C + c. Здесь C — именно число столбцов (ширина строки). Это критично: множитель — всегда «сколько элементов в строке», потому что мы «перешагиваем» через целые строки.
Сравним доступ:
- в 2D: m[r][c]
- в 1D: flat[r * C + c]
Вот маленькая демонстрация, где данные одинаковые:
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t R = 2, C = 3;
int m[R][C] = {{1, 2, 3}, {4, 5, 6}};
int flat[R * C] = {1, 2, 3, 4, 5, 6};
std::size_t r = 1, c = 2;
std::cout << m[r][c] << '\n'; // 6
std::cout << flat[r * C + c] << '\n'; // 6
}
Это не значит, что мы должны всегда хранить всё «плоско». Это значит, что вы понимаете, что происходит под капотом: строка — это блок длины C, и мы прыгаем по строкам умножением.
4. Практический пример: температуры по городам и дням
Чтобы не оставлять 2D‑массивы как «теорию про матрицы», соберём маленький сценарий. Пусть у нас есть несколько городов (строки) и несколько дней (столбцы). Мы хотим ввести температуры и красиво вывести таблицей. Обратите внимание: это всё ещё базовые конструкции — ввод, вложенные циклы, аккуратные границы.
Сначала просто создадим и заполним таблицу:
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t Cities = 2;
constexpr std::size_t Days = 3;
int t[Cities][Days] = {};
for (std::size_t city = 0; city < Cities; ++city) {
for (std::size_t day = 0; day < Days; ++day) {
std::cin >> t[city][day];
}
}
std::cout << "OK\n"; // OK
}
Теперь выведем таблицу. Здесь удобно вспомнить форматирование вывода (мы уже видели setw раньше, и он идеально подходит для табличек). Если вам кажется, что это «слишком красиво для новичка», то поверьте: таблица без форматирования обычно выглядит как «цифры поссорились и разбежались».
#include <cstddef>
#include <iomanip>
#include <iostream>
int main() {
constexpr std::size_t Cities = 2;
constexpr std::size_t Days = 3;
int t[Cities][Days] = {{-1, 0, 2}, {3, 1, -2}};
for (std::size_t city = 0; city < Cities; ++city) {
for (std::size_t day = 0; day < Days; ++day) {
std::cout << std::setw(4) << t[city][day];
}
std::cout << '\n';
}
// -1 0 2
// 3 1 -2
}
Если захотите сделать «шапку» с номерами дней — это тоже делается обычным циклом (и не требует новых тем). Важно лишь помнить: печать шапки — отдельный проход по столбцам.
5. Ментальная модель «массив массивов»
В 2D‑массивах очень легко начать думать “это какая-то особая структура”. И вот тут полезно вернуться к простой модели: m — массив строк, а каждая строка — массив элементов. Тогда многие вещи внезапно становятся логичными. Например, m[r] — это строка; у неё есть C элементов; поэтому m[r][c] — элемент строки.
Даже чтение объявления начинает восприниматься спокойнее. int m[R][C]; означает: “m хранит R строк, а каждая строка хранит C целых чисел”. И когда вы пишете вложенные циклы, вы по сути пишете: “обойти все строки, и в каждой строке обойти все элементы”. Это практически объяснение алгоритма русскими словами, только компилятору нужны фигурные скобки.
Если вы держите в голове эту модель, вы реже путаете индексы местами и реже ставите неправильные границы. А ещё это подготовит вас к моменту, когда вы увидите, что «строка» может передаваться и использоваться отдельно — но это уже за границами сегодняшней темы.
6. Типичные ошибки при работе с 2D‑массивами
Ошибка №1: перепутали границы R и C в циклах.
Очень частая ситуация: внешний цикл идёт до C, внутренний до R, а потом вы удивляетесь, почему программа читает «не туда» или выходит за границы. Лечится договорённостью об именах: rows/cols или r/c, и дисциплиной: r < R, c < C.
Ошибка №2: использовали <= вместо <.
В двумерных массивах эта ошибка становится «в два раза веселее», потому что можно выйти за границы либо по строкам, либо по столбцам. Формулировка должна быть как мантра: последний допустимый индекс — R-1 или C-1, поэтому в цикле всегда r < R и c < C.
Ошибка №3: перепутали порядок индексов и начали писать m[c][r].
Компилятор не будет ругаться: это всё ещё «два числа в скобках». Но смысл станет неправильным, а баги — коварными. Спасает единое правило: всегда m[row][col], и если вы печатаете таблицу, внешний цикл — строки.
Ошибка №4: неверный множитель в формуле плоского индекса.
Если вы переводите (r, c) в flat[...], множитель должен быть C, а не R. В этом месте ошибка особенно обидная: данные будут читаться “как будто перемешаны”, и выглядит это как мистика. На деле это просто неверная «ширина строки» в расчёте.
Ошибка №5: забыли про предсказуемую инициализацию.
int m[R][C]; создаёт массив, но не гарантирует, что там нули. Если вы потом печатаете «что там лежит» до ввода — вы читаете мусор. Если нужен безопасный старт, пишите = {} или {} и спите спокойно (по крайней мере в этой части жизни).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ