1. Вступ
Коли ви пишете програму, дуже швидко зʼясовується, що одного числа замало. Треба зберегти, наприклад, 7 значень кроків за тиждень, 12 місяців витрат або 10 оцінок за контрольні. Можна, звісно, завести змінні x1, x2, x3… але це шлях у темний бік, де код починає нагадувати серіал на 500 серій без сценариста.
C‑масив — це «коробка» фіксованого розміру, у якій лежить N елементів одного типу, а до кожного елемента можна звернутися за номером, тобто індексом.
Синтаксис T arr[N]
Запис має такий вигляд:
T arr[N];
Де:
- T — тип елементів (int, double, char тощо)
- arr — імʼя масиву
- N — кількість елементів (фіксована)
Важливо одразу вловити головну думку: масив — це один обʼєкт, що містить N елементів. Не «N окремих змінних», а саме один цілісний обʼєкт.
Розмір задається одразу й не змінюється
Дуже поширена ментальна пастка новачка: здається, що масив — це як список, який можна розтягувати. Але C‑масив — не про це. Його розмір задається в момент оголошення і далі не змінюється. Якщо ви оголосили int a[5];, то це завжди 5 комірок, і «додати шосту» без зміни інструмента вже не вийде. Про інші інструменти поговоримо пізніше, а сьогодні тримаємо фокус.
У навчальних задачах і в реальному коді варто виробити гарну звичку: зберігати розмір масиву не «магічним числом», а константою часу компіляції. Тут нам підійдуть constexpr і тип std::size_t (ми вже обговорювали, що розміри та індекси зазвичай живуть саме в size_t).
Мініприклад: оголосімо тижневий трекер кроків. Наш мініпроєкт на сьогодні назвемо StepStats.
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t DAYS = 7;
int steps[DAYS] = {}; // усі елементи дорівнюють 0
std::cout << DAYS << '\n'; // 7
}
Зверніть увагу на = {} — це не «декор», а ремінь безпеки: масив одразу отримує передбачувані значення.
Розмір масиву має бути відомий під час компіляції
Довжина масиву має бути відома на момент компіляції програми. Так, саме так. Такий код не працюватиме:
#include <cstddef>
#include <iostream>
int main() {
int n;
std::cin >> n;
int arr[n]; // ❌ у стандартному C++ це помилка
}
Річ у тім, що C++ — дуже стара мова, і під час її створення це вважалося нормою.
2. Індексація: від 0 до N - 1
Тепер найважливіша частина, через яку ламаються долі, дедлайни й інколи клавіатури: індекси.
У масиву з N елементів допустимі такі індекси:
- перший елемент: arr[0]
- останній елемент: arr[N - 1]
Тобто діапазон індексів — від 0 до N-1 включно.
Це історична традиція C/C++: індекс — це «зсув від початку». Зсув до першого елемента дорівнює нулю, тому й перший індекс — 0.
Ось зручна табличка, щоб краще це закріпити:
| Кількість елементів N | Перший індекс | Останній індекс |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Покажімо це в коді й заодно потренуймося не писати «останній елемент = arr[N]», бо це неправда:
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t N = 5;
int a[N] = {1, 2, 3, 4, 5};
std::cout << a[0] << '\n'; // 1
std::cout << a[N - 1] << '\n'; // 5
}
3. Обхід масиву циклом: межа i < N
Коли у вас є масив, майже завжди наступний крок — пройтися по ньому циклом. І тут виникає легендарна помилка «off‑by‑one»: промахнулися на одиницю — і ласкаво просимо до країни дивностей.
Правильна форма циклу для масиву довжини N:
for (std::size_t i = 0; i < N; ++i) {
// працюємо з arr[i]
}
Чому строго < N, а не <= N? Тому що arr[N] — це вже не елемент масиву. Останній — arr[N - 1].
Схема, яку корисно тримати в голові:
flowchart LR
A["i = 0"] --> B{"i < N ?"}
B -- так --> C["використовуємо arr[i]"]
C --> D["++i"]
D --> B
B -- ні --> E["кінець циклу"]
І приклад для StepStats: виведемо всі 7 значень. Поки що вони нульові.
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t DAYS = 7;
int steps[DAYS] = {};
for (std::size_t i = 0; i < DAYS; ++i) {
std::cout << steps[i] << ' '; // 0 0 0 0 0 0 0
}
std::cout << '\n';
}
4. Ініціалізація та введення масиву
Переходимо до теми, де новачки часто бачать у виведенні «сміття» й починають підозрювати, що компʼютер вас ненавидить. Насправді ні. Він просто буквально виконує те, що ви написали.
Повна ініціалізація {...}
Якщо ви задали значення всім елементам, усе просто й красиво:
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t N = 3;
int a[N] = {10, 20, 30};
std::cout << a[1] << '\n'; // 20
}
Часткова ініціалізація {10, 20}
Якщо ви вказали менше значень, ніж розмір масиву, то «хвіст» заповниться нулями. Це приємна властивість спискової ініціалізації, на яку можна спиратися в навчальних задачах.
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t N = 5;
int a[N] = {10, 20}; // решта елементів стане 0
for (std::size_t i = 0; i < N; ++i) {
std::cout << a[i] << ' '; // 10 20 0 0 0
}
std::cout << '\n';
}
Нульова ініціалізація = {}
Якщо ви хочете, щоб усі елементи були 0, пишіть = {}. Це один із найкращих способів отримати передбачуваний результат.
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t N = 4;
double x[N] = {}; // 0.0 0.0 0.0 0.0
std::cout << x[0] << '\n'; // 0
}
Чому «неініціалізований» масив небезпечний
Важливе попередження. Якщо ви напишете:
int a[5];
…то елементи не зобовʼязані бути нулями. Там може бути що завгодно. Не тому, що C++ шкідливий, а тому, що ви попросили створити масив і не сказали, якими значеннями його заповнити.
Ми не показуватимемо приклад, який демонструє «сміття», тому що це залежить від компілятора та середовища, а навчальний приклад має бути відтворюваним. Але правило просте: якщо масив має починатися «з чистого аркуша», використовуйте = {}.
У StepStats це особливо логічно: поки користувач не ввів кроки, значення мають бути нульовими, а не «випадковими числами, які чудово мотивують на спорт, але тільки якщо ви не знаєте, що це сміття».
Шаблон «прочитати N значень»
Тепер зробімо перший корисний крок у нашому StepStats: введемо кроки за тиждень.
Тут є важливий момент: введення через std::cin не гарантує, що користувач введе коректні числа. Але повноцінну обробку помилок введення ми сьогодні не ускладнюємо — просто тренуємо механіку масиву й циклів.
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t DAYS = 7;
int steps[DAYS] = {};
for (std::size_t d = 0; d < DAYS; ++d) {
std::cin >> steps[d]; // зчитуємо 7 чисел
}
}
А тепер додамо виведення, щоб перевірити, що ми справді все зберегли, а не лише прочитали й забули:
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t DAYS = 7;
int steps[DAYS] = {};
for (std::size_t d = 0; d < DAYS; ++d) std::cin >> steps[d];
for (std::size_t d = 0; d < DAYS; ++d) std::cout << steps[d] << ' ';
std::cout << '\n';
}
5. Чому вихід за межі — це гірше, ніж просто помилка
У мовах із жорсткою перевіркою меж, або в контейнерах, які вміють їх перевіряти, ви зазвичай отримаєте зрозумілу помилку. У C‑масивах усе суворіше: якщо ви звернулися до arr[N] або arr[999], компілятор часто не може цього відстежити, а під час виконання ви можете отримати непередбачувану поведінку.
Це називають UB (undefined behavior) — поведінкою, яку стандарт не зобовʼязаний визначати. Людською мовою: «може працювати, може падати, може тихо зіпсувати дані, а може вдавати, що все нормально, щоб зламатися завтра».
Тема UB загалом дуже велика, але сьогодні нам достатньо одного практичного правила: ніколи не звертайтеся до елементів поза 0..N-1. І так, це стосується й випадкового <= замість <.
До речі, сам факт, що в стандарті окремо обговорюють індексування та його тонкощі, на кшталт питань «array subscripting and xvalues», натякає: ця операція не така проста, як здається.
6. Часті помилки в коді: приклади та профілактика
Зараз буде розділ, який економить години життя. Помилки тут максимально людські: ви не «поганий програміст», ви просто вперше опинилися у світі, де 0 — це «перший», а N — це «вже повз».
i <= N замість i < N
Помилка виглядає невинно: «я хочу пройти N елементів». Але при i <= N ви зробите одну зайву ітерацію й полізете в arr[N].
Поганий код (не робіть так):
for (std::size_t i = 0; i <= N; ++i) {
std::cout << arr[i];
}
Хороший код:
for (std::size_t i = 0; i < N; ++i) {
std::cout << arr[i];
}
Плутанина між «останнім елементом» і arr[N]
Це майже завжди «помилка мислення»: здається, що якщо елементів N, то останній індекс теж N. Але індекси починаються з 0, тож останній — N - 1.
У StepStats останній день тижня, сьомий за рахунком для людини, має індекс 6.
«Магічні числа» по всьому коду
Якщо ви пишете 7 у пʼяти місцях, то майже гарантовано одного разу зміните 7 на 10 у чотирьох місцях і забудете про пʼяте. Це класика.
Замість цього тримайте константу:
constexpr std::size_t DAYS = 7;
І використовуйте її всюди.
Індекс у int і дивні порівняння
Технічно int i = 0; i < DAYS; ++i часто «працює». Але коли типи змішуються, можна наразитися на неприємні неявні перетворення.
Ми вже бачили, що розміри — це зазвичай std::size_t, тож і індекс у таких циклах логічно робити std::size_t. Це не «тому що так прийнято», а тому, що так менше шансів випадково порівняти «знакове» з «беззнаковим» і отримати сюрприз.
7. Типові помилки під час роботи з C‑масивами
Помилка № 1: вихід за межі масиву через <=.
Ця помилка особливо підступна тим, що виглядає «майже правильно», а наслідки можуть проявитися не одразу. Один зайвий крок циклу — і ви звертаєтеся до памʼяті, яка не є елементом масиву. Підсумком може бути що завгодно: від «ніби працює» до падіння на рівному місці.
Помилка № 2: читання неініціалізованих елементів.
Якщо ви оголосили int a[10]; і одразу друкуєте a[0], ви читаєте значення, яке самі не задавали. Іноді це 0, іноді ні — і ви не повинні на це розраховувати. Якщо потрібен передбачуваний старт, використовуйте = {} або заповнюйте масив введенням до першого читання.
Помилка № 3: «магічні числа» замість єдиної константи розміру.
Коли розмір масиву записаний як 7 в одному місці, 7 в іншому й ще 7 в умові циклу, код швидко перестає бути керованим. Один раз змінили розмір — і забули виправити цикл або виведення. Константа constexpr std::size_t N = ...; робить код значно надійнішим.
Помилка № 4: неправильне розуміння «останнього елемента».
Новачки часто пишуть arr[N], думаючи, що це останній елемент. Насправді останній елемент — arr[N - 1], тому що індексація починається з нуля. Ця помилка неприємна тим, що виглядає логічно по-людськи, але в програмуванні логіка тут інша.
Помилка № 5: спроба зробити масив «динамічним» через int n; cin >> n; int a[n];.
У стандарті C++ розмір C‑масиву має бути відомий під час компіляції. Деякі компілятори дозволяють такий код як розширення, але це непереносне рішення, і в навчальному контексті його зазвичай вважають помилковим підходом. Якщо розмір відомий заздалегідь — використовуйте C‑масив; якщо ні, потрібен інший інструмент, але це вже не тема сьогоднішньої лекції.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ