JavaRush /Курси /C++ SELF /Знайомство з масивами: T arr[N]

Знайомство з масивами: T arr[N]

C++ SELF
Рівень 11 , Лекція 1
Відкрита

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 Перший індекс Останній індекс
1
0
0
2
0
1
5
0
4
7
0
6

Покажімо це в коді й заодно потренуймося не писати «останній елемент = 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‑масив; якщо ні, потрібен інший інструмент, але це вже не тема сьогоднішньої лекції.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ