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

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

  • автор:

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

Перегрузка операторов (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 другого объекта. Однако логика оператора может быть более сложной, и чтобы не повторяться, мы можем таким образом выражать одни операторы через другие.

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

Определяемый пользователем тип может перегружать предопределенный оператор 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 false Операторы true должны быть перегружены вместе.
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#?

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

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

Обратная связь

Ожидается в ближайшее время: в течение 2024 года мы постепенно откажемся от GitHub Issues как механизма обратной связи для контента и заменим его новой системой обратной связи. Дополнительные сведения см. в разделе https://aka.ms/ContentUserFeedback.

Отправить и просмотреть отзыв по

Дополнительные ресурсы

Значок отказа согласно Закону Калифорнии о защите конфиденциальности потребителей (CCPA)

  • Светлая
  • Темная
  • Высокая контрастность
  • Предыдущие версии
  • Блог
  • Участие в доработке
  • Конфиденциальность
  • Условия использования
  • Товарные знаки
  • © Microsoft 2024

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

Python 3 логотип

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

Например, у нас есть два класса:

 В данном примере класс B наследует класс A, но переопределяет метод go, поэтому он имеет мало общего с аналогичным методом класса A.

Однако в python имеются методы, которые, как правило, не вызываются напрямую, а вызываются встроенными функциями или операторами.

Например, метод __init__ перегружает конструктор класса. Конструктор - создание экземпляра класса.

 

__new__(cls[, . ]) — управляет созданием экземпляра. В качестве обязательного аргумента принимает класс (не путать с экземпляром). Должен возвращать экземпляр класса для его последующей его передачи методу __init__.

__init__(self[, . ]) - как уже было сказано выше, конструктор.

__del__(self) - вызывается при удалении объекта сборщиком мусора.

__repr__(self) - вызывается встроенной функцией repr; возвращает "сырые" данные, использующиеся для внутреннего представления в python.

__str__(self) - вызывается функциями str, print и format. Возвращает строковое представление объекта.

__bytes__(self) - вызывается функцией bytes при преобразовании к байтам.

__format__(self, format_spec) - используется функцией format (а также методом format у строк).

__le__(self, other) - x ≤ y вызывает x.__le__(y).

__eq__(self, other) - x == y вызывает x.__eq__(y).

__ne__(self, other) - x != y вызывает x.__ne__(y)

__gt__(self, other) - x > y вызывает x.__gt__(y).

__ge__(self, other) - x ≥ y вызывает x.__ge__(y).

__hash__(self) - получение хэш-суммы объекта, например, для добавления в словарь.

__bool__(self) - вызывается при проверке истинности. Если этот метод не определён, вызывается метод __len__ (объекты, имеющие ненулевую длину, считаются истинными).

__getattr__(self, name) - вызывается, когда атрибут экземпляра класса не найден в обычных местах (например, у экземпляра нет метода с таким названием).

__setattr__(self, name, value) - назначение атрибута.

__delattr__(self, name) - удаление атрибута (del obj.name).

__call__(self[, args. ]) - вызов экземпляра класса как функции.

__len__(self) - длина объекта.

__getitem__(self, key) - доступ по индексу (или ключу).

__setitem__(self, key, value) - назначение элемента по индексу.

__delitem__(self, key) - удаление элемента по индексу.

__iter__(self) - возвращает итератор для контейнера.

__reversed__(self) - итератор из элементов, следующих в обратном порядке.

__contains__(self, item) - проверка на принадлежность элемента контейнеру (item in self).

Перегрузка арифметических операторов

__add__(self, other) - сложение. x + y вызывает x.__add__(y).

__sub__(self, other) - вычитание (x - y).

__mul__(self, other) - умножение (x * y).

__truediv__(self, other) - деление (x / y).

__floordiv__(self, other) - целочисленное деление (x // y).

__mod__(self, other) - остаток от деления (x % y).

__divmod__(self, other) - частное и остаток (divmod(x, y)).

__pow__(self, other[, modulo]) - возведение в степень (x ** y, pow(x, y[, modulo])).

__lshift__(self, other) - битовый сдвиг влево (x

__rshift__(self, other) - битовый сдвиг вправо (x >> y).

__and__(self, other) - битовое И (x & y).

__xor__(self, other) - битовое ИСКЛЮЧАЮЩЕЕ ИЛИ (x ^ y).

__or__(self, other) - битовое ИЛИ (x | y).

__radd__(self, other),

__rsub__(self, other),

__rmul__(self, other),

__rtruediv__(self, other),

__rfloordiv__(self, other),

__rmod__(self, other),

__rdivmod__(self, other),

__rpow__(self, other),

__rlshift__(self, other),

__rrshift__(self, other),

__rand__(self, other),

__rxor__(self, other),

__ror__(self, other) - делают то же самое, что и арифметические операторы, перечисленные выше, но для аргументов, находящихся справа, и только в случае, если для левого операнда не определён соответствующий метод.

Например, операция x + y будет сначала пытаться вызвать x.__add__(y), и только в том случае, если это не получилось, будет пытаться вызвать y.__radd__(x). Аналогично для остальных методов.

__iadd__(self, other) - +=.

__isub__(self, other) - -=.

__imul__(self, other) - *=.

__itruediv__(self, other) - /=.

__ifloordiv__(self, other) - //=.

__imod__(self, other) - %=.

__ipow__(self, other[, modulo]) - **=.

__ilshift__(self, other) -

__irshift__(self, other) - >>=.

__iand__(self, other) - &=.

__ixor__(self, other) - ^=.

__ior__(self, other) - |=.

__neg__(self) - унарный -.

__pos__(self) - унарный +.

__abs__(self) - модуль (abs()).

__invert__(self) - инверсия (~).

__complex__(self) - приведение к complex.

__int__(self) - приведение к int.

__float__(self) - приведение к float.

__round__(self[, n]) - округление.

__enter__(self), __exit__(self, exc_type, exc_value, traceback) - реализация менеджеров контекста.

Рассмотрим некоторые из этих методов на примере двухмерного вектора, для которого переопределим некоторые методы:

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

Для вставки кода на Python в комментарий заключайте его в теги

  • Модуль csv - чтение и запись CSV файлов
  • Создаём сайт на Django, используя хорошие практики. Часть 1: создаём проект
  • Онлайн-обучение Python: сравнение популярных программ
  • Книги о Python
  • GUI (графический интерфейс пользователя)
  • Курсы Python
  • Модули
  • Новости мира Python
  • NumPy
  • Обработка данных
  • Основы программирования
  • Примеры программ
  • Типы данных в Python
  • Видео
  • Python для Web
  • Работа для Python-программистов
  • Сделай свой вклад в развитие сайта!
  • Самоучитель Python
  • Карта сайта
  • Отзывы на книги по Python
  • Реклама на сайте

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

Желание написать данную статью появилось после прочтения поста Перегрузка C++ операторов, потому что в нём не были раскрыты многие важные темы.

Самое главное, что необходимо помнить — перегрузка операторов, это всего лишь более удобный способ вызова функций, поэтому не стоит увлекаться перегрузкой операторов. Использовать её следует только тогда, когда это упростит написание кода. Но, не настолько, чтобы это затрудняло чтение. Ведь, как известно, код читается намного чаще, чем пишется. И не забывайте, что вам никогда не дадут перегрузить операторы в тандеме со встроенными типами, возможность перегрузки есть только для пользовательских типов/классов.

Синтаксис перегрузки

Синтаксис перегрузки операторов очень похож на определение функции с именем operator@, где @ — это идентификатор оператора (например +, -, >). Рассмотрим простейший пример:

class Integer < private: int value; public: Integer(int i): value(i) <>const Integer operator+(const Integer& rv) const < return (value + rv.value); >>; 

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

В большинстве случаев, операторы (кроме условных) возвращают объект, или ссылку на тип, к которому относятся его аргументы (если типы разные, то вы сами решаете как интерпретировать результат вычисления оператора).

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

Рассмотрим примеры перегрузки унарных операторов для определенного выше класса Integer. Заодно определим их в виде дружественных функций и рассмотрим операторы декремента и инкремента:

class Integer < private: int value; public: Integer(int i): value(i) <>//унарный + friend const Integer& operator+(const Integer& i); //унарный - friend const Integer operator-(const Integer& i); //префиксный инкремент friend const Integer& operator++(Integer& i); //постфиксный инкремент friend const Integer operator++(Integer& i, int); //префиксный декремент friend const Integer& operator--(Integer& i); //постфиксный декремент friend const Integer operator--(Integer& i, int); >; //унарный плюс ничего не делает. const Integer& operator+(const Integer& i) < return i.value; >const Integer operator-(const Integer& i) < return Integer(-i.value); >//префиксная версия возвращает значение после инкремента const Integer& operator++(Integer& i) < i.value++; return i; >//постфиксная версия возвращает значение до инкремента const Integer operator++(Integer& i, int) < Integer oldValue(i.value); i.value++; return oldValue; >//префиксная версия возвращает значение после декремента const Integer& operator--(Integer& i) < i.value--; return i; >//постфиксная версия возвращает значение до декремента const Integer operator--(Integer& i, int)

Теперь вы знаете, как компилятор различает префиксные и постфиксные версии декремента и инкремента. В случае, когда он видит выражение ++i, то вызывается функция operator++(a). Если же он видит i++, то вызывается operator++(a, int). То есть вызывается перегруженная функция operator++, и именно для этого используется фиктивный параметр int в постфиксной версии.

Бинарные операторы

Рассмотрим синтаксис перегрузки бинарных операторов. Перегрузим один оператор, который возвращает l-значение, один условный оператор и один оператор, создающий новое значение (определим их глобально):

class Integer < private: int value; public: Integer(int i): value(i) <>friend const Integer operator+(const Integer& left, const Integer& right); friend Integer& operator+=(Integer& left, const Integer& right); friend bool operator==(const Integer& left, const Integer& right); >; const Integer operator+(const Integer& left, const Integer& right) < return Integer(left.value + right.value); >Integer& operator+=(Integer& left, const Integer& right) < left.value += right.value; return left; >bool operator==(const Integer& left, const Integer& right)

Во всех этих примерах операторы перегружаются для одного типа, однако, это необязательно. Можно, к примеру, перегрузить сложение нашего типа Integer и определенного по его подобию Float.

Аргументы и возвращаемые значения
  • Если аргумент не изменяется оператором, в случае, например унарного плюса, его нужно передавать как ссылку на константу. Вообще, это справедливо для почти всех арифметических операторов (сложение, вычитание, умножение. )
  • Тип возвращаемого значения зависит от сути оператора. Если оператор должен возвращать новое значение, то необходимо создавать новый объект (как в случае бинарного плюса). Если вы хотите запретить изменение объекта как l-value, то нужно возвращать его константным.
  • Для операторов присваивания необходимо возвращать ссылку на измененный элемент. Также, если вы хотите использовать оператор присваивания в конструкциях вида (x=y).f(), где функция f() вызывается для для переменной x, после присваивания ей y, то не возвращайте ссылку на константу, возвращайте просто ссылку.
  • Логические операторы должны возвращать в худшем случае int, а в лучшем bool.
Оптимизация возвращаемого значения

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

return Integer(left.value + right.value);

Честно говоря, не знаю, какая ситуация актуальна для C++11, все рассуждения далее справедливы для C++98.
На первый взгляд, это похоже на синтаксис создания временного объекта, то есть как будто бы нет разницы между кодом выше и этим:

Integer temp(left.value + right.value); return temp; 

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

Особые операторы

В C++ есть операторы, обладающие специфическим синтаксисом и способом перегрузки. Например оператор индексирования []. Он всегда определяется как член класса и, так как подразумевается поведение индексируемого объекта как массива, то ему следует возвращать ссылку.

Оператор запятая

В число «особых» операторов входит также оператор запятая. Он вызывается для объектов, рядом с которыми поставлена запятая (но он не вызывается в списках аргументов функций). Придумать осмысленный пример использования этого оператора не так-то просто. Хабраюзер AxisPod в комментариях к предыдущей статье о перегрузке рассказал об одном.

Оператор разыменования указателя

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

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

Оператор присваивания обязательно определяется в виде функции класса, потому что он неразрывно связан с объектом, находящимся слева от "=". Определение оператора присваивания в глобальном виде сделало бы возможным переопределение стандартного поведения оператора " cpp">class Integer < private: int value; public: Integer(int i): value(i) <>Integer& operator=(const Integer& right) < //проверка на самоприсваивание if (this == &right) < return *this; >value = right.value; return *this; > >;

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

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

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

  • Оператор выбора члена класса ".".
  • Оператор разыменования указателя на член класса ".*"
  • В С++ отсутствует оператор возведения в степень (как в Fortran) "**".
  • Запрещено определять свои операторы (возможны проблемы с определением приоритетов).
  • Нельзя изменять приоритеты операторов
Рекомендации к форме определения операторов

Как мы уже выяснили, существует два способа операторов — в виде функции класса и в виде дружественной глобальной функции.
Роб Мюррей, в своей книге C++ Strategies and Tactics определил следующие рекомендации по выбору формы оператора:

Оператор Рекомендуемая форма
Все унарные операторы Член класса
= () [] -> ->* Обязательно член класса
+= -= /= *= ^= &= |= %= >>= Член класса
Остальные бинарные операторы Не член класса

Почему так? Во-первых, на некоторые операторы изначально наложено ограничение. Вообще, если семантически нет разницы как определять оператор, то лучше его оформить в виде функции класса, чтобы подчеркнуть связь, плюс помимо этого функция будет подставляемой (inline). К тому же, иногда может возникнуть потребность в том, чтобы представить левосторонний операнд объектом другого класса. Наверное, самый яркий пример — переопределение > для потоков ввода/вывода.

Литература

Брюс Эккель — Философия C++. Введение в стандартный C++.

  • C++
  • operators overloading
  • перегрузка операторов

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

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