Перегрузка оператора сложения c. Основы перегрузки операторов. Объявления перегруженных функций-членов

Последнее обновление: 12.08.2018

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

Class Counter { public int Value { get; set; } }

Данный класс представляет некоторый счетчик, значение которого хранится в свойстве Value.

И допустим, у нас есть два объекта класса Counter - два счетчика, которые мы хотим сравнивать или складывать на основании их свойства Value, используя стандартные операции сравнения и сложения:

Counter c1 = new Counter { Value = 23 }; Counter c2 = new Counter { Value = 45 }; bool result = c1 > c2; Counter c3 = c1 + c2;

Но на данный момент ни операция сравнения, ни операция сложения для объектов Counter не доступны. Эти операции могут использоваться для ряда примитивных типов. Например, по умолчанию мы можем складывать числовые значения, но как складывать объекты комплексных типов - классов и структур компилятор не знает. И для этого нам надо выполнить перегрузку нужных нам операторов.

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

Public static возвращаемый_тип operator оператор(параметры) { }

Этот метод должен иметь модификаторы public static , так как перегружаемый оператор будет использоваться для всех объектов данного класса. Далее идет название возвращаемого типа. Возвращаемый тип представляет тот тип, объекты которого мы хотим получить. К примеру, в результате сложения двух объектов Counter мы ожидаем получить новый объект Counter. А в результате сравнения двух мы хотим получить объект типа bool, который указывает истинно ли условное выражение или ложно. Но в зависимости от задачи возвращаемые типы могут быть любыми.

Затем вместо названия метода идет ключевое слово operator и собственно сам оператор. И далее в скобках перечисляются параметры. Бинарные операторы принимают два параметра, унарные - один параметр. И в любом случае один из параметров должен представлять тот тип - класс или структуру, в котором определяется оператор.

Например, перегрузим ряд операторов для класса Counter:

Class Counter { public int Value { get; set; } public static Counter operator +(Counter c1, Counter c2) { return new Counter { Value = c1.Value + c2.Value }; } public static bool operator >(Counter c1, Counter c2) { return c1.Value > c2.Value; } public static bool operator <(Counter c1, Counter c2) { return c1.Value < c2.Value; } }

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

Так как в случае с операцией сложения мы хотим сложить два объекта класса Counter, то оператор принимает два объекта этого класса. И так как мы хотим в результате сложения получить новый объект Counter, то данный класс также используется в качестве возвращаемого типа. Все действия этого оператора сводятся к созданию, нового объекта, свойство Value которого объединяет значения свойства Value обоих параметров:

Public static Counter operator +(Counter c1, Counter c2) { return new Counter { Value = c1.Value + c2.Value }; }

Также переопределены две операции сравнения. Если мы переопределяем одну из этих операций сравнения, то мы также должны переопределить вторую из этих операций. Сами операторы сравнения сравнивают значения свойств Value и в зависимости от результата сравнения возвращают либо true, либо false.

Теперь используем перегруженные операторы в программе:

Static void Main(string args) { Counter c1 = new Counter { Value = 23 }; Counter c2 = new Counter { Value = 45 }; bool result = c1 > c2; Console.WriteLine(result); // false Counter c3 = c1 + c2; Console.WriteLine(c3.Value); // 23 + 45 = 68 Console.ReadKey(); }

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

Public static int operator +(Counter c1, int val) { return c1.Value + val; }

Данный метод складывает значение свойства Value и некоторое число, возвращая их сумму. И также мы можем применить этот оператор:

Counter c1 = new Counter { Value = 23 }; int d = c1 + 27; // 50 Console.WriteLine(d);

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

Public static Counter operator ++(Counter c1) { c1.Value += 10; return c1; }

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

И более корректная перегрузка оператора инкремента будет выглядеть так:

Public static Counter operator ++(Counter c1) { return new Counter { Value = c1.Value + 10 }; }

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

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

Например, используем операцию префиксного инкремента:

Counter counter = new Counter() { Value = 10 }; Console.WriteLine($"{counter.Value}"); // 10 Console.WriteLine($"{(++counter).Value}"); // 20 Console.WriteLine($"{counter.Value}"); // 20

Консольный вывод:

Теперь используем постфиксный инкремент:

Counter counter = new Counter() { Value = 10 }; Console.WriteLine($"{counter.Value}"); // 10 Console.WriteLine($"{(counter++).Value}"); // 10 Console.WriteLine($"{counter.Value}"); // 20

Консольный вывод:

Также стоит отметить, что мы можем переопределить операторы true и false . Например, определим их в классе Counter:

Class Counter { public int Value { get; set; } public static bool operator true(Counter c1) { return c1.Value != 0; } public static bool operator false(Counter c1) { return c1.Value == 0; } // остальное содержимое класса }

Эти операторы перегружаются, когда мы хотим использовать объект типа в качестве условия. Например:

Counter counter = new Counter() { Value = 0 }; if (counter) Console.WriteLine(true); else Console.WriteLine(false);

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

    унарные операторы +, -, !, ~, ++, --

    бинарные операторы +, -, *, /, %

    операции сравнения ==, !=, <, >, <=, >=

    логические операторы &&, ||

    операторы присваивания +=, -=, *=, /=, %=

И есть ряд операторов, которые нельзя перегрузить, например, операцию равенства = или тернарный оператор?: , а также ряд других.

Полный список перегружаемых операторов можно найти в документации msdn

При перегрузке операторов также следует помнить, что мы не можем изменить приоритет оператора или его ассоциативность, мы не можем создать новый оператор или изменить логику операторов в типах, который есть по умолчанию в.NET.

Мы рассмотрели основные аспекты использования перегрузки операторов. В этом материалы вашему вниманию будут представлены перегружаемые операторы C++. Для каждого раздела характерна семантика, т.е. ожидаемое поведение. Кроме того, будут показаны типичные способы объявления и реализации операторов.

В примерах кода X означает пользовательский тип, для которого реализован оператор. T - это необязательный тип, пользовательский либо встроенный. Параметры бинарного оператора будут называться lhs и rhs . Если оператор будет объявлен как метод класса, у его объявления будет префикс X:: .

operator=

  • Определение справа налево : в отличие от большинства операторов, operator= правоассоциативен, т.е. a = b = c означает a = (b = c) .

Копирование

  • Семантика : присваивание a = b . Значение или состояние b передаётся a . Кроме того, возвращается ссылка на a . Это позволяет создавать цепочки вида c = a = b .
  • Типичное объявление : X& X::operator= (X const& rhs) . Возможны другие типы аргументов, но используется это нечасто.
  • Типичная реализация : X& X::operator= (X const& rhs) { if (this != &rhs) { //perform element wise copy, or: X tmp(rhs); //copy constructor swap(tmp); } return *this; }

Перемещение (начиная с C++11)

  • Семантика : присваивание a = temporary() . Значение или состояние правой величины присваивается a путём перемещения содержимого. Возвращается ссылка на a .
  • : X& X::operator= (X&& rhs) { //take the guts from rhs return *this; }
  • Сгенерированный компилятором operator= : компилятор может создать только два вида этого оператора. Если же оператор не объявлен в классе, компилятор пытается создать публичные операторы копирования и перемещения. Начиная с C++11 компилятор может создавать оператор по умолчанию: X& X::operator= (X const& rhs) = default;

    Сгенерированный оператор просто копирует/перемещает указанный элемент, если такая операция разрешена.

operator+, -, *, /, %

  • Семантика : операции сложения, вычитания, умножения, деления, деления с остатком. Возвращается новый объект с результирующим значением.
  • Типичные объявление и реализация : X operator+ (X const lhs, X const rhs) { X tmp(lhs); tmp += rhs; return tmp; }

    Обычно, если существует operator+ , имеет смысл также перегрузить и operator+= для того, чтобы использовать запись a += b вместо a = a + b . Если же operator+= не перегружен, реализация будет выглядеть примерно так:

    X operator+ (X const& lhs, X const& rhs) { // create a new object that represents the sum of lhs and rhs: return lhs.plus(rhs); }

Унарные operator+, —

  • Семантика : положительный или отрицательный знак. operator+ обычно ничего не делает и поэтому почти не используется. operator- возвращает аргумент с противоположным знаком.
  • Типичные объявление и реализация : X X::operator- () const { return /* a negative copy of *this */; } X X::operator+ () const { return *this; }

operator<<, >>

  • Семантика : во встроенных типах операторы используются для битового сдвига левого аргумента. Перегрузка этих операторов с именно такой семантикой встречается редко, на ум приходит лишь std::bitset . Однако, для работы с потоками была введена новая семантика, и перегрузка операторов ввода/вывода весьма распространена.
  • Типичные объявление и реализация : поскольку в стандартные классы iostream добавлять методы нельзя, операторы сдвига для определённых вами классов нужно перегружать в виде свободных функций: ostream& operator<< (ostream& os, X const& x) { os << /* the formatted data of rhs you want to print */; return os; } istream& operator>> (istream& is, X& x) { SomeData sd; SomeMoreData smd; if (is >> sd >> smd) { rhs.setSomeData(sd); rhs.setSomeMoreData(smd); } return lhs; }

    Кроме того, тип левого операнда может быть любым классом, которые должен вести себя как объект ввода/вывода, то есть правый операнд может быть и встроенного типа.

    MyIO& MyIO::operator<< (int rhs) { doYourThingWith(rhs); return *this; }

Бинарные operator&, |, ^

  • Семантика : Битовые операции «и», «или», «исключающее или». Эти операторы перегружаются очень редко. Опять же, единственным примером является std::bitset .

operator+=, -=, *=, /=, %=

  • Семантика : a += b обычно означает то же, что и a = a + b . Поведение остальных операторов аналогично.
  • Типичные определение и реализация : поскольку операция изменяет левый операнд, скрытое приведение типов нежелательно. Поэтому эти операторы должны быть перегружены как методы класса. X& X::operator+= (X const& rhs) { //apply changes to *this return *this; }

operator&=, |=, ^=, <<=, >>=

  • Семантика : аналогична operator+= , но для логических операций. Эти операторы перегружаются так же редко, как и operator| и т.д. operator<<= и operator>>= не используются для операций ввода/вывода, поскольку operator<< и operator>> уже изменяют левый аргумент.

operator==, !=

  • Семантика : проверка на равенство/неравенство. Смысл равенства очень сильно зависит от класса. В любом случае, учитывайте следующие свойства равенств:
    1. Рефлексивность, т.е. a == a .
    2. Симметричность, т.е. если a == b , то b == a .
    3. Транзитивность, т.е. если a == b и b == c , то a == c .
  • Типичные объявление и реализация : bool operator== (X const& lhs, X cosnt& rhs) { return /* check for whatever means equality */ } bool operator!= (X const& lhs, X const& rhs) { return !(lhs == rhs); }

    Вторая реализация operator!= позволяет избежать повторов кода и исключает любую возможную неопределённость в отношении любых двух объектов.

operator<, <=, >, >=

  • Семантика : проверка на соотношение (больше, меньше и т.д.). Обычно используется, если порядок элементов однозначно определён, то есть сложные объекты с несколькими характеристиками сравнивать бессмысленно.
  • Типичные объявление и реализация : bool operator< (X const& lhs, X const& rhs) { return /* compare whatever defines the order */ } bool operator> (X const& lhs, X const& rhs) { return rhs < lhs; }

    Реализация operator> с использованием operator< или наоборот обеспечивает однозначное определение. operator<= может быть реализован по-разному, в зависимости от ситуации . В частности, при отношении строго порядка operator== можно реализовать лишь через operator< :

    Bool operator== (X const& lhs, X const& rhs) { return !(lhs < rhs) && !(rhs < lhs); }

operator++, –

  • Семантика : a++ (постинкремент) увеличивает значение на 1 и возвращает старое значение. ++a (преинкремент) возвращает новое значение. С декрементом operator-- все аналогично.
  • Типичные объявление и реализация : X& X::operator++() { //preincrement /* somehow increment, e.g. *this += 1*/; return *this; } X X::operator++(int) { //postincrement X oldValue(*this); ++(*this); return oldValue; }

operator()

  • Семантика : исполнение объекта-функции (функтора). Обычно используется не для изменения объекта, а для использования его в качестве функции.
  • Нет ограничений на параметры : в отличие от прошлых операторов, в этом случае нет никаких ограничений на количество и тип параметров. Оператор может быть перегружен только как метод класса.
  • Пример объявления : Foo X::operator() (Bar br, Baz const& bz);

operator

  • Семантика : доступ к элементам массива или контейнера, например, в std::vector , std::map , std::array .
  • Объявление : тип параметра может быть любым. Тип возвращаемого значения обычно является ссылкой на то, что хранится в контейнере. Часто оператор перегружается в двух версиях, константной и неконстантной: Element_t& X::operator(Index_t const& index); const Element_t& X::operator(Index_t const& index) const;

operator!

  • Семантика : отрицание в логическом смысле.
  • Типичные объявление и реализация : bool X::operator!() const { return !/*some evaluation of *this*/; }

explicit operator bool

  • Семантика : использования в логическом контексте. Чаще всего используется с умными указателями.
  • Реализация : explicit X::operator bool() const { return /* if this is true or false */; }

operator&&, ||

  • Семантика : логические «и», «или». Эти операторы определены только для встроенного логического типа и работают по «ленивому» принципу, то есть второй аргумент рассматривается, только если первый не определяет результат. При перегрузке это свойство теряется, поэтому перегружают эти операторы редко.

Унарный operator*

  • Семантика : разыменовывание указателя. Обычно перегружается для классов с умными указателями и итераторами. Возвращает ссылку на то, куда указывает объект.
  • Типичные объявление и реализация : T& X::operator*() const { return *_ptr; }

operator->

  • Семантика : доступ к полю по указателю. Как и предыдущий, этот оператор перегружается для использования с умными указателями и итераторами. Если в коде встречается оператор -> , компилятор перенаправляет вызовы на operator-> , если возвращается результат пользовательского типа.
  • Usual implementation : T* X::operator->() const { return _ptr; }

operator->*

  • Семантика : доступ к указателю-на-поле по указателю. Оператор берёт указатель на поле и применяет его к тому, на что указывает *this , то есть objPtr->*memPtr - это то же самое, что и (*objPtr).*memPtr . Используется очень редко.
  • Возможная реализация : template T& X::operator->*(T V::* memptr) { return (operator*()).*memptr; }

    Здесь X - это умный указатель, V - тип, на который указывает X , а T - тип, на который указывает указатель-на-поле. Неудивительно, что этот оператор редко перегружают.

Унарный operator&

  • Семантика : адресный оператор. Этот оператор перегружают очень редко.

operator,

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

operator~

  • Семантика : оператор побитовой инверсии. Один из наиболее редко используемых операторов.

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

  • Семантика : позволяет скрытое или явное приведение объектов класса к другим типам.
  • Объявление : //conversion to T, explicit or implicit X::operator T() const; //explicit conversion to U const& explicit X::operator U const&() const; //conversion to V& V& X::operator V&();

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

operator new, new, delete, delete

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

Заключение

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

Доброго времени суток!

Желание написать данную статью появилось после прочтения поста Перегрузка 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) { Integer oldValue(i.value); i.value--; return oldValue; }
Теперь вы знаете, как компилятор различает префиксные и постфиксные версии декремента и инкремента. В случае, когда он видит выражение ++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) { return left.value == right.value; }
Во всех этих примерах операторы перегружаются для одного типа, однако, это необязательно. Можно, к примеру, перегрузить сложение нашего типа 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 в комментариях к предыдущей статье о перегрузке рассказал об одном .
Оператор разыменования указателя
Перегрузка этих операторов может быть оправдана для классов умных указателей. Этот оператор обязательно определяется как функция класса, причём на него накладываются некоторые ограничения: он должен возвращать либо объект (или ссылку), либо указатель, позволяющий обратиться к объекту.
Оператор присваивания
Оператор присваивания обязательно определяется в виде функции класса, потому что он неразрывно связан с объектом, находящимся слева от "=". Определение оператора присваивания в глобальном виде сделало бы возможным переопределение стандартного поведения оператора "=". Пример:
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++ - перегрузкой операций. Но сначала разберёмся, для чего это нужно.

С базовыми типами вы можете использовать любые операции: +, -, *, ++, = и многие другие. Например:

int a=2,b=3,c;
c = a + b;

Здесь над переменными типа int сначала выполняется операция +, а затем результат присваивается переменной c с помощью операции =. Над классами такое проделать не получится. Создадим простой класс:

код на языке c++ class Counter { public: int counter; Counter() : c(0) {} }; Counter a,b,c; a.counter = 2; b.counter = 3; c = a + b;

Здесь компилятор выдаст ошибку на последней строке: он не знает как именно нужно действовать при использовании операций = и + над объектами класса Counter. Можно решить данную проблему вот так:

код на языке c++ class Counter { public: int counter; Counter() : count(0) {} void AddCounters (Counter& a, Counter& b) { counter = a.counter + b.counter; } }; Counter a,b,c; a.counter = 2; b.counter = 3; c.AddCounters(a,b);

Согласитесь, использование операции + и = в данном случае сделало бы программу более понятной. Так вот, чтобы использовать стандартные операции C++ с классами, эти операции нужно перегрузить (overload).

Перегрузка унарных операций

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

код на языке c++ int a=1; ++a; // a = 2; --a; // a = 1;

Теперь научим класс Counter использовать преинкремент (предекремент):

код на языке c++ class Counter { private: counter; public: Counter() : counter(0) {} void operator++ () { counter += 1; // Можно также counter++ или ++counter - // это не имеет значения в данном случае. } }; Counter a; ++a; ++a; ++a;

Этот код работает. При использовании операции ++ (важно чтобы этот знак находился до идентификатора!) над объектом класса Counter, происходит увеличение переменной counter объекта a.

В данном примере мы перегрузили операцию ++. Делается это созданием метода внутри класса. Единственной важной особенностью данного метода - имя идентификатора. Имя идентификатора для перегруженных операций состоит из ключевого слова operator и названия операции. Во всём остальном этот метод определяется как и любые другие.

Использовать перегруженные операции с пользовательскими типами очень просто - так же как и с обычными типами данных.

Перегрузка постфиксных операций

Примеры постфиксной операции:

int a = 3;
a++;
a--;

То есть, здесь знак операции ставится после имени идентификатора. Для использования постфиксных операций с пользовательскими типами данных нужно совсем немного:

код на языке c++ public: void operator++ () { counter += 1; } void operator++ (int) { counter += 1; }

Единственным отличием префиксной операции от постфиксной - ключевое слово int в списке аргументов. Но int - это не аргумент! Это слово говорит, что перегружается постфиксная операция. Теперь операцию ++ можно использовать как перед идентификатором объекта, так и после:

Counter a;
++a;
++a;
a++;
a++;

Перегрузка бинарных операций

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

код на языке c++ Counter operator+ (Counter t) { Counter summ; summ.counter = counter + t.counter; return summ; } Counter c1,c2,c3; c1.counter = 3; c2.counter = 2; c3 = c1 + c2;

Какая переменная вызовет функцию operator+? В перегруженных бинарных операциях всегда вызывается метод левого операнда. В данном случае метод operator+ вызывает объект c1.

В метод мы передаём аргумент. Аргументом всегда является правый операнд.

Кроме того, в данном случае операция + должна вернуть какой-то результат, чтобы присвоить его объекту c3. Мы возвращаем объект Counter. Возвращаемое значение присваивается переменной c3.

Заметьте, мы перегрузили операцию +, но не перегружали операцию =! Конечно же нужно добавить этот метод к классу Counter:

код на языке c++ Counter operator= (Counter t) { Counter assign; counter = t.counter; assign.counter = t.counter; return assign; }

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

Counter c1(5),c2,c3;
c3 = c2 = c1;

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

С++ поддерживает перегрузку операторов (operator overloading). За небольшими исключениями большинство операторов С++ могут быть перегружены, в результате чего они получат специаль­ное значение по отношению к определенным классам. Например, класс, определяющий связан­ный список, может использовать оператор + для того, чтобы добавлять объект к списку. Другой класс может использовать оператор + совершенно иным способом. Когда оператор перегружен, ни одно из его исходных значений не теряет смысла. Просто для определенного класса объектов определен новый оператор. Поэтому перегрузка оператора + для того, чтобы обрабатывать свя­занный список, не изменяет его действия по отношению к целым числам.

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

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

тип имя_класса::operator#(список_аргументов)
{
// действия, определенные применительно к классу
}

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

Чтобы увидеть, как работает перегрузка операторов, начнем с простого примера. В нем созда­ется класс three_d, содержащий координаты объекта в трехмерном пространстве. Следующая про­грамма перегружает операторы + и = для класса three_d:

#include
class three_d {

public:
three_d operators+(three_d t);
three_d operator=(three_d t);
void show ();

};
// перегрузка +
three_d three_d::operator+(three_d t)
{
three_d temp;
temp.x = x+t.x;
temp.у = y+t.y;
temp.z = z+t.z;
return temp;
}
// перегрузка =
three_d three_d::operator=(three_d t)
{
x = t.x;
y = t.y;
z = t.z;
return *this;
}
// вывод координат X, Y, Z
void three_d::show ()
{
cout << x << ", ";
cout << у << ", ";
cout << z << "\n";
}
// присвоение координат

{
x = mx;
y = my;
z = mz;
}
int main()
{
three_d a, b, c;
a.assign (1, 2, 3);
b.assign (10, 10, 10);
a.show();
b.show();
с = a+b; // сложение а и b
c.show();

с.show();

с.show();
b.show ();
return 0;
}

Эта программа выводит на экран следующие данные:

1, 2, 3
10, 10, 10
11, 12, 13
22, 24, 26
1, 2, 3
1, 2, 3

Если рассмотреть эту программу внимательно, может вызвать удивление, что обе функции-опе­ратора имеют только по одному параметру, несмотря на то, что они перегружают бинарный оператор. Это связано с тем, что при перегрузке бинарного оператора с использованием функции-члена ей передается явным образом только один аргумент. Вторым аргументом служит ука­затель this, который передается ей неявно. Так, в строке

Temp.x = х + t.x;

Х соответствует this->x, где х ассоциировано с объектом, который вызывает функцию-оператор. Во всех случаях именно объект слева от знака операции вызывает функцию-оператор. Объект, стоящий справа от знака операции, передается функции.

При перегрузке унарной операции функция-оператор не имеет параметров, а при перегрузке бинарной операции функция-оператор имеет один параметр. (Нельзя перегрузить триадный опе­ратор?:.) Во всех случаях объект, активизирующий функцию-оператор, передается неявным об­разом с помощью указателя this.

Чтобы понять, как работает перегрузка операторов, тщательно проанализируем, как работа­ет предыдущая программа, начиная с перегруженного оператора +. Когда два объекта типа three_d подвергаются воздействию оператора +, значения их соответствующих координат скла­дываются, как это показано в функции operator+(), ассоциированной с данным классом. Обра­тим, однако, внимание, что функция не модифицирует значений операндов. Вместо этого она возвращает объект типа three_d, содержащий результат выполнения операции. Чтобы понять, почему оператор + не изменяет содержимого объектов, можно представить себе стандартный арифметический оператор +, примененный следующим образом: 10 + 12. Результатом этой опе­рации является 22, однако ни 10 ни 12 от этого не изменились. Хотя не существует правила о том, что перегруженный оператор не может изменять значений своих операндов, обычно име­ет смысл следовать ему. Если вернуться к данному примеру, то нежелательно, чтобы оператор + изменял содержание операндов.

Другим ключевым моментом перегрузки оператора сложения служит то, что он возвращает объект типа three_d. Хотя функция может иметь в качестве значения любой допустимый тип язы­ка С++, тот факт, что она возвращает объект типа three_d, позволяет использовать оператор + в более сложных выражениях, таких, как a+b+с. Здесь а+b создает результат типа three_d. Это значение затем прибавляется к с. Если бы значением суммы а+b было значение другого типа, то мы не могли бы затем прибавить его к с.

В противоположность оператору +, оператор присваивания модифицирует свои аргументы. (В этом, кроме всего прочего, и заключается смысл присваивания.) Поскольку функция operator=() вызывается объектом, стоящим слева от знака равенства, то именно этот объект модифицируется
при выполнении операции присваивания. Однако даже оператор присваивания обязан возвра­щать значение, поскольку как в С++, так и в С оператор присваивания порождает величину, стоящую с правой стороны равенства. Так, для того, чтобы выражение следующего вида

А = b = с = d;

Было допустимым, необходимо, чтобы оператор operator=() возвращал объект, на который ука­зывает указатель this и который будет объектом, стоящим с левой стороны оператора присваива­ния. Если сделать таким образом, то можно выполнить множественное присваивание.

Можно перегрузить унарные операторы, такие как ++ или --. Как уже говорилось ранее, при перегрузке унарного оператора с использованием функции-члена, эта функция-член не имеет аргументов. Вместо этого операция выполняется над объектом, осуществляющим вызов функции-оператора путем неявной передачи указателя this. В качестве примера ниже рассмотрена расши­ренная версия предыдущей программы, в которой определяется оператор-инкремент для объекта типа three_d:

#include
class three_d {
int x, y, z; // трехмерные координаты
public:
three_d operator+(three_d op2); // op1 подразумевается
three_d operator=(three_d op2); // op1 подразумевается
three_d operator++ (); // op1 также подразумевается
void show();
void assign (int mx, int my, int mz);
};
// перегрузка +
three_d three_d::operator+(three_d op2)
{
three_d temp;
temp.x = x+op2.x; // целочисленное сложение
temp.у = y+op2.y; // и в данном случае + сохраняет
temp.z = z+op2.z; // первоначальное значение
return temp;
}
// перегрузка =
three_d three_d::operator=(three_d op2)
{
x = op2.x; // целочисленное присваивание
у = op2.y; // и в данном случае = сохраняет
z = op2.z; // первоначальное значение
return *this;
}
// перегрузка унарного оператора
three_d three_d::operator++()
{
х++;
у++;
z++;
return *this;
}
// вывести координаты X, Y, Z
void three_d::show()
{
cout << x << ", ";
cout << у << ", ";
cout << z << "\n";
}
// присвоение координат
void three_d::assign (int mx, int my, int mz)
{
x = mx;
y = my;
z = mz;
}
int main()
{
three_d a, b, c;
a.assign (1, 2, 3);
b.assign (10, 10, 10);
a.show();
b.show();
с = a+b; // сложение а и b
c.show();
с = a+b+c; // сложение a, b и с
с.show();
с = b = a; // демонстрация множественного присваивания
с.show();
b.show ();
++c; // увеличение с
c.show();
return 0;
}

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

O++;
++O;

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

Loc operator++(int х);

Если ++ предшествует операнду, то вызывается функция operator++(). Если же ++ следует за операндом, то тогда вызывается функция operator++(int х), где х принимает значение 0.

Действие перегруженного оператора по отношению к тому классу, для которого он опреде­лен, не обязательно должно соответствовать каким-либо образом действию этого оператора для встроенных типов С++. Например, операторы << и >> применительно к cout и cin имеют мало общего с их действием на переменные целого типа. Однако, исходя из стремления сделать код более легко читаемым и хорошо структурированным, желательно, чтобы перегруженные опера­торы соответствовали, там где это возможно, смыслу исходных операторов. Например, оператор + по отношению к классу three_d концептуально сходен с оператором + для переменных целого типа. Мало пользы, например, можно ожидать от такого оператора +, действие которого на
соответствующий класс будет напоминать действие оператора ||. Хотя можно придать перегру­женному оператору любой смысл по своему выбору, но для ясности его применения желательно, чтобы его новое значение соотносилось с исходным значением.

Имеются некоторые ограничения на перегрузку операторов. Во-первых, нельзя изменить при­оритет оператора. Во-вторых, нельзя изменить число операндов оператора. Наконец, за исклю­чением оператора присваивания, перегруженные операторы наследуются любым производным классом. Каждый класс обязан определить явным образом свой собственный перегруженный опе­ратор =, если он требуется для каких-либо целей. Разумеется, производные классы могут пере­грузить любой оператор, включая и тот, который был перегружен базовым классом. Следующие операторы не могут быть перегружены:
. :: * ?



Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: