JavaRush /Курси /C++ SELF /std::array<T, N>: size() і begin()/end()

std::array<T, N>: size() і begin()/end()

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

1. Знайомство зі std::array

Якщо ви щойно опанували звичайні масиви int a[5], цілком природно запитати: «Навіщо стандартній бібліотеці ще один масив?». Відповідь практична: C‑масив — історична конструкція, яка має багато переваг і не менше сюрпризів. std::array зберігає переваги — фіксований розмір і швидкість, — але прибирає частину сюрпризів: у нього є методи, розмір не губиться по дорозі, і він добре поєднується із загальними підходами стандартної бібліотеки.

std::array<T, N> — це контейнер стандартної бібліотеки, тобто тип «із коробки», який міститься в заголовку <array> і поводиться як повноцінний обʼєкт із вбудованими можливостями. Він стандартизований і розвивається разом зі стандартом C++, зокрема разом із повʼязаними утилітами для роботи з масивами.

Найважливіше: std::array зберігає рівно N елементів типу T, як і C‑масив. Але робить це «в стилі сучасного C++»: розмір можна запитати, початок і кінець — отримати однаковим способом, а сам тип стає суворішим.

2. Оголошення: N — частина типу

Коли ви вперше бачите std::array<int, 5>, може здатися, що це «якийсь складний шаблон». Так, це шаблон, але ідея проста: T — тип елемента, N — кількість елементів. І ця кількість — не змінна, а частина типу. Тобто std::array<int, 5> і std::array<int, 6> — різні типи.

Почнімо з оголошення й доступу до елементів. Нехай наш міні‑застосунок для рівня 11 — це «термометр на тиждень»: ми зберігаємо 7 температур і рахуємо просту статистику.

#include <array>
#include <cstddef>
#include <iostream>

int main() {
    std::array<int, 7> temps{10, 11, 9, 8, 12, 13, 7};
    std::cout << temps[0] << ' ' << temps[6] << '\n'; // 10 7
}

Тут temps — повноцінний обʼєкт, а не «шматок памʼяті без паспорта». У нього є методи й передбачувана поведінка.

Ще один плюс: N фіксується просто в типі. Якщо ви спробуєте присвоїти масив іншого розміру, компілятор зупинить вас раніше, ніж програма почне «цікаво жити».

3. Ініціалізація: повна, часткова і {}

У новачків з ініціалізацією зазвичай буває два режими. Перший — «я все заповнив явно, я молодець». Другий — «ой, а чому там сміття?». std::array допомагає триматися першого режиму, бо спискова ініціалізація працює дуже передбачувано.

Повна ініціалізація виглядає звично:

#include <array>
#include <iostream>

int main() {
    std::array<int, 3> a{1, 2, 3};
    std::cout << a[1] << '\n'; // 2
}

Часткова ініціалізація теж працює: «хвіст» заповниться нулями (для чисел — 0, для double0.0).

#include <array>
#include <iostream>

int main() {
    std::array<int, 5> a{10, 20}; // решта стануть 0
    std::cout << a[2] << ' ' << a[4] << '\n'; // 0 0
}

Нульова ініціалізація — це коли ви хочете гарантовано стартувати «з чистого аркуша», без сюрпризів:

#include <array>
#include <iostream>

int main() {
    std::array<int, 4> a{}; // усі 0
    std::cout << a[0] << ' ' << a[3] << '\n'; // 0 0
}

Для нашого «термометра на тиждень» це зручно: ми можемо оголосити масив, а потім заповнити його введенням користувача. Але навіть до введення значення будуть передбачуваними, а не в стилі «які байти були в памʼяті».

4. size(): менше магічних чисел

Коли ви працюєте з фіксованим масивом, найпоширеніша помилка — «десь я написав 7, десь 6, а десь узагалі забув оновити». std::array пропонує здоровішу звичку: запитувати розмір в обʼєкта.

size() повертає кількість елементів (тип зазвичай std::size_t).

Перепишімо частину нашого міні‑застосунку: введемо температури й виведемо їх назад, але межу циклу візьмемо з size().

#include <array>
#include <cstddef>
#include <iostream>

int main() {
    std::array<int, 7> temps{};

    for (std::size_t i = 0; i < temps.size(); ++i) {
        std::cin >> temps[i];
    }

    std::cout << "Read " << temps.size() << " values\n"; // наприклад: Read 7 values
}

Річ навіть не в тому, що «так коротше». Річ у тому, що тепер розмір живе в одному місці — у типі std::array<int, 7>, а код обходу не зобовʼязаний памʼятати, що це саме 7. Він просто запитує масив: «скільки вас там?».

Таблиця: C‑масив vs std::array

Властивість int a[7] (C‑масив) std::array<int, 7>
Можна запитати розмір «офіційно» ні (потрібні трюки) так, a.size()
Розмір повʼязаний із типом частково (у типі, але легко губиться при decay) так, і майже не «губиться випадково»
Легко втратити розмір (decay) дуже легко помітно складніше «випадково»
Працює як обʼєкт (копіювання/присвоювання цілком) ні (не присвоюється напряму) так (копіюється й присвоюється як звичайний обʼєкт)

Ця таблиця не робить C‑масив «поганим». Він просто старіший і менш суворий. А ми зараз вчимося писати так, щоб чіткі правила допомагали, а не заважали.

5. begin() і end(): межі діапазону

Із size() ми вже відчуваємо, що std::array — не просто «памʼять», а обʼєкт. Наступний крок — зрозуміти, що в контейнера можна запитати «межі». Для цього й існують begin() і end().

Важливо правильно вловити ідею: begin() — це «точка входу» в послідовність, тобто доступ до першого елемента. end() — це «точка одразу після останнього елемента». Не останній елемент, а саме позиція «після». Це звучить дивно, але це дуже зручна домовленість: діапазон елементів — це завжди [begin, end) (включно з початком і не включно з кінцем).

Візуально це можна уявити так:

flowchart LR
    B["begin()"] --> E0[0] --> E1[1] --> E2[2] --> D[...]
    D --> EN[останній елемент] --> X["end()"]

Чому end() — «після останнього»? Тому що тоді навіть порожній масив описується коректно: якщо елементів немає, begin() == end(). І це дуже зручна загальна логіка.

Для std::array begin()/end() повертають «ітератори» — на цьому етапі сприймайте їх якомога простіше: як «курсор, схожий на вказівник», який уміє рухатися елементами й дозволяє отримати поточний елемент.

Подивімося на мінімальний приклад:

#include <array>
#include <iostream>

int main() {
    std::array<int, 3> a{5, 6, 7};

    auto it = a.begin();          // "вказівник" на перший елемент
    std::cout << *it << '\n';     // 5
}

Тут зʼявляється *it. Так, це оператор «взяти елемент, на який вказує ітератор». Ми не заглиблюємося у велику тему вказівників і памʼяті, але тримаємо просту модель: it вказує на елемент, *it — це сам елемент.

6. Обхід за ітераторами без індексів

Індексні цикли чудові, особливо коли ви тільки вчитеся: i зрозуміло, межа зрозуміла, a[i] — теж зрозуміло. Але інколи зручніше пройтися не через числа, а через межі контейнера. begin()/end() якраз дають універсальний спосіб пройти від початку до кінця, не знаючи розмір «вручну».

Ось такий вигляд має виведення елементів через ітератори:

#include <array>
#include <iostream>

int main() {
    std::array<int, 4> a{10, 20, 30, 40};

    for (auto it = a.begin(); it != a.end(); ++it) {
        std::cout << *it << ' ';
    }
    std::cout << '\n'; // 10 20 30 40
}

Розберімо цей цикл простими словами. Ми ставимо it на початок масиву. Поки it не дійшов до «позиції після останнього» (a.end()), ми друкуємо поточний елемент *it і рухаємо ітератор уперед ++it.

Перевага такого обходу в тому, що це одна й та сама звичка для контейнерів і діапазонів. Мінус для новачка — трохи більше «нових значків» (*it, ++it). Тому тримаймо баланс: індекси — для простоти, begin/end — щоб зрозуміти «межі контейнера» як окрему ідею.

7. Приклад: статистика температур

Зараз ми зберемо невеликий фрагмент логіки в стилі «один файл, один main, ніякої магії». Наше завдання: ввести 7 температур, вивести їх, порахувати суму та середнє. Частину зробимо через індекси, бо так простіше, а частину — через begin/end, бо сьогодні ми саме це й вивчаємо.

Введення і друк через size()

#include <array>
#include <cstddef>
#include <iostream>

int main() {
    std::array<int, 7> temps{};

    for (std::size_t i = 0; i < temps.size(); ++i) {
        std::cin >> temps[i];
    }

    for (std::size_t i = 0; i < temps.size(); ++i) {
        std::cout << temps[i] << ' ';
    }
    std::cout << '\n'; // наприклад: 10 11 9 8 12 13 7
}

Тут ми закріпили дві речі: «розмір запитуємо в масиву» і «межа циклу — саме < size()».

Сума через begin()/end()

Тепер порахуймо суму через ітератори:

#include <array>
#include <iostream>

int main() {
    std::array<int, 7> temps{10, 11, 9, 8, 12, 13, 7};

    int sum = 0;
    for (auto it = temps.begin(); it != temps.end(); ++it) {
        sum += *it;
    }

    std::cout << "sum=" << sum << '\n'; // sum=70
}

Зверніть увагу: тут ми взагалі не використовуємо індекси. Ми просто йдемо від begin() до end().

Середнє: цілі та double

Середнє = сума / кількість. Якщо все int, середнє теж вийде int — із відкиданням дробової частини. Це нормально, якщо ви це розумієте. Якщо хочеться отримати дробове середнє, потрібно привести до double.

#include <array>
#include <iostream>

int main() {
    std::array<int, 7> temps{10, 11, 9, 8, 12, 13, 7};

    int sum = 0;
    for (auto it = temps.begin(); it != temps.end(); ++it) {
        sum += *it;
    }

    double avg = static_cast<double>(sum) / temps.size();
    std::cout << "avg=" << avg << '\n'; // avg=10
}

Так, у прикладі avg вийшло рівно 10, але лише тому, що числа так склалися. В інших наборах могло б вийти, наприклад, 10.4286...

Чому std::array часто безпечніший за C‑масив

Інколи початківці думають, що «контейнер стандартної бібліотеки» обовʼязково повільніший, бо «там усередині, мабуть, магія». У std::array магії майже немає: це тонка оболонка навколо фіксованого блоку елементів. На практиці він схожий на C‑масив, але має нормальний інтерфейс.

Ключовий момент безпеки тут навіть не в тому, що std::array «перевіряє межі» (оператор [] не перевіряє). Безпека в тому, що ви менше пишете вручну. Менше ручної роботи — менше місць, де можна помилитися. size() зменшує шанс «off-by-one», begin/end задають єдиний спосіб обходу, а сам обʼєкт можна передати або скопіювати як ціле, якщо це потрібно, без танців у стилі «це масив, його не можна присвоювати».

Ще одна дуже практична річ: std::array не перетворюється сам собою на «просто адресу першого елемента» так легко, як C‑масив. Тобто вам складніше випадково втратити розмір. Якщо ж вам потрібен «сирий доступ», він теж є, але це вже буде усвідомлений крок.

8. Типові помилки під час роботи зі std::array

Помилка №1: знову писати «магічне число» замість size().
Якщо ви оголосили std::array<int, 7>, а потім робите for (int i = 0; i < 7; ++i), це схоже на ситуацію, коли ви купили розумну пральну машину, але перете руками, бо «так звичніше». Звичка використовувати a.size() окупається дуже швидко: ви змінюєте розмір один раз у типі, а не шукаєте всі «7» по файлу.

Помилка №2: плутати end() з «останнім елементом».
end() — це позиція «після останнього». Якщо ви спробуєте зробити *a.end(), це логічно те саме, що «взяти елемент за межами». Тобто нічого доброго. Якщо потрібен останній елемент за індексом, це a[a.size() - 1]. Якщо говорити мовою ітераторів, то до end() можна дійти, але розіменовувати end() не можна.

Помилка №3: писати умову it <= a.end() в ітераторному циклі.
Майже завжди коректний цикл виглядає як it != a.end() (або it < a.end() для ітераторів випадкового доступу, але це окрема історія). Коли ви пишете <= end(), ви гарантовано потрапляєте в ситуацію «а от зараз я спробую обробити end як елемент», і це закінчиться погано або дивно.

Помилка №4: забути, що size() — це не int.
size() повертає тип розміру (std::size_t). Якщо ви зберігаєте індекс у int, а потім порівнюєте його з size(), компілятор може почати попереджати, а інколи й логіка може дати збій на межах. На цьому етапі курсу розумне правило просте: індекси й розміри тримаємо в std::size_t, і життя стає спокійнішим.

Помилка №5: очікувати, що std::array «автоматично захищає від виходу за межі».
std::array — зручніший і суворіший обʼєкт, але a[i] все одно вимагає, щоб i був у діапазоні 0..size()-1. Контейнер допомагає тим, що дає size() і нормальні межі, але думати головою все одно доведеться.

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