JavaRush /Курси /C++ SELF /Доступ до елементів std::v...

Доступ до елементів std::vector: [] чи at(), front()/ back(), empty()

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

1. Вступ

Коли ви вперше бачите std::vector, може здатися, що все просто: v[0], v[1] — і готово. Але в реальній програмі індекс майже ніколи не буває «магічно правильним». Він може надходити з введення, обчислюватися за формулою, залежати від умов або просто виявитися неправильним через помилку «off-by-one». Тому доступ до елементів — це не лише синтаксис, а й дисципліна: як зробити так, щоб програма не падала й не псувала дані.

Уявіть, що вектор — це ряд комірок на складі. v.size() каже, скільки комірок справді зайнято коробками. Якщо ви намагаєтеся взяти коробку з комірки, якої не існує, складський робот не стане ввічливо пояснювати, що ви помилилися. Він просто вріжеться в стіну. У програмуванні наслідки бувають ще драматичнішими.

2. Індекси й межі: де можна, а де не можна

Перш ніж вирішувати, що краще — [] чи at(), потрібно раз і назавжди чітко визначити межі.

Якщо size() == n, то коректні індекси:

  • від 0
  • до n - 1
  • і лише якщо n > 0

Цю схему зручно тримати в голові так:

v.size() == 5

індекси:  0   1   2   3   4
елементи: [ ] [ ] [ ] [ ] [ ]

v[5]  — не можна (це «після останнього»)
v[-1] — не можна (і в C++ це ще й окрема пастка)

Розгляньмо невеликий приклад, який друкує розмір і нагадує, який індекс має останній елемент, якщо він існує:

#include <cstddef>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{10, 20, 30};

    std::cout << "розмір = " << v.size() << '\n';                    // розмір = 3
    std::cout << "останній індекс = " << (v.size() - 1) << '\n';     // останній індекс = 2
}

Цей код коректний лише тому, що v не порожній. Якби v був порожнім, вираз v.size() - 1 став би небезпечною ідеєю. Запамʼятаймо просте правило: не віднімайте 1 від розміру, доки не переконаєтеся, що розмір > 0.

Акуратний друк «останнього індексу»

Дуже поширена думка: «хочу надрукувати останній індекс — це size() - 1». Так, але лише якщо size() > 0. І ось тут empty() знову виявляється найзрозумілішим способом не потрапити в халепу.

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v;

    if (!v.empty()) {
        std::cout << "останній індекс = " << (v.size() - 1) << '\n';
    } else {
        std::cout << "немає останнього індексу: вектор порожній\n"; // немає останнього індексу: вектор порожній
    }
}

Чому це важливо? Тому що порожні контейнери — не «рідкість», а цілком нормальний стан програми. Користувач ще нічого не додав, файл виявився порожнім, дані не надійшли — і ось уже size() == 0. Програма має вміти працювати й у такому сценарії, а не лише у світі, де «все заповнено й ідеально».

3. Доступ за індексом: operator[] і at()

operator[]: швидкий доступ без підстрахування

Ось важлива думка, яку легко недооцінити.

v[i] (тобто operator[]) не перевіряє межі. Це максимально швидкий і «довірливий» спосіб доступу: ви кажете «дай елемент i» — і він намагається його дати. Якщо i неправильний, програма починає поводитися некоректно: може впасти, може друкувати сміття, а може й «тихо» зіпсувати дані. І це найгірше: іноді помилка проявляється не одразу, а через 20 рядків коду, коли ви вже забули, де саме схибили.

Мініприклад коректного доступу через []:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{5, 6, 7};

    std::cout << v[0] << '\n'; // 5
    std::cout << v[2] << '\n'; // 7
}

А ось приклад, який компілюється, але логічно небезпечний. Не сприймайте його як норму:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{5, 6, 7};

    std::cout << v[3] << '\n'; // некоректний індекс: елемента з індексом 3 немає
}

Чому тоді [] узагалі існує, якщо це так небезпечно? Тому що:

  • іноді індекс гарантовано коректний (наприклад, ви перебираєте i від 0 до v.size() у правильних межах),
  • іноді вам важлива швидкість (у серйозних задачах так справді буває),
  • а іноді ви свідомо хочете мати коротший код і самі відповідаєте за перевірки.

Корисна модель:
[] — «я впевнений, що індекс правильний».

at(i): доступ із перевіркою меж і явним сигналом про помилку

Метод v.at(i) робить майже те саме, що й v[i], але з однією принциповою різницею: він перевіряє межі. Якщо індекс неправильний, стандартна бібліотека явно сигналізує про помилку під час виконання. Деталі механізму, тобто як саме це влаштовано, ми поки не розбираємо. Нам важливо інше: помилка не буде «тихою».

Приклад, де at() працює як звичайний доступ:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{100, 200, 300};

    std::cout << v.at(1) << '\n'; // 200
}

А ось приклад, де індекс неправильний і ви отримаєте явну помилку під час виконання:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{100, 200, 300};

    std::cout << v.at(3) << '\n'; // некоректний індекс: програма аварійно завершиться
}

Це не означає, що at() «рятує» від потреби думати. Це означає лише одне: якщо ви помилилися, проблема спливе одразу, і знайти причину буде простіше.

Корисна модель: at() — «я майже впевнений, але хочу, щоб у разі помилки все було очевидно».

Головний безпечний шаблон: спочатку перевірка, потім доступ

Найпоширеніший сценарій: індекс надходить ззовні. Наприклад, користувач вводить номер елемента, який хоче переглянути. У такому разі ні [], ні at() не варто викликати наосліп, доки ви не перевірили межі.

Базова перевірка звучить так:

якщо i < v.size(), то індекс коректний
інакше — некоректний

Зробімо невеликий фрагмент для навчального консольного застосунку. Нехай у нас є список цін у кошику, поки що без жодних класів і функцій, а користувач хоче подивитися ціну за номером:

#include <cstddef>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> prices{120, 80, 250};
    std::size_t i = 0;

    std::cin >> i;

    if (i < prices.size()) {
        std::cout << prices.at(i) << '\n'; // наприклад, введення 1 -> 80
    } else {
        std::cout << "Немає такого індексу\n";
    }
}

Зверніть увагу на стиль: ми перевірили i < prices.size() і лише потім звернулися до prices.at(i). Здавалося б, навіщо at(), якщо межі вже перевірено? На практиці це непогана звичка: перевірка — це логіка програми, а at() — додатковий ремінь безпеки. Якщо ви десь помилилися в перевірці або індекс змінився, ви отримаєте явний сигнал.

Якщо ви впевнені, що перевірка коректна, наприклад у циклі, можна використовувати й []. Але на старті краще триматися консервативного варіанта: так буде менше загадкових падінь.

Мінітаблиця вибору: [] чи at()

Спосіб доступу Перевірка меж Що буде, якщо індекс неправильний Коли використовувати
v[i]
ні некоректна поведінка (може бути що завгодно) коли індекс точно коректний (наприклад, у правильному циклі)
v.at(i)
так явний сигнал про помилку під час виконання коли індекс надійшов ззовні або ви налагоджуєте код

4. empty(), front() і back(): доступ до першого й останнього

empty(): як зручно перевірити, чи вектор порожній

Перевірка «вектор порожній?» трапляється постійно, особливо перед front()/back().

Технічно v.empty() — майже те саме, що v.size() == 0. Але empty() читається як людська мова: «порожній?». Це робить код зрозумілішим, особливо в умовах.

Приклад:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v;

    if (v.empty()) {
        std::cout << "Вектор порожній\n"; // Вектор порожній
    }
}

Практичніший приклад: нехай у нашому застосунку «кошик» може бути порожнім, і ми хочемо коректно це обробити:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> prices; // порожній кошик

    if (!prices.empty()) {
        std::cout << "Перша ціна = " << prices[0] << '\n';
    } else {
        std::cout << "Кошик порожній\n"; // Кошик порожній
    }
}

Тут ключовий момент: prices[0] не можна використовувати, доки ви не переконалися, що вектор не порожній.

front() і back(): перший і останній елемент

Іноді вам не потрібен конкретний індекс. Вам потрібен «найперший» або «найостанній» елемент. Наприклад, у кошику — перший товар або останній доданий товар.

Для цього є:

  • v.front() — перший елемент
  • v.back() — останній елемент

Це дуже зручно, але тут є важливе правило: вектор має бути непорожнім. Якщо вектор порожній, front() і back() використовувати не можна.

Приклад із непорожнім вектором:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> prices{120, 80, 250};

    std::cout << prices.front() << '\n'; // 120
    std::cout << prices.back() << '\n';  // 250
}

Тепер акуратний варіант, який не падає на порожньому векторі:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> prices;

    if (!prices.empty()) {
        std::cout << prices.front() << ' ' << prices.back() << '\n';
    } else {
        std::cout << "Немає цін: список порожній\n"; // Немає цін: список порожній
    }
}

Це дуже поширений шаблон: if (!v.empty()) { … front()/back() … }.

front/back чи індекси: що читається краще

Іноді новачки пишуть v[0] і v[v.size() - 1] скрізь, де потрібні перший і останній елементи. Це працює, якщо вектор непорожній, але читається важче й потребує більшої акуратності.

Порівняйте:

// варіант А
int first = v.front();
int last  = v.back();

і

// варіант Б
int first = v[0];
int last  = v[v.size() - 1];

Варіант А коротший і зрозуміліший. Варіант Б теж нормальний, але в ньому більше «гострих кутів»: потрібно памʼятати про size() - 1, потрібно памʼятати, що цього не можна робити, коли size() == 0, і складніше з першого погляду перевірити, що все безпечно.

Тому практичне правило на цьому етапі таке: якщо вам потрібен саме перший або останній елемент, використовуйте front()/back() разом із empty(). Індекси залишайте для випадків, коли вам потрібен конкретний номер елемента.

5. Практичний приклад: «мінікошик» із переглядом елементів

Зберімо невеликий, але цілісний фрагмент: у нас є вектор цін, і ми хочемо вміти:

  • друкувати першу й останню ціну, якщо вони є,
  • друкувати ціну за індексом, який увів користувач,
  • не падати на порожньому векторі й за неправильного індексу.

Ми поки не робимо меню й не пишемо функції — це буде пізніше в курсі. Зараз нам потрібен просто прямолінійний main, щоб логіку було добре видно:

#include <cstddef>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> prices{120, 80, 250};

    if (!prices.empty()) {
        std::cout << "перша=" << prices.front() << '\n';   // перша=120
        std::cout << "остання=" << prices.back() << '\n';  // остання=250
    }

    std::size_t i = 0;
    std::cin >> i;

    if (i < prices.size()) {
        std::cout << "prices[" << i << "]=" << prices.at(i) << '\n';
        // введення 1 -> prices[1]=80
    } else {
        std::cout << "Індекс поза межами\n";
    }
}

У цьому коді добре видно, як три інструменти — empty(), front()/back() і at() — працюють разом.

6. Типові помилки під час доступу до елементів std::vector

Помилка № 1: звертатися до v[0], коли вектор порожній.
Це класика. Код компілюється, виглядає «логічно», але насправді ви берете елемент, якого немає. Часто так стається, коли ви забули, що reserve() не створює елементи, або коли ви щойно створили std::vector<int> v; і відразу полізли в v[0]. Вирішується це простим правилом: перед зверненням до першого чи останнього елемента або до фіксованого індексу завжди ставте собі запитання: «а точно size() > 0?» І не соромтеся писати if (!v.empty()).

Помилка № 2: плутати «є памʼять» і «є елементи».
Якщо ви зробили v.reserve(100), у вас могла зрости capacity(), але size() залишився таким самим. Це означає, що елементів усе ще немає, і доступ за індексом, як і раніше, некоректний. Новачки інколи думають: «ну я ж зарезервував, отже, вектор ніби на 100 елементів». Ні. Він усе ще ніби на 0 елементів, просто з великим запасом памʼяті. Щоб елементи зʼявилися, потрібен push_back() або resize().

Помилка № 3: вважати, що at() «якось безпечно поверне щось», навіть якщо індекс неправильний.
at() не повертає «порожнє значення» і не «виправляє» індекс. Він потрібен для того, щоб помилка була явною. Тому правильний стиль такий: спочатку перевірка i < v.size(), потім at(i) (або [], якщо ви впевнені). Якщо ви розраховуєте, що at() сам розвʼяже проблему неправильного індексу, програма аварійно завершуватиметься в найнесподіваніший момент.

Помилка № 4: використовувати v.size() - 1, не перевіривши, що size() > 0.
Це особливо підступно, бо код виглядає математично правильним. Але для порожнього вектора «останнього індексу» не існує. Найпростіший спосіб не потрапляти в цю пастку — перевіряти empty() перед обчисленням size() - 1 і перед будь-якими діями, які передбачають існування елементів.

Помилка № 5: неправильна перевірка меж «включно з останнім».
Іноді пишуть if (i <= v.size()), думаючи: «ну size() же останній». Ні: size() — це кількість елементів, а останній індекс — size() - 1, якщо елементи є. Тому коректна перевірка — строго i < v.size(). Якщо ви ловите себе на бажанні написати <=, зупиніться й ще раз перевірте модель індексів: позиція end існує, але елемента там немає.

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