Урок №146. Перевантаження оператора індексації []

  Юрій  | 

  Оновл. 25 Вер 2021  | 

 179

На цьому уроці ми розглянемо перевантаження оператора індексації в мові С++.

Перевантаження оператора індексації []

При роботі з масивами оператор індексації ([]) використовується для вибору певних елементів:

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

Оскільки змінна-член m_array є закритою, то ми не маємо прямого доступу до m_array через об’єкт array. Це означає, що ми не можемо напряму отримати або встановити значення елементів m_array. Що робити?

Можна використати геттери і сеттери:

Хоча це працює, але це не дуже зручно. Розглянемо наступний приклад:

Тут ми присвоюємо елементу 4 значення 5 чи елементу 5 значення 4? Без перегляду визначення методу setItem() цього не зрозуміти.

Можна також просто повернути весь масив (m_array) і використати оператор [] для доступу до його елементів:

Але можна зробити ще простіше, перевантаживши оператор індексації.

Оператор індексації є одним з операторів, перевантаження якого повинне виконуватися через метод класу. Функція перевантаження оператора [] завжди прийматиме один параметр: значення індексу (елемент масиву, до якого потрібен доступ). У нашому випадку з IntArray нам потрібно, щоб користувач просто вказав в квадратних дужках індекс для повернення значення елементу за цим індексом:

Тепер щоразу, коли ми будемо використовувати оператор індексації ([]) з об’єктом класу IntArray, компілятор повертатиме відповідний елемент масиву m_array! Це дозволить нам безпосередньо як отримувати, так і присвоювати значення елементам m_array:

Все просто. При обробці array[4] компілятор спочатку перевіряє, чи є функція перевантаження оператора []. Якщо є, то він передає в функцію перевантаження значення всередині квадратних дужок (в даному випадку 4) в якості аргументу.

Зверніть увагу, хоча ми можемо вказати параметр за замовчуванням для функції перевантаження оператора [], але в даному випадку, якщо ми, використовуючи [], не вкажемо всередині дужок значення індексу, то отримаємо помилку.

Чому оператор індексації [] використовує повернення по посиланню?

Розглянемо детально, як обробляється стейтмент array[4] = 5. Оскільки пріоритет оператора індексації вище пріоритету оператора присвоювання, то спочатку виконується частина array[4]. array[4] призводить до виклику функції перевантаження оператора [], яка поверне array.m_array[4]. Оскільки оператор [] використовує повернення по посиланню, то він повертає фактичний елемент array.m_array[4]. Наш частково оброблений вираз стає array.m_array[4] = 5, що є прямою операцією присвоювання значення елементу масиву.

З уроку №13 ми вже знаємо, що будь-яке значення, яке знаходиться зліва від оператора присвоювання, має бути l-value (змінною з адресою в пам’яті). Оскільки результат виконання оператора [] може використовуватися в лівій частині операції присвоювання (наприклад, array[4] = 5), то значення, що повертається, оператора [] має бути l-value. Посилання ж завжди є l-values, тому що їх можна використовувати тільки зі змінними, які мають адреси в пам’яті. Тому, використовуючи повернення по посиланню, компілятор залишиться задоволений, що повертається l-value, і ніяких проблем не буде.

Розглянемо, що станеться, якщо оператор [] використовуватиме повернення по значенню замість повернення по посиланню. array[4] призведе до виклику функції перевантаження оператора [], який повертатиме значення елементу array.m_array[4] (не індекс, а значення за вказаним індексом). Наприклад, якщо значенням m_array[4] є 7, то виконання оператора [] призведе до повернення значення 7. array[4] = 5 оброблятиметься як 7 = 5, що є безглуздим! Якщо ви спробуєте це зробити, то компілятор видасть наступну помилку:

C:VCProjectsTest.cpp(386) : error C2106: '=' : left operand must be l-value

Використання оператора індексації з константними об’єктами класу

У вищенаведеному прикладі метод operator[]() не є константним, і ми можемо використовувати цей метод для зміни даних неконстантних об’єктів. Однак, що станеться, якщо наш об’єкт класу IntArray буде const? В цьому випадку ми не зможемо викликати неконстантний operator[](), так як він змінює значення об’єкту (детально про класи і const читайте в матеріалах уроку №131).

Доброю новиною є те, що ми можемо визначити окремо неконстантну і константну версії operator[](). Неконстантна версія буде використовуватися з неконстантними об’єктами, а версія const — з об’єктами const:

Рядок carray[4] = 5; потрібно закоментувати і програма скомпілюється (це перевірка на зміну даних константних об’єктів — змінювати дані не можна, можна тільки виводити).

Перевірка помилок

Ще однією перевагою перевантаження оператора індексації є те, що ми можемо виконати перевірку переданих значень індексу. При прямому доступі до елементів масиву (через геттери і сеттери), оператор індексу не перевіряє, чи є індекс коректним. Наприклад, компілятор не скаржитиметься на наступний код:

Однак, якщо ми знаємо довжину нашого масиву, ми можемо виконати перевірку переданого індексу на коректність у функції перевантаження оператора []:

У прикладі, наведеному вище, ми використовували стейтмент assert (який знаходиться в заголовку cassert) для перевірки діапазону index. Якщо вираз всередині assert приймає значення false (тобто користувач ввів некоректний індекс), то програма негайно завершиться з виведенням повідомлення про помилку, що краще, ніж альтернативний варіант — пошкодження пам’яті. Це найпоширеніший спосіб перевірки помилок з використанням функцій перевантаження.

Вказівники на об’єкти і перевантажений оператор []

Якщо ви спробуєте викликати operator[]() для вказівника на об’єкт, то C++ припустить, що ви намагаєтеся індексувати масив. Розглянемо наступний приклад:

Справа в тому, що вказівник вказує на адресу в пам’яті, а не на значення. Тому спочатку вказівник потрібно розіменувати, а потім вже використовувати оператор []:

Це жахливо і тут дуже легко наробити помилок. Не використовуйте вказівники на об’єкти, якщо це не є обов’язковим.

Аргумент, що передається, не обов’язково повинен бути цілим числом

Як згадувалося вище, C++ передає в функцію перевантаження те, що користувач вказав в квадратних дужках в якості аргументу (в більшості випадків, це цілочисельне значення). Однак це не є обов’язковою вимогою і, насправді, ви можете визначити функцію перевантаження так, щоб ваш перевантажений оператор [] приймав значення будь-якого типу, якого ви тільки побажаєте (double, string тощо). Наприклад:

Результат виконання програми:

Hello, world!

Висновки

Перевантаження оператора індексації зазвичай використовується для забезпечення прямого доступу до елементів масиву, який знаходиться всередині класу (в якості змінної-члена). Оскільки рядки часто використовуються в реалізації масивів символів, то оператор [] часто перевантажують в класах з рядками, щоб мати доступ до кожного символу рядка окремо.

Тест

Завдання №1

Контейнер map — це клас, в якому всі елементи зберігаються у вигляді пари ключ-значення. Ключ повинен бути унікальним і використовуватися для доступу до зв’язаної пари. У цьому завданні вам потрібно написати програму, яка дозволить присвоювати оцінки учням, вказуючи тільки ім’я учня. Для цього використовуйте контейнер map: ім’я учня — ключ, оцінка (тип char) — значення.

a) Спочатку напишіть структуру StudentGrade з двома елементами: ім’я студента (std::string) і оцінка (char).

Відповідь №1.a)

b) Додайте клас GradeMap, який містить std::vector типу StudentGrade з ім’ям m_map. Додайте порожній конструктор за замовчуванням.

Відповідь №1.b)

c) Реалізуйте перевантаження оператора [] для цього класу. Функція перевантаження повинна приймати параметр std::string (ім’я учня) і повертати посилання на його оцінку. У функції перевантаження спочатку виконайте пошук зазначеного імені учня в векторі (використовуйте цикл foreach). Якщо учень знайшовся, то повертайте посилання на його оцінку, і все — готово!

В протилежному випадку, використовуйте функцію std::vector::push_back() для додання StudentGrade нового учня. Коли ви це зробите, std::vector додасть собі копію нового StudentGrade (при необхідності змінивши розмір). Нарешті, вам потрібно буде повернути посилання на оцінку студента, якого ви тільки що додали в std::vector — для цього використовуйте std::vector::back().

Наступна програма повинна скомпілюватися без помилок:

Відповідь №1.c)

Завдання №2

Клас GradeMap і програма, яку ми написали, неефективна з кількох причин. Опишіть один спосіб поліпшення класу GradeMap.

Відповідь №2

std::vector не є відразу відсортованим. Це означає, що кожен раз, при виклику operator[](), ми перебиратимемо весь std::vector для пошуку елемента. З кількома елементами це не є проблемою, але, у міру того як їх кількість збільшуватиметься, процес пошуку елемента ставатиме все повільнішим і повільнішим. Ми могли б це оптимізувати, зробивши m_map відсортованим і використовуючи бінарний пошук. Таким чином, кількість елементів, які використовуватимуться при перегляді під час пошуку одного елемента, зменшиться в рази.

Завдання №3

Чому наступна програма не працює належним чином?

Відповідь №3

При доданні Martin, std::vector повинен збільшити свій розмір. А для цього потрібне динамічне виділення нового блоку пам’яті, копіювання елементів масиву в цей новий блок і видалення старого блоку. Коли це станеться, то будь-які посилання на існуючі елементи в std::vector зникнуть! Іншими словами, після того, як виконається push_back("Martin"), gradeJohn залишиться посиланням на видалену пам’ять. Це і призведе до невизначених результатів.

Оцінити статтю:

1 Зірка2 Зірки3 Зірки4 Зірки5 Зірок (4 оцінок, середня: 5,00 з 5)
Loading...

Залишити відповідь

Ваш E-mail не буде опублікований. Обов'язкові поля відмічені *