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, для double — 0.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() і нормальні межі, але думати головою все одно доведеться.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ