Перегрузка операторов
operator Ключевое слово объявляет функцию, указывающую, какой оператор-Symbol означает при применении к экземплярам класса. Это дает оператору более одного значения — «перегружает» его. Компилятор различает разные значения оператора, проверяя типы его операндов.
Синтаксис
тип operator operator-символ ( parameter-list )
Remarks
Функцию большинства встроенных операторов можно переопределить глобально или для отдельных классов. Перегруженные операторы реализуются в виде функции.
Имя перегруженного оператора — operator x, где x — это оператор, как показано в следующей таблице. Например, для перегрузки оператора сложения необходимо определить функцию с именем operator +. Аналогично, чтобы перегрузить оператор сложения и присваивания, += Определите функцию с именем operator + =.
Переопределяемые операторы
| Оператор | Имя | Тип |
|---|---|---|
| , | Запятая | Двоичные данные |
| ! | Логическое НЕ | Унарный |
| != | Неравенство | Двоичные данные |
| % | Modulus | Двоичные данные |
| %= | Назначение модуля | Двоичные данные |
| & | Побитовое И | Двоичные данные |
| & | Взятие адреса | Унарный |
| && | Логическое И | Двоичные данные |
| &= | Назначение побитового И | Двоичные данные |
| ( ) | Вызов функции | — |
| ( ) | Оператор приведения | Унарный |
| * | Умножение | Двоичные данные |
| * | Разыменование указателя | Унарный |
| *= | Присваивание умножения | Двоичные данные |
| + | Сложение | Двоичные данные |
| + | Унарный плюс | Унарный |
| ++ | Шаг 1 | Унарный |
| += | Присваивание сложения | Двоичные данные |
| — | Вычитание | Двоичные данные |
| — | Унарное отрицание | Унарный |
| — | Уменьшить 1 | Унарный |
| -= | Присваивание вычитания | Двоичные данные |
| -> | Выбор члена | Двоичные данные |
| — >* | Выбор указателя на член | Двоичные данные |
| / | Отдел | Двоичные данные |
| /= | Присваивание деления | Двоичные данные |
| Больше | Двоичные данные | |
| >= | Больше или равно | Двоичные данные |
| >> | Сдвиг вправо | Двоичные данные |
| >>= | Сдвиг вправо и присваивание | Двоичные данные |
| [ ] | Индекс массива | — |
| ^ | Исключающее ИЛИ | Двоичные данные |
| ^= | Исключающее ИЛИ/присваивание | Двоичные данные |
| | | Побитовое ИЛИ | Двоичные данные |
| |= | Назначение побитового включающего ИЛИ | Двоичные данные |
| || | Логическое ИЛИ | Двоичные данные |
| Дополнение до единицы | Унарный | |
| delete | Удаление | — |
| new | Создать | — |
| операторы преобразования | операторы преобразования | Унарный |
Существует 1 две версии унарных операторов инкремента и декремента: добавочное и инкрементное.
Урок №130. Введение в перегрузку операторов
Обновл. 13 Сен 2021 |
Из урока №102 мы уже знаем, что перегрузка функций обеспечивает механизм создания и выполнения вызовов функций с одним и тем же именем, но с разными параметрами. Это позволяет одной функции работать с несколькими разными типами данных (без необходимости придумывать уникальные имена для каждой из функций).
В языке C++ операторы реализованы в виде функций. Используя перегрузку функции оператора, вы можете определить свои собственные версии операторов, которые будут работать с разными типами данных (включая классы). Использование перегрузки функции для перегрузки оператора называется перегрузкой оператора.
Операторы, как функции
Рассмотрим следующий фрагмент:
Теперь рассмотрим следующий фрагмент:
Теперь рассмотрим, что произойдет, если мы попытаемся добавить два объекта класса:
Вызов перегруженных операторов
При обработке выражения, содержащего оператор, компилятор использует следующие алгоритмы действий:
Если все операнды являются фундаментальных типов данных, то вызывать следует встроенные соответствующие версии операторов (если таковые существуют). Если таковых не существует, то компилятор выдаст ошибку.
Если какой-либо из операндов является пользовательского типа данных (например, объект класса или перечисление), то компилятор будет искать версию оператора, которая работает с таким типом данных. Если компилятор не найдет ничего подходящего, то попытается выполнить конвертацию одного или нескольких операндов пользовательского типа данных в фундаментальные типы данных, чтобы таким образом он мог использовать соответствующий встроенный оператор. Если это не сработает — компилятор выдаст ошибку.
Ограничения в перегрузке операторов
Во-первых, почти любой существующий оператор в языке C++ может быть перегружен. Исключениями являются:
оператор разрешения области видимости ( :: );
Во-вторых, вы можете перегрузить только существующие операторы. Вы не можете создавать новые или переименовывать существующие. Например, вы не можете создать оператор ** для выполнения операции возведения в степень.
В-третьих, по крайней мере один из операндов перегруженного оператора должен быть пользовательского типа данных. Это означает, что вы не можете перегрузить operator+() для выполнения операции сложения значения типа int со значением типа double. Однако вы можете перегрузить operator+() для выполнения операции сложения значения типа int с объектом класса Mystring.
В-четвертых, изначальное количество операндов, поддерживаемых оператором, изменить невозможно. Т.е. с бинарным оператором используются только два операнда, с унарным — только один, с тернарным — только три.
Наконец, все операторы сохраняют свой приоритет и ассоциативность по умолчанию (независимо от того, для чего они используются), и это не может быть изменено.
Вам нужно будет явно заключать в скобки часть с возведением в степень (например, 2 + (5 ^ 2) ) каждый раз, когда вы хотите, чтобы она выполнялась первой, что очень легко забыть и, таким образом, наделать ошибок. Поэтому проводить подобные эксперименты не рекомендуется.
Примечание: В языке C++ для возведения в степень используется функция pow() из заголовочного файла cmath. В примере, приведенном выше, с выполнением выражения 2 + 5 ^ 2 в языке C++, имеется в виду, что вы перегрузите побитовый оператор XOR ( ^ ) для выполнения операции возведения в степень.
Правило: При перегрузке операторов старайтесь максимально приближенно сохранять функционал операторов в соответствии с их первоначальными применениями.
Для чего использовать перегрузку операторов? Вы можете перегрузить оператор + для соединения объектов вашего класса String или для выполнения операции сложения двух объектов вашего класса Fraction. Вы можете перегрузить оператор для вывода вашего класса на экран (или записи в файл). Вы можете перегрузить оператор равенства ( == ) для сравнения двух объектов класса и т.д. Подобные применения делают перегрузку операторов одной из самых полезных особенностей языка C++, так как это упрощает процесс работы с классами и открывает новые возможности.
Перегрузка операторов / FAQ C++
Что там с перегрузкой операторов?
Она позволяет вам предоставить интуитивно понятный интерфейс для пользователей вашего класса, а также позволяет шаблонам одинаково хорошо работать с классами и со встроенными/внутренними типами.
Перегрузка операторов позволяет операторам C/C++ иметь определяемое пользователем значение для определяемых пользователем типов (классов). Перегруженные операторы – это синтаксический сахар для вызовов функций:
Каковы преимущества перегрузки операторов?
Перегружая в классе стандартные операторы, вы можете использовать интуицию пользователей этого класса. Это позволяет пользователям программировать на языке предметной области, а не на языке машины.
Конечная цель – сократить как кривую обучения, так и количество брака.
Можно увидеть примеры перегрузки операторов?
Вот несколько из многих примеров перегрузки операторов:
Но перегрузка операторов делает мой класс уродливым; разве она не должна сделать мой код более понятным?
Перегрузка операторов облегчает жизнь пользователям класса, а не его разработчикам!
Рассмотрим следующий пример.
Некоторым людям не нравится ключевое слово operator или несколько странный синтаксис, связанный с ним в теле самого класса. Но синтаксис перегрузки операторов не должен облегчать жизнь разработчику класса. Предполагается, что он облегчит жизнь пользователям класса:
Помните: в мире, ориентированном на повторное использование, обычно будет много людей, которые используют ваш класс, но только один человек, который создает его (вы сами); поэтому вы должны делать то, что приносит пользу многим, а не немногим.
Какие операторы можно/нельзя перегружать?
Ниже показан пример оператора индекса (он возвращает ссылку). Сначала без перегрузки оператора:
А ниже показана та же логика с перегрузкой оператора:
Программистом может быть перегружено большинство операторов. Исключения составляют:
sizeof нельзя перегружать, потому что от него неявно зависят встроенные операции, такие как инкрементирование указателя в массиве. Рассмотрим пример:
Таким образом, программист не может придать sizeof(X) новое и иное значение без нарушения основных правил языка.
Эту проблему можно решить несколькими способами. До сих пор в области стандартизации не было очевидно, какой путь лучше всего. Для получения дополнительной информации смотрите D&E.
Могу ли я определять свои операторы?
Извините, но нет. Возможность рассматривалась несколько раз, но каждый раз решалось, что вероятные проблемы перевешивают вероятные преимущества.
Могу ли я создать operator** для операций «возведения в степень»?
Если вы сомневаетесь, считайте, что x ** y совпадает с x * (*y) (другими словами, компилятор предполагает, что y является указателем). Кроме того, перегрузка оператора – это просто синтаксический сахар для вызовов функций. Хотя этот синтаксический сахар может быть очень сладким, он не добавляет ничего фундаментального. Я предлагаю вам перегрузить pow(base, exponent) (версия с двойной точностью находится в ).
Кстати, operator^ может работать для возведения в степень, за исключением того, что у него неправильный приоритет и ассоциативность.
В предыдущих ответах FAQ говорится, какие операторы я могу переопределить; но какие операторы я должен переопределить?
Определяющий фактор: не вводите пользователей в заблуждение.
Помните цель перегрузки операторов: снизить стоимость и уровень дефектов в коде, который использует ваш класс. Если вы создаете операторы, которые сбивают с толку ваших пользователей (потому что они крутые; потому что они делают код быстрее; потому что вам нужно доказать себе, что вы можете сделать это; неважно почему), вы пошли против всех причин использования перегрузки операторов.
Есть ли рекомендации / «практические правила» для перегрузки операторов?
Вот несколько рекомендаций / практических правил (но обязательно прочитайте предыдущий ответ FAQ, прежде чем читать этот список):
Предостережение: этот список не является исчерпывающим. Это означает, что есть и другие пункты, которые вы можете считать «пропущенными». Я знаю.
Предостережение: этот список содержит рекомендации, а не жесткие правила. Это означает, что почти все пункты имеют исключения, и большинство из этих исключений явно не указано. Я знаю.
Почему интерфейс моего класса Matrix не должен выглядеть как массив массивов?
В качестве примера того, когда физическая компоновка имеет существенное значение: в проекте необходимо получать доступ к элементам матрицы в столбцах (то есть, алгоритм обращается ко всем элементам в одном столбце, затем к элементам в другом и т.д.), и если физическая структура является строковой, доступ может «опережать кеш». Например, если размер строк почти равен размеру кеш-памяти процессора, машина может получать «промах кеша» (cache miss) почти при каждом доступе к элементу. В этом конкретном проекте мы получили повышение производительности на 20% за счет изменения сопоставления с логической компоновки (строка, столбец) на физическую компоновку (столбец, строка).
Конечно, есть много подобных примеров из численных методов, и разреженные матрицы – это совершенно другое измерение в этом вопросе. Поскольку, как правило, с использованием подхода operator() реализовать разреженную матрицу или поменять местами порядок строк/столбцов проще, подход operator() ничего не теряет и может что-то получить – у него нет недостатков, и есть потенциальные преимущества.
Я всё еще не понимаю. Почему интерфейс моего класса Matrix не должен выглядеть как массив массивов?
По тем же причинам, по которым вы инкапсулируете свои структуры данных, и по той же причине, по которой вы проверяете параметры, чтобы убедиться, что они корректны.
Суть предыдущих двух ответов FAQ заключается в том, что m(i,j) дает вам чистый и простой способ проверить все параметры и скрыть (и, следовательно, при желании изменить) внутреннюю структуру данных. В мире уже слишком много открытых структур данных и слишком много параметров, выходящих за границы, и это стоит слишком больших денег и вызывает слишком много задержек и слишком много проблем.
Теперь все знают, что вы другой. Вы обладаете ясновидением и прекрасно знаете будущее, и вы знаете, что никто никогда не получит никакой пользы от изменения внутренней структуры данных вашей матрицы. Кроме того, вы хороший программист, в отличие от тех бездельников, которые иногда передают неправильные параметры, поэтому вам не нужно беспокоиться о неприятных мелочах, таких как проверка параметров. Но даже если вам не нужно беспокоиться о расходах на поддержку (никому не нужно менять ваш код), могут быть еще один или два программиста, которые еще не совсем идеальны. Для них затраты на поддержку высоки, ошибки реальны, а требования меняются. Верите вы или нет, но время от времени им нужно (лучше присядьте) менять свой код.
Возможно, я утрирую. Но в этом был смысл. Дело в том, что инкапсуляция и проверка параметров – не костыли для слабых. Разумно использовать методы, которые упрощают инкапсуляцию и/или проверку параметров. Синтаксис m(i,j) – один из таких приемов.
Если вы просто хотите проверить параметры, просто убедитесь, что внешний operator[] возвращает объект, а не необработанный массив, тогда operator[] этого объекта сможет проверить свой параметр обычным способом. Помните, что это может замедлить вашу программу. В частности, если эти подобные массивам, внутренние объекты в конечном итоге выделяют собственный блок памяти для своей строки матрицы, накладные расходы на производительность для создания/уничтожения ваших объектов матриц могут резко возрасти. Теоретическая стоимость по-прежнему составляет O (строки × столбцы), но на практике накладные расходы распределителя памяти ( new или malloc ) могут быть намного больше, чем что-либо еще, и другие затраты могут стать незаметными на фоне этих накладных расходов. Например, в двух самых известных компиляторах C++ метод с отдельным выделением памяти для каждой строки был в 10 раз медленнее, чем метод «одно выделение памяти для всей матрицы». 10% – это одно, 10х – другое.
Если вы хотите проверить параметры без вышеуказанных накладных расходов и/или если вы хотите инкапсулировать (и, возможно, изменить) внутреннюю структуру данных матрицы, выполните следующие действия:
Как мне разрабатывать свои классы, снаружи (сначала интерфейсы) или изнутри (сначала данные)?
Хороший интерфейс обеспечивает упрощенное представление, которое выражается в словарном запасе пользователя. В случае объектно-ориентированного программирования интерфейс обычно представляет собой набор общедоступных методов либо одного класса, либо узкой группы классов.
Начиная с точки зрения пользователя, нам может потребоваться, чтобы наш класс LinkedList поддерживал операции, похожие на доступ к элементам массива с использованием арифметики указателей:
Вот методы, которые явно являются встраиваемыми (и, вероятно, находятся в том же заголовочном файле):
Заключение: связанный список имеет два разных типа данных. Значения элементов, хранящихся в связанном списке, находятся в зоне ответственности пользователя связанного списка (и только пользователя; сам связанный список не пытается запретить пользователям изменять третий элемент на 5), а данные инфраструктуры связанного списка (указатели next и т.д.) и их значения находятся в зоне ответственности связанного списка (и только связанного списка; например, связанный список не позволяет пользователям изменять (или даже просматривать!) различные указатели next ).
Таким образом, единственным методами get() / set() были получение и установка значений элементов связанного списка, а не инфраструктуры связанного списка. Поскольку связанный список скрывает инфраструктурные указатели и тому подобное, он может обеспечивать очень высокую надежность в отношении этой инфраструктуры (например, если бы это был двусвязный список, он мог бы гарантировать, что каждый прямой указатель соответствовал обратному указателю из следующего объекта Node ).
Через фиктивный параметр.
Обратите внимание на разные типы возвращаемых значений: префиксная версия возвращает результат по ссылке, постфиксная версия – по значению. Если причина этого была вам не очевидна сразу, то ситуация прояснится после того, как вы увидите определения (и после того, как вы вспомните, что y = x++ и y = ++x присваивают y разные значения).
Другой вариант для постфиксной версии – ничего не возвращать:
Однако вы не должны заставлять постфиксную версию возвращать объект this по ссылке; вы были предупреждены.
Вот как вы используете эти операторы:
Предполагая, что возвращаемые типы не являются «недействительными», вы можете использовать их в более крупных выражениях:
Очевидно, что когда i++ появляется как часть более крупного выражения, это другое дело: он используется потому, что это единственное логически правильное решение, а не потому, что это старая привычка, которую вы приобрели при программировании на C.
BestProg
Данная тема отображает возможности языка C++ по реализации «перегрузки» операторов. Не все современные языки программирования поддерживают перегрузку операторов. Хорошое понимание процесса программирования перегруженных операторов есть показателем профессиональности и высокого мастерства современного программиста.
Перед рассмотрением данной темы рекомендуется ознакомиться со следующей темой:
Содержание
Поиск на других ресурсах:
1. Что такое унарные и бинарные операторы?
Унарные операторы – это операторы, которые для вычислений требуют одного операнда, который может размещаться справа или слева от самого оператора.
Примеры унарных операторов:
Бинарные операторы – это операторы, которые для вычисления требуют двух операндов.
2. В чем состоит суть перегрузки операторов? Что такое операторная функция?
Язык C++ имеет широкие возможности для перегрузки большинства операторов. Перегрузка оператора означает использование оператора для оперирования объектами классов. Перегрузка оператора – способ объявления и реализации оператора таким образом, что он обрабатывает объекты конкретных классов или выполняет некоторые другие действия. При перегрузке оператора в классе вызывается соответствующая операторная функция (operator function), которая выполняет действия, которые касаются данного класса.
Если оператор «перегружен», то его можно использовать в других методах в обычном для него виде. Например, команды поэлементного суммирования двух массивов a1 и a2
лучше вызвать более естественном способом:
В данном примере оператор ‘+’ считается перегруженным.
3. Какими способами можно реализовать операторную функцию для заданного класса? Какие существуют разновидности операторных функций?
Для заданного класса операторную функцию в классе можно реализовать:
4. Общая форма операторной функции, которая реализована в классе. Ключевое слово operator
Общая форма операторной функции, реализованной в классе, имеет следующий вид:
5. Пример перегрузки унарных и бинарных операторов для класса, который содержит одиночные данные. Операторная функция реализована внутри класса
В вышеприведенном коде, в операции суммирования ‘+’ объект P1 вызывает операторную функцию. То есть, фрагмент строки
Реализовать операторную функцию operator+() в классе можно и по другому
В вышеприведенной функции в операторе return создается временный объект путем вызова конструктора с двумя параметрами, реализованного в классе. Если (в данном случае) из тела класса убрать конструктор с двумя параметрами
то вышеприведенный вариант функции operator+() работать не будет, так как для создания объекта типа Point эта функция использует конструктор с двумя параметрами. В этом случае компилятор выдаст сообщение
что значит: нет метода (конструктора) Point::Point() принимающего 2 аргумента.
Использование класса ArrayFloat в другом методе
7. Пример суммирования двух массивов. Операторная функция operator+() размещается внутри класса
Ниже продемонстрировано использование класса ArrayFloat и операторной функции operator+() этого класса.
8. Какие ограничения накладываются на перегруженные операторы?
На использование перегруженных операторов накладываются следующие ограничения:
9. Какие операторы нельзя перегружать?
Нельзя перегружать следующие операторы:
10. Объекты каких типов может возвращать операторная функция? Примеры операторных функций, которые возвращают объекты разных типов
Операторная функция может возвращать объекты любых типов. Наиболее часто операторная функция возвращает объект типа класса, в котором она реализованная или с которыми она работает.
Текст класса следующий:
Далее демонстрируется использование класса Complex и перегруженных операторных функций в некотором другому методе
11. Можно ли изменять значения операндов в операторной функции?
Да, можно. Однако такие действия не являются полезными с точки зрения здравого смысла. Так, например, операция умножения
не изменяет значения своих операндов 6 и 9. Результат равен 54. Если операторная функция operator*() будет изменять значения своих операндов, то это может привести к невидимым ошибкам в программах, поскольку программист по привычке, будет считать, что значения операндов есть неизменными.
12. Можно ли реализовать операторные функции в классе, которые перегружают одинаковый оператор, получают одинаковые параметры но возвращают разные значения?
Нет, нельзя. Операторная функция не может иметь несколько реализаций в классе с одинаковой сигнатурой параметров (когда типы и количество параметров совпадают). В случае нарушения этого правила компилятор выдает ошибку:
Например. Нельзя в классе перегружать оператор ‘+’ так как показано ниже
Это правило касается любых функций класса.



