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
}
Якщо вам потрібен передбачуваний старт, а він майже завжди потрібен, то = {} або просто {} — це ваш невеликий щит від хаосу.
2. 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, а між рядками ми переходимо множенням.
3. Практичний приклад: температури за містами й днями
Щоб не залишати 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
}
Якщо захочете додати рядок заголовка з номерами днів, це теж можна зробити звичайним циклом — без нових тем. Важливо лише памʼятати: виведення заголовка — це окремий прохід по стовпцях.
4. Ментальна модель «масив масивів»
У 2D‑масивах дуже легко почати думати: «це якась особлива структура». І тут корисно повернутися до простої моделі: m — масив рядків, а кожен рядок — масив елементів. Тоді багато речей одразу стають логічними. Наприклад, m[r] — це рядок; у нього є C елементів; тому m[r][c] — це елемент рядка.
Навіть читання оголошення починає сприйматися спокійніше. int m[R][C]; означає: «m зберігає R рядків, а кожен рядок зберігає C цілих чисел». І коли ви пишете вкладені цикли, то, по суті, формулюєте просту ідею: «обійти всі рядки, а в кожному рядку — всі елементи». Це майже пояснення алгоритму звичайними українськими словами — тільки компілятору потрібні фігурні дужки.
Якщо тримати в голові цю модель, ви рідше плутатимете індекси місцями й рідше ставитимете неправильні межі. А ще вона підготує вас до моменту, коли ви побачите, що «рядок» можна передавати й використовувати окремо, — але це вже поза межами сьогоднішньої теми.
5. Типові помилки під час роботи з 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]; створює масив, але не гарантує, що там будуть нулі. Якщо ви потім друкуєте «що там лежить» до введення, то читаєте сміття. Якщо потрібен безпечний старт, пишіть = {} або {} і спіть спокійно — принаймні в цій частині життя.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ