Урок 138. Перегрузка оператора индексации

  Юрий Ворон  | 

    | 

  Обновлено 24 Апр 2018  | 

 2203

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

Рассмотрим следующий класс 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, что является прямой операцией присваивания значения элементу массива.

В уроке 10 мы узнали, что любое значение, которое находится слева от оператора присваивания должно быть 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 читайте в уроке 123).

Хорошей новостью является то, что мы можем определить отдельно неконстантную и константную версии operator[]. Неконстантная версия будет использоваться с неконстантными объектами, а версия const с объектами const.

Строчку carray[4] = 5 нужно закомментировать и программа скомпилируется (эта проверка на изменение данных константных объектов – изменять данные нельзя, можно только выводить).

Проверка ошибок

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

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

В примере выше мы использовали стейтмент assert (который находится в заголовочном файле cassert) для проверки диапазона index. Если выражение внутри assert принимает значение false (т.е. пользователь ввел некорректный индекс), то программа немедленно завершится с выводом сообщения об ошибке, что лучше, нежели альтернативный вариант — повреждение памяти. Это самый распространенный способ проверки ошибок с использованием функций перегрузки.

Указатели на объекты и перегруженный оператор []

Если вы попытаетесь вызвать operator[] для указателя на объект, то C++ предположит, что вы пытаетесь индексировать массив.

Рассмотрим следующий пример:

Дело в том, что указатель указывает на адрес памяти, а не на значение. Поэтому сначала указатель нужно разыменовать, а затем уже использовать оператор []:

Это ужасно и здесь очень легко наделать ошибок. Не используйте указатели на объекты, если это не необходимо.

Передаваемый аргумент не обязательно должен быть целым числом

Как упоминалось выше, C++ передает в функцию перегрузки то, что пользователь указал в квадратных скобках в качестве аргумента, в большинстве случаев — целочисленное значение. Однако это не является обязательным требованием и на самом деле вы можете определить функцию перегрузки так, чтобы ваш перегруженный оператор [] принимал значения любого типа, которого вы только пожелаете (double, std::string и т.д.). Например:

Результат:

Hello, world!

Итого

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

Тест

Задание №1

Контейнер map — это класс, в котором все элементы хранятся в виде пары: ключ-значение. Ключ должен быть уникальным и использоваться для доступа к связанной паре. В этом задании вам нужно будет написать программу, которая позволит присваивать оценки ученикам, указывая только имя ученика. Для этого используйте контейнер map: имя ученика — ключ, оценка (типа char) — значение.

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

Ответ a)

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

Ответ b)

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

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

Следующая программа должна компилироваться без ошибок:

Ответ c)

Дополнительно

Задание #1: Класс GradeMap и программа, которую мы написали, неэффективна по нескольким причинам. Опишите один способ улучшения класса GradeMap.

Ответ 1

std::vector не является изначально отсортированным. Это означает, что каждый раз, при вызове operator[], мы будем перебирать весь std::vector для поиска элемента. С несколькими элементами это не является проблемой, но по мере того, как их количество будет увеличиваться, процесс поиска элемента будет становиться всё медленнее и медленнее. Мы могли бы это оптимизировать, сделав m_map отсортированным и используя бинарный поиск. Таким образом, количество элементов, которые будут использоваться при просмотре в поиске одного элемента, уменьшится в разы.

Задание #2: Почему следующая программа не работает должным образом?

Ответ 2

При добавлении Martin-а, std::vector должен увеличить свой размер. А для этого потребуется динамическое выделение нового блока памяти, копирование элементов массива в этот новый блок и удаление старого блока. Когда это произойдет, любые ссылки на существующие элементы в std::vector пропадут! Другими словами, после того, как выполнится push_back(«Martin»), gradeJohn останется ссылкой на удаленную память. Это и приведет к неопределенным результатам.

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (9 оценок, среднее: 5,00 из 5)
Загрузка...

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

ВОЛШЕБНАЯ ТАБЛЕТКА ПО С++