Что такое перегрузка операторов
Перейти к содержимому

Что такое перегрузка операторов

  • автор:

Перегрузка операторов

Ключевое слово operator объявляет функцию, указывающую, что означает оператор-символ при применении к экземплярам класса. Это дает оператору более одного значения — «перегружает» его. Компилятор различает разные значения оператора, проверяя типы его операндов.

Синтаксис

type operator operator-symbol(parameter-list)

Замечания

Функцию большинства встроенных операторов можно переопределить глобально или для отдельных классов. Перегруженные операторы реализуются в виде функции.

Имя перегруженного оператора — operator x, где x — оператор, как показано в следующей таблице. Например, чтобы перегрузить оператор сложения, необходимо определить функцию с именем operator+. Аналогичным образом, чтобы перегрузить оператор добавления или назначения, +=определите функцию, называемую оператором +=.

Переопределяемые операторы

Оператор Имя. Тип
, Comma Binary
! Логическое НЕ Унарный
!= Неравенство Binary
% Модуль Binary
%= Назначение модуля Binary
& Побитовое И Binary
& Взятие адреса Унарный
&& Логическое И Binary
&= Назначение побитового И Binary
( ) Вызов функции
( ) Оператор приведения Унарный
* Умножение Binary
* Разыменование указателя Унарный
*= Присваивание умножения Binary
+ Дополнение Binary
+ Унарный плюс Унарный
++ Добавочный 1 Унарный
+= Присваивание сложения Binary
Вычитание Binary
Унарное отрицание Унарный
Декремент 1 Унарный
-= Присваивание вычитания Binary
-> Выбор члена Binary
->* Выбор указателя на член Binary
/ Подразделение Binary
/= Присваивание деления Binary
Меньше Binary
< Сдвиг влево Binary
Сдвиг влево и присваивание Binary
Меньше или равно Binary
= Передача прав и обязанностей Binary
== Equality Binary
> Больше Binary
>= Больше или равно Binary
>> Сдвиг вправо Binary
>>= Сдвиг вправо и присваивание Binary
[ ] Индекс массива
^ Исключающее ИЛИ Binary
^= Исключающее ИЛИ/присваивание Binary
| Побитовое ИЛИ Binary
|= Назначение побитового включающего ИЛИ Binary
|| Логическое ИЛИ Binary
~ Дополнение до единицы Унарный
delete DELETE
new Новый
операторы преобразования операторы преобразования Унарный

1 Две версии унарных операторов добавочного и декремента существуют: preincrement и postincrement.

Дополнительные сведения см . в общих правилах перегрузки операторов. Ограничения для разных категорий перегруженных операторов описываются в следующих разделах.

  • Унарные операторы
  • Бинарные операторы
  • Передача прав и обязанностей
  • Вызов функции
  • Индексация
  • Доступ к члену класса
  • Добавочный и декремент.
  • Пользовательские преобразования типов

Операторы, перечисленные в следующей таблице, не могут быть перегружены. Таблица содержит символы # препроцессора и ##.

Непереопределяемые операторы

Оператор Имя.
. Выбор члена
.* Выбор указателя на член
:: Разрешение области
? : Условный
# Препроцессор: преобразование в строку
## Препроцессор: конкатенация

Хотя перегруженные операторы обычно называются компилятором неявным образом при их появлении в коде, их можно вызывать и явным образом — точно так же, как и любую функцию-член или функцию, не являющуюся членом.

Point pt; pt.operator+( 3 ); // Call addition operator to add 3 to pt. 

Пример

В следующем примере оператор перегружает + два сложных числа и возвращает результат.

// operator_overloading.cpp // compile with: /EHsc #include using namespace std; struct Complex < Complex( double r, double i ) : re(r), im(i) <>Complex operator+( Complex &other ); void Display( ) < cout private: double re, im; >; // Operator overloaded using a member function Complex Complex::operator+( Complex &other ) < return Complex( re + other.re, im + other.im ); >int main()
6.8, 11.2 

В этом разделе

  • Общие правила перегрузки операторов
  • Перегрузка унарных операторов
  • Бинарные операторы
  • Передача прав и обязанностей
  • Вызов функции
  • Индексация
  • Доступ к членам

Что такое перегрузка операторов

Перегрузка операторов (operator overloading) позволяет определить для объектов классов втроенные операторы, такие как +, -, * и т.д. Для определения оператора для объектов своего класса, необходимо определить функцию, название которой содержит слово operator и символ перегружаемого оператора. Функция оператора может быть определена как член класса, либо вне класса.

Перегрузить можно только те операторы, которые уже определены в C++. Создать новые операторы нельзя. Также нельзя изменить количество операндов, их ассоциативность, приоритет.

Если функция оператора определена как отдельная функция и не является членом класса, то количество параметров такой функции совпадает с количеством операндов оператора. Например, у функции, которая представляет унарный оператор, будет один параметр, а у функции, которая представляет бинарный оператор, — два параметра. Если оператор принимает два операнда, то первый операнд передается первому параметру функции, а второй операнд — второму параметру. При этом как минимум один из параметров должен представлять тип класса.

Формальное определение операторов в виде функций-членов класса:

// бинарный оператор ReturnType operator Op(Type right_operand); // унарный оператор ClassType& operator Op();

Формальное определение операторов в виде функций, которые не являются членами класса:

// бинарный оператор ReturnType operator Op(const ClassType& left_operand, Type right_operand); // альтернативное определение, где класс, для которого создается оператор, представляет правый операнд ReturnType operator Op(Type left_operand, const ClassType& right_operand); // унарный оператор ClassType& operator Op(ClassType& obj);

Здесь ClassType представляет тип, для которого определяется оператор. Type — тип другого операнда, который может совпадать, а может и не совпадать с первым. ReturnType — тип возвращаемого результата, который также может совпадать с одним из типов операндов, а может и отличаться. Op — сама операция.

Рассмотрим пример с классом Counter, который хранит некоторое число:

#include class Counter < public: Counter(int val) < value =val; >void print() < std::cout Counter operator + (const Counter& counter) const < return Counter; > private: int value; >; int main() < Counter c1; Counter c2; Counter c3 = c1 + c2; c3.print(); // Value: 30 >

Здесь в классе Counter определен оператор сложения, цель которого сложить два объекта Counter:

Counter operator + (const Counter& counter) const < return Counter; >

Текущий объект будет представлять левый операнд операции. Объект, который передается в функцию через параметр counter, будет представлять правый операнд операции. Здесь параметр функции определен как константная ссылка, но это необязательно. Также функция оператора определена как константная, но это тоже не обязательно.

Результатом оператора сложения является новый объект Counter, в котором значение value равно сумме значений value обоих операндов.

После опеределения оператора можно складывать два объекта Counter:

Counter c1; Counter c2; Counter c3 ; c3.print(); // Value: 30

Подобным образом можно определить функцию оператора вне класса:

#include class Counter < public: Counter(int val) < value =val; >void print() < std::cout int value; // к приватным переменным внешняя функция оператора не может обращаться >; // определяем оператор сложения вне класса Counter operator + (const Counter& c1, const Counter& c2) < return Counter; > int main() < Counter c1; Counter c2; Counter c3 ; c3.print(); // Value: 30 >

Если бинарный оператор определяется в виде внешней функции, как здесь, то он принимает два параметра. Первый параметр будет представлять левый операнд операции, а второй параметр — правый операнд.

Но по сравнению с предыдущим кодом здесь сделано еще пару изменений. Во-первых, внешняя функция естественно не может обращаться к приватным полям класса, поэтому для доступа к ним придется создавать отдельные функции, которые бы возвращали значения полей. Я для простоты просто сделал переменную value публичной. Другим решением в данном случае могло быть определение дружественной функции оператора. Второй момент — внешние функции оператора не могут быть константными. Поэтому гораздо определение операторов внутри класса имеет некоторые преимущества.

Стоит отметить, что необязательно возвращать объект класса. Это может быть и любой объект в зависимости от ситуации. И также мы можем определять дополнительные перегруженные функции операторов:

#include class Counter < public: Counter(int val) < value =val; >void print() < std::cout Counter operator + (const Counter& counter) const < return Counter; > int operator + (int number) const < return value + number; >private: int value; >; int main() < Counter counter; int number = counter + 30; std::cout 

Здесь определена вторая версия оператора сложения, которая складывает объект Counter с числом и возвращает также число. Поэтому левый операнд операции должен представлять тип Counter, а правый операнд - тип int.

Какие операторы где переопределять? Операторы присвоения, индексирования ([]), вызова (()), доступа к члену класса по указателю (->) следует определять в виде функций-членов класса. Операторы, которые изменяют состояние объекта или непосредственно связаны с объектом (инкремент, декремент), обычно также определяются в виде функций-членов класса. Операторы выделения и удаления памяти ( new new[] delete delete[] ) определяются только в виде функций, которые не являются членами класса. Все остальные операторы можно определять как отдельные функции, а не члены класса.

Операторы сравнения

Результатом операторов сравнения ( == , != , < , >), как правило, является значение типа bool . Например, перегрузим данные операторы для типа Counter:

#include class Counter < public: Counter(int val) < value =val; >void print() < std::cout bool operator == (const Counter& counter) const < return value == counter.value; >bool operator != (const Counter& counter) const < return value != counter.value; >bool operator > (const Counter& counter) const < return value >counter.value; > bool operator < (const Counter& counter) const < return value < counter.value; >private: int value; >; int main() < Counter c1(20); Counter c2(10); bool b1 = c1 == c2; // false bool b2 = c1 >c2; // true std::cout c2 b">default:

#include class Counter < public: Counter(int val) < value =val; >void print() < std::cout bool operator == (const Counter& counter) const = default; bool operator != (const Counter& counter) const = default; private: int value; >; int main() < Counter c1(20); Counter c2(10); bool b1 = c1 == c2; // false bool b2 = c1 != c2; // true std::cout bool operator == (const Counter& counter) const = default;

По умолчанию будут сравниваться все поля класса, для которых определен оператор ==. Если значения всех полей будут равны, то оператор возвратить true

Операторы присвоения

Оператор присвоения обычно возвращает ссылку на свой левый операнд:

#include class Counter < public: Counter(int val) < value =val; >void print() < std::cout // оператор присвоения Counter& operator += (const Counter& counter) < value += counter.value; return *this; // возвращаем ссылку на текущий объект >private: int value; >; int main() < Counter c1; Counter c2; c1 += c2; c1.print(); // Value: 70 >

Унарные операции

Унарные операции обычно возвращают новый объект, созданный на основе имеющегося. Например, возьмем операцию унарного минуса:

#include class Counter < public: Counter(int val) < value =val; >void print() < std::cout // оператор унарного минуса Counter operator - () const < return Counter; > private: int value; >; int main() < Counter c1; Counter c2 = -c1; // применяем оператор унарного минуса c2.print(); // Value: -20 >

Здесь операция унарного минуса возвращает новый объект Counter, значение value в котором фактически равно значению value текущего объекта, умноженного на -1.

Операции инкремента и декремента

Особую сложность может представлять переопределение операций инкремента и декремента, поскольку нам надо определить и префиксную, и постфиксную форму для этих операторов. Определим подобные операторы для типа Counter:

#include class Counter < public: Counter(int val) < value =val; >void print() < std::cout // префиксные операторы Counter& operator++ () < value += 1; return *this; >Counter& operator-- () < value -= 1; return *this; >// постфиксные операторы Counter operator++ (int) < Counter copy ; ++(*this); return copy; > Counter operator-- (int) < Counter copy ; --(*this); return copy; > private: int value; >; int main() < Counter c1; Counter c2 = c1++; c2.print(); // Value: 20 c1.print(); // Value: 21 --c1; c1.print(); // Value: 20 >

Префиксные операторы должны возвращать ссылку на текущий объект, который можно получить с помощью указателя this:

Counter& operator++ ()

В самой функции можно определить некоторую логику по инкременту значения. В данном случае значение value увеличивается на 1.

Постфиксные операторы должны возвращать значение объекта до инкремента, то есть предыдущее состояние объекта. Поэтому постфиксная форма возвращает копию объекта до инкремента:

Counter operator++ (int) < Counter copy ; ++(*this); return copy; >

Чтобы постфиксная форма отличалась от префиксной постфиксные версии получают дополнительный параметр типа int, который не используется. Хотя в принципе мы можем его использовать.

Переопределение оператора

#include class Counter < public: Counter(int val) < value =val; >int getValue()const private: int value; >; std::ostream& operator <<(std::ostream& stream, const Counter& counter) < stream << "Value: "; stream << counter.getValue(); return stream; >int main() < Counter counter1; Counter counter2; std::cout 

Стандартный выходной поток cout имеет тип std::ostream . Поэтому первый параметр (левый операнд) представляет объект ostream , а второй (правый операнд) - выводимый объект Counter. Поскольку мы не можем изменить стандартное определение std::ostream, поэтому определяем функцию оператора, которая не является членом класса.

std::ostream& operator

В данном случае для выводим значение переменной value. Для получения значения value извне класса Counter я добавил функцию getValue() .

Возвращаемое значение всегда должно быть ссылкой на тот же объект потока, на который ссылается левый операнд оператора.

После определения функции оператора можно выводить на консоль объекты Counter:

Counter counter1; std::cout 

Выражение одних операторов через другие

Иногда более оптимально выражать одни операторы через другие, нежели создавать отдельно операторы с повторяющейся логикой. Например:

#include class Counter < public: Counter(int n) < value = n; >void print() const < std::cout Counter& operator+=(const Counter& counter) < value += counter.value; return *this; >; Counter& operator+(const Counter& counter) < Counter copy< value >; // копируем данные текущего объекта return copy += counter; >; private: int value; >; int main() < Counter counter1; Counter counter2; counter1 += counter2; counter1.print(); // value: 30 Counter counter3 ; counter3.print(); // value: 40 >

Здесь вначале реализован оператор сложения с присвоением +=:

Counter& operator+=(const Counter& counter) < value += counter.value; return *this; >;

В функции оператора сложения мы создаем копию текущего объекта и к этой копии и аргументу применяем оператор +=:

Counter& operator+(const Counter& counter) < Counter copy< value >; // копируем данные текущего объекта return copy += counter; >;

В данном случае суть сложения: к полю value прибавляем значение value другого объекта. Однако логика оператора может быть более сложной, и чтобы не повторяться, мы можем таким образом выражать одни операторы через другие.

Что такое перегрузка операторов

В статье про С++ мы упоминали перегрузку операторов. Это мощный и гибкий инструмент, который может оказаться опасным и непредсказуемым в неумелых руках. Настало время разобраться.

�� Опытным программистам: мы намеренно упростим детали для понимания сути. Ну сорян.

На примере сложения

Во всех языках есть оператор «плюс» — обычно он умеет складывать числа и соединять строки:

‘два’ + ‘два’ = ‘двадва’

‘четы’ + ‘ре’ = ‘четыре’

Допустим, мы пишем софт для интернет-магазина, и у нас есть там класс объектов «заказ». Напомним, что класс — это как бы чертёж, по которому создаются объекты. А объект — это такая коробка с данными и функциями, которыми мы можем управлять как единым целым. Подробнее об этом — в статьях про объекты и классы.

В объекте типа «заказ» лежит куча всего:

  • массив с содержимым корзины,
  • дата и время, когда сформирован заказ,
  • метод «очистить корзину»,
  • место для промокода,
  • метод «применить промокод»,
  • метод «проверить наличие товаров по складу»
  • идентификатор пользователя,
  • что-нибудь ещё интересное.

Допустим, наша система устроена так, что у любого заказа может быть два идентификатора пользователя: постоянный или временный.

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

В какой-то момент пользователь с временным идентификатором логинится в систему, и нам хочется сделать следующую операцию:

Обе части выражения — это объекты класса «Заказ». А наш язык программирования не знает, что значит «сложить два объекта класса „Заказ“». Он не знает:

  • Что с чем складывать? Число товаров? Суммы? Номера товаров? Номера телефонов? Ведь язык не понимает, что за объект перед ним. Для него это просто коробка с данными, он может с ними делать что хочешь.
  • Что возвращать? Число? Строку? Объект? Список заказов?
  • Может быть, нужно сравнить два заказа и по каким-то критериям определить самый актуальный?
  • Или нужно объединить две корзины в одну?
  • А что тогда делать с повторяющимися товарами? Заменить? Добавить в количество? Проигнорировать?

Вроде бы простая операция — а столько вопросов. Вот этому всему мы можем обучить оператор «+», и это будет перегрузка оператора.

�� Короче

Перегрузка оператора — это когда мы обучаем язык программирования, как оператору типа плюс, минус, умножить и т. д. вести себя с определённым типом вводных — например, с объектами, матрицами или картинками.

В случае с нашим примером мы можем сказать, что если складываются два заказа, делай следующее:

  1. Найди, какой из этих заказов постоянный.
  2. Переложи из временного в постоянный все уникальные товары.
  3. Если есть неуникальные товары (например, и в том, и в другом заказе была одна и та же позиция), склей их и поставь максимальное количество. Например, если в постоянном заказе стояло 3 штуки одного артикула, а во временном этого же артикула 9 штук, то поставь в постоянный 9 штук.
  4. Временному заказу поставь статус «Склеено».
  5. Залогируй время склейки заказов.
  6. Верни постоянный заказ с обновлёнными данными.

Довольно много действий для одного плюса, не находите?

Что хорошего в перегрузке

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

В результате программист экономит много кода, не пишет специальный отдельный обработчик такого сложения и не держит в голове параметры его вызова.

Чем опасна перегрузка операторов

Когда вы используете в коде перегруженный оператор, он выглядит как самый обычный оператор. Ну складывает и складывает. Ну умножает и умножает, чего такого-то?

Это, с одной стороны, элегантно. А с другой, создаёт проблемы в отладке.

  1. Представьте, что после вас какой-то программист переделал структуру класса «Заказ», и теперь там по-другому работает массив с товарными позициями. Раньше у товаров были числовые идентификаторы типа integer (целые числа), а новый программист переделал их на строки.
  2. Ваш язык в неявном виде поддерживает сравнение чисел со строками и наоборот. Он производит какие-то свои преобразования и позволяет сравнить число со строкой. В 99,9% случаев это не сломает вашу программу, и даже перегруженный оператор будет работать.
  3. Но в 0,1% случаев сравнение случится некорректно, и никто не будет понимать, в чём дело. Где-то под капотом перегруженный оператор некорректно склеивает списки покупок, у пользователя вываливаются какие-то «левые» товары, которых он не заказывал. Он не глядя их оплачивает и потом катает жалобу на ваш магазин.

А ещё, в особо экзотических случаях и больших проектах, программист шутки ради может перегрузить оператор сложения так, что он будет не складывать, а вычитать. И заметить, в чём тут ошибка, в таких случаях бывает очень сложно.

И что?

Перегрузка операторов — это полезно, но сложно.

Если программист не понимает полностью механизма работы перегрузок, лучше не перегружать.

Если понимает — он молодец и может учить стандартные инструменты нестандартному поведению.

Не перегружайтесь, берегите себя.

Получите ИТ-профессию

В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.

Перегрузка операторов — предопределенные унарные, арифметические операторы, операторы равенства и сравнения

Определяемый пользователем тип может перегружать предопределенный оператор C#. То есть тип может указать пользовательскую реализацию операции, если один или оба операнда принадлежат этому типу. В разделе Перегружаемые операторы показано, какие операторы C# можно перегружать.

Для объявления оператора используйте ключевое слово operator . Объявление оператора должно соответствовать следующим правилам:

  • Оно должно включать public и модификатор static .
  • У унарного оператора один входной параметр. У бинарного оператора два входных параметра. В каждом случае хотя бы один параметр должен иметь тип T или T? , где T — тип, который содержит объявление оператора.

В следующем примере определяется упрощенная структура, представляющая рациональное число. Структура перегружает некоторые арифметические операторы:

public readonly struct Fraction < private readonly int num; private readonly int den; public Fraction(int numerator, int denominator) < if (denominator == 0) < throw new ArgumentException("Denominator cannot be zero.", nameof(denominator)); >num = numerator; den = denominator; > public static Fraction operator +(Fraction a) => a; public static Fraction operator -(Fraction a) => new Fraction(-a.num, a.den); public static Fraction operator +(Fraction a, Fraction b) => new Fraction(a.num * b.den + b.num * a.den, a.den * b.den); public static Fraction operator -(Fraction a, Fraction b) => a + (-b); public static Fraction operator *(Fraction a, Fraction b) => new Fraction(a.num * b.num, a.den * b.den); public static Fraction operator /(Fraction a, Fraction b) < if (b.num == 0) < throw new DivideByZeroException(); >return new Fraction(a.num * b.den, a.den * b.num); > public override string ToString() => $" / "; > public static class OperatorOverloading < public static void Main() < var a = new Fraction(5, 4); var b = new Fraction(1, 2); Console.WriteLine(-a); // output: -5 / 4 Console.WriteLine(a + b); // output: 14 / 8 Console.WriteLine(a - b); // output: 6 / 8 Console.WriteLine(a * b); // output: 5 / 8 Console.WriteLine(a / b); // output: 10 / 4 >> 

Вы можете расширить предыдущий пример, определив неявное преобразование из int в Fraction . Затем перегруженные операторы будут поддерживать аргументы этих двух типов. То есть можно будет добавить целое число к дроби и получить дробь.

Можно также использовать ключевое слово operator для определения пользовательского преобразования типа. Дополнительные сведения см. в разделе Операторы пользовательского преобразования.

Перегружаемые операторы

В следующей таблице показаны операторы, которые могут быть перегружены:

Операторы Примечания
+x , -x , !x , ~x , ++ , -- , true , false Операторы true и false должны быть перегружены вместе.
x + y , x - y , x * y , x / y , x % y ,
x & y , x | y , x ^ y ,
x > y , x >>> y
x == y , x != y , x < y , x > y , x = y Должен быть перегружен парами следующим образом: == и != , < и >, = .

Неперегружаемые операторы

В следующей таблице показаны операторы, которые нельзя перегрузить.

Операторы Альтернативные варианты
x && y , x || y Перегрузка операторов true и и false операторов & или | . Дополнительные сведения см. в разделе Пользовательские условные логические операторы.
a[i] , a?[i] Определите индексатор.
(T)x Определите преобразования пользовательских типов, которые могут выполняться выражением приведения. Дополнительные сведения см. в разделе Операторы пользовательского преобразования.
+= , -= , *= , /= , %= , &= , |= , ^= , >= , >>>= Перегрузка соответствующего двоичного оператора. Например, при перегрузке двоичного + оператора += неявно перегружается.
^x , x = y , x.y , x?.y , c ? t : f , x ?? y , ??= y ,
x..y , x->y , => , f(x) , as , await , checked , unchecked , default , delegate , is , nameof , new ,
sizeof , stackalloc , switch , typeof , with
Нет.

Спецификация языка C#

Дополнительные сведения см. в следующих разделах статьи Спецификация языка C#:

См. также

  • справочник по C#
  • Операторы и выражения C#
  • Операторы пользовательского преобразования
  • Рекомендации по разработке. Перегрузки операторов
  • Рекомендации по разработке. Операторы равенства
  • Почему перегруженные операторы всегда являются статическими в C#?

Совместная работа с нами на GitHub

Источник этого содержимого можно найти на GitHub, где также можно создавать и просматривать проблемы и запросы на вытягивание. Дополнительные сведения см. в нашем руководстве для участников.

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

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