JavaRush /Курсы /C++ SELF /2D‑массивы: идея layout и индексации

2D‑массивы: идея layout и индексации

C++ SELF
11 уровень , 4 лекция
Открыта

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 (непредсказуемое поведение), то есть «может работать, может не работать, может сделать вид, что работает, а потом внезапно сломать вам вечер».

Полезно один раз увидеть индексы как таблицу:

r\c
0 1 2
0
m[0][0]
m[0][1]
m[0][2]
1
m[1][0]
m[1][1]
m[1][2]

И маленький пример «взяли конкретную клетку»:

#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]; создаёт массив, но не гарантирует, что там нули. Если вы потом печатаете «что там лежит» до ввода — вы читаете мусор. Если нужен безопасный старт, пишите = {} или {} и спите спокойно (по крайней мере в этой части жизни).

1
Задача
C++ SELF, 11 уровень, 4 лекция
Недоступна
Таблица тренировки
Таблица тренировки
1
Задача
C++ SELF, 11 уровень, 4 лекция
Недоступна
Суммы рядов
Суммы рядов
1
Задача
C++ SELF, 11 уровень, 4 лекция
Недоступна
Максимум матрицы
Максимум матрицы
1
Задача
C++ SELF, 11 уровень, 4 лекция
Недоступна
Индексы памяти
Индексы памяти
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ