Перевантажені оператори. Оператори new і 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); декременту 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); bool operator==(const Integer& left, const Integer& right); ); const Integer operator + (const Integer & left, const Integer & right) ( return Integer (left.value + right.value); ) 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++, що перевантажуються. До кожного розділу характерна семантика, тобто. очікувана поведінка. Крім того, будуть показані типові способи оголошення та реалізації операторів.

У прикладах коду 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.
  • : 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 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

Ці оператори повністю відрізняються від усіх вищезгаданих, оскільки вони не працюють з користувальницькими типами. Їхнє навантаження дуже складне, і тому не буде тут розглядатися.

Висновок

Основною думкою є таке: не варто перевантажувати оператори лише тому, що ви вмієте це робити. Перевантажуйте їх лише в тих випадках, коли це виглядає природним та необхідним. Але пам'ятайте, що якщо ви перевантажите один оператор, то доведеться перевантажувати інші.

Доброго вам дня!

Бажання написати цю статтю з'явилося після прочитання посту, тому що в ньому не було розкрито багато важливих тем.

Найголовніше, що необхідно пам'ятати - перевантаження операторів, це лише зручніший спосіб виклику функцій, тому не варто захоплюватися перевантаженням операторів. Використовувати її слід лише тоді, коли це спростить написання коду. Але, не настільки, щоб це ускладнювало читання. Адже, як відомо, код читається набагато частіше, ніж пишеться. І не забувайте, що вам ніколи не дадуть перевантажити оператори в тандемі з вбудованими типами, можливість перевантаження є тільки для типів/класів.

Синтаксис навантаження

Синтаксис перевантаження операторів дуже схожий визначення функції з ім'ям 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); 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); декременту 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); bool operator==(const Integer& left, const Integer& right); ); const Integer operator + (const Integer & left, const Integer & right) ( return Integer (left.value + right.value); ) 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++ є оператори, які мають специфічний синтаксис і спосіб перевантаження. Наприклад оператор індексування. Він завжди визначається як член класу і, оскільки мається на увазі поведінка об'єкта, що індексується як масиву, то йому слід повертати посилання.
Оператор кома
До «особливих» операторів входить також оператор кома. Він викликається для об'єктів, поруч із якими поставлена ​​кома (але не викликається у списках аргументів функцій). Придумати осмислений приклад використання цього оператора не так просто. Хабраюзер у коментарях до попередньої статті про перевантаження.
Оператор розіменування покажчика
Перевантаження цих операторів можна виправдати для класів розумних покажчиків. Цей оператор обов'язково визначається як функція класу, причому на нього накладаються деякі обмеження: він повинен повертати або об'єкт (або посилання) або покажчик, що дозволяє звернутися до об'єкта.
Оператор присвоєння
Оператор присвоювання обов'язково визначається як функції класу, оскільки він нерозривно пов'язані з об'єктом, що є ліворуч від " = " . Визначення оператора присвоєння у глобальному вигляді уможливило б перевизначення стандартної поведінки оператора "=". Приклад:
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
  • навантаження операторів
Додати теги

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

У C#, подібно до будь-якої мови програмування, є готовий набір лексем, що використовуються для виконання базових операційнад вбудованими типами. Наприклад, відомо, що операція + може застосовуватися до двох цілих, щоб дати їхню суму:

// Операція + з цілими. int а = 100; int b = 240; int = а + b; //з ​​тепер одно 340

Тут немає нічого нового, але чи замислювалися ви коли-небудь про те, що та сама операція + може застосовуватися до більшості вбудованих типів даних C#? Наприклад, розглянемо такий код:

// Операція + з рядками. string si = "Hello"; string s2 = "world!"; string s3 = si + s2; // s3 тепер містить Hello world!

По суті, функціональність операції + унікальним чином базуються на представлених типах даних (рядках чи цілих у цьому випадку). Коли операція + застосовується до числовим типам, ми отримуємо арифметичну сумуоперандів. Однак коли та сама операція застосовується до рядковим типам, Виходить конкатенація рядків.

Мова C# надає можливість будувати спеціальні класи та структури, які також унікально реагують на той самий набір базових лексем (на кшталт операції +). Майте на увазі, що абсолютно кожну вбудовану операцію C# перевантажувати не можна. У наступній таблиці описані можливості перевантаження основних операцій:

Операція C# Можливість навантаження
+, -, !, ++, --, true, false Цей набір унарних операцій може бути перевантажений
+, -, *, /, %, &, |, ^, > Ці бінарні операції можуть бути перевантажені
==, !=, <, >, <=, >= Ці операції порівняння можуть бути перевантажені. C# вимагає спільного навантаження " подібних " операцій (тобто.< и >, <= и >=, == і!=)
Операція не може бути перевантажена. Проте аналогічну функціональність пропонують індексатори.
() Операція () не може бути перевантажена. Однак ту ж функціональність надають спеціальні методи перетворення
+=, -=, *=, /=, %=, &=, |=, ^=, >= Скорочені операції присвоювання що неспроможні перевантажуватися; однак ви отримуєте їх автоматично, перевантажуючи відповідну бінарну операцію

Перевантаження операторів тісно пов'язані з перевантаженням методів. Для навантаження оператора служить ключове слово operatorщо визначає операторний метод, який, у свою чергу, визначає дію оператора щодо свого класу. Існують дві форми операторних методів (operator): одна – для унарних операторів, інша – для бінарних. Нижче наведено загальну форму для кожного різновиду цих методів:

// Загальна форманавантаження унарного оператора. public static повертається_тип operator op(тип_параметра операнд) (// Операції) // Загальна форма навантаження бінарного оператора. public static повертається_тип operator op(тип_параметра1 операнд1, тип_параметра2 операнд2) (// Операції)

Тут замість op підставляється оператор, що перевантажується, наприклад + або /, а повертається_типпозначає конкретний типзначення, що повертається зазначеною операцією. Це значення може бути будь-якого типу, але часто воно вказується такого ж типу, як і клас, для якого перевантажується оператор. Така кореляція спрощує застосування операторів, що перевантажуються, у виразах. Для унарних операторів операндпозначає операнд, що передається, а для бінарних операторів те ж саме позначають операнд1і операнд2. Зверніть увагу на те, що операторні методи повинні мати обидва специфікатори типу - public і static.

Перевантаження бінарних операторів

Давайте розглянемо застосування навантаження бінарних операторів на найпростішому прикладі:

Using System; використовуючи System.Collections.Generic; використовуючи System.Linq; використовуючи System.Text; namespace ConsoleApplication1 ( class MyArr ( // Координати точки в тривимірному просторі public int x, y, z; public MyArr (int x = 0, int y = 0, int z = 0) ( this.x = x; this.y = y;this.z=z;) // Перевантажуємо бінарний оператор+ public static Operator MyArr + (MyArr obj1, MyArr obj2) ( MyArr arr = New MyArr (); arr.x = obj1.x + obj2.x; arr.y = obj1.y + obj2.y; arr.z = obj1.z + obj2.z, return arr; arr.y = obj1.y - obj2.y; arr.z = obj1.z - obj2.z; return arr; , -4);MyArr Point2 = new MyArr(0, -3, 18); .WriteLine("Координати другої точки: " + Point2.x + " " + Point2.y + " " + Point2.z + "\n"); MyArr Point3 = Point1 + Point2; Console.WriteLine("\nPoint1 + Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Point3 = Point1 - Point2; Console.WriteLine("\nPoint1 - Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Console.ReadLine(); ) ) )

Перевантаження унарних операторів

Унарні оператори перевантажуються так само, як і бінарні. Головна відмінність полягає, звичайно, у тому, що вони мають лише один операнд. Давайте модернізуємо попередній приклад, доповнивши навантаження операцій ++, --, -:

Using System; використовуючи System.Collections.Generic; використовуючи System.Linq; використовуючи System.Text; namespace ConsoleApplication1 ( class MyArr ( // Координати точки в тривимірному просторі public int x, y, z; public MyArr (int x = 0, int y = 0, int z = 0) ( this.x = x; this.y = y; y = obj1.y + obj2.y; arr.z = obj1.z + obj2.z; return arr; (); arr.x = obj1.x - obj2.x; arr.y = obj1.y - obj2.y; static Operator MyArr -(MyArr obj1) ( MyArr arr = new MyArr(); arr.x = -obj1.x; arr.y = -obj1.y; arr.z = -obj1.z; return arr; ) // Перевантажуємо унарний оператор ++ (MyArr obj1) ( obj1.x += 1; obj1.y += 1; obj1.z +=1; return obj1; ) // Перевантажуємо унарний оператор -- public static MyArr operator --(MyArr obj1) (obj1.x -= 1; obj1.y -= 1; obj1.z -= 1; return obj1; ) ) class Program ( static void Main(string args) ( MyArr Point1 = новий MyArr(1, 12, -4); MyArr Point2 = новий MyArr(0, -3, 18); Console.WriteLine("Координати першої точки: + Point1.x + Point1.y + Point1.z); Console.WriteLine("Координати другої точки: + Point2.x + Point2.y + Point2.z + MyArr Point3 = Point1 + Point2; Console.WriteLine("Point1 - Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Point3 = -Point1; Console.WriteLine("-Point1 = " + Point3.x + " " + Point3.y + " " + Point3.z); Point2++; Console.WriteLine("Point2++ = " + Point2.x + " " + Point2.y + " " + Point2.z); Point2-; WriteLine("Point2-- = " + Point2.x + " " + Point2.y + " " + Point2.z); Console.ReadLine(); ) ) )

Останнє оновлення: 20.10.2017

Перевантаження операторів дозволяє визначити дії, які виконуватиме оператор. Перевантаження передбачає створення функції, назва якої містить слово operator і символ оператора, що перевантажується. Функція оператора може бути визначена як член класу або поза класом.

Перевантажити можна тільки оператори, які вже визначені в C++. Створити нові оператори не можна.

Якщо функція оператора визначена як окрема функція і не є членом класу, кількість параметрів такої функції збігається з кількістю операндів оператора. Наприклад, функція, яка представляє унарний оператор, матиме один параметр, а функцію, яка представляє бінарний оператор, - два параметри. Якщо оператор приймає два операнди, то перший операнд передається першому параметру функції, а другий операнд - другому параметру. При цьому як мінімум один із параметрів повинен представляти тип класу

Розглянемо приклад із класом Counter, який представляє секундомір та зберігає кількість секунд:

#include << seconds << " seconds" << std::endl; } int seconds; }; Counter operator + (Counter c1, Counter c2) { return Counter(c1.seconds + c2.seconds); } int main() { Counter c1(20); Counter c2(10); Counter c3 = c1 + c2; c3.display(); // 30 seconds return 0; }

Тут функція оператора не є частиною класу Counter та визначена поза ним. Ця функція перевантажує оператор додавання для типу Counter. Вона є бінарною, тому приймає два параметри. В даному випадку ми складаємо два об'єкти Counter. Повертає функція також об'єкт Counter, який зберігає загальну кількість секунд. Тобто по суті тут операція додавання зводиться до складання секунд обох об'єктів:

Counter operator + (Counter c1, Counter c2) ( return Counter(c1.seconds + c2.seconds); )

У цьому необов'язково повертати об'єкт класу. Це може бути об'єкт вбудованого примітивного типу. Також ми можемо визначати додаткові перевантажені функції операторів:

Int operator + (Counter c1, int s) ( return c1.seconds + s; )

Ця версія складає об'єкт Counter з числом та повертає також число. Тому лівий операнд операції має представляти тип Counter, а правий операнд - тип int. І, наприклад, ми можемо застосувати цю версію оператора так:

Counter c1(20); int seconds = c1 + 25; // 45 std::cout<< seconds << std::endl;

Також функції операторів можна визначити як члени класів. Якщо функція оператора визначена як член класу, то лівий операнд доступний через покажчик this і представляє поточний об'єкт, а правий операнд передається в подібну функцію як єдиний параметр:

#include class Counter ( public: Counter(int sec) ( seconds = sec; ) void display() ( std::cout<< seconds << " seconds" << std::endl; } Counter operator + (Counter c2) { return Counter(this->seconds + c2.seconds); ) int operator + (int s) ( return this->seconds + s; ) int seconds; ); int main() ( Counter c1(20); Counter c2(10); Counter c3 = c1 + c2; c3.display(); // 30 seconds int seconds = c1 + 25; // 45 return 0; )

В даному випадку до лівого операнда в функціях операторів ми звертаємося через покажчик цього.

Які оператори де перевизначати? Оператори присвоєння, індексування (), виклику (()), доступу до члена класу за вказівником (->) слід визначати як функцій-членів класу. Оператори, які змінюють стан об'єкта чи безпосередньо пов'язані з об'єктом (інкремент, декремент,), зазвичай також визначаються як функцій-членів класу. Решта операторів частіше визначаються як окремі функції, а чи не члени класу.

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

Ряд операторів перевантажуються парами. Наприклад, якщо ми визначаємо оператор == , необхідно також визначити і оператор != . А при визначенні оператора< надо также определять функцию для оператора >. Наприклад, перевантажимо дані оператори:

Bool operator == (Counter c1, Counter c2) ( return c1.seconds == c2.seconds; ) bool operator != (Counter c1, Counter c2) ( return c1.seconds != c2.seconds; ) bool operator > ( Counter c1, Counter c2) ( return c1.seconds > c2.seconds; ) bool operator< (Counter c1, Counter c2) { return c1.seconds < c2.seconds; } int main() { Counter c1(20); Counter c2(10); bool b1 = c1 == c2; // false bool b2 = c1 >c2; // true std::cout<< b1 << std::endl; std::cout << b2 << std::endl; return 0; }

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

#include class Counter ( public: Counter(int sec) ( seconds = sec; ) void display() ( std::cout<< seconds << " seconds" << std::endl; } Counter& operator += (Counter c2) { seconds += c2.seconds; return *this; } int seconds; }; int main() { Counter c1(20); Counter c2(10); c1 += c2; c1.display(); // 30 seconds return 0; }

Операції інкременту та декременту

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

#include class Counter ( public: Counter(int sec) ( seconds = sec; ) void display() ( std::cout<< seconds << " seconds" << std::endl; } // префиксные операторы Counter& operator++ () { seconds += 5; return *this; } Counter& operator-- () { seconds -= 5; return *this; } // постфиксные операторы Counter operator++ (int) { Counter prev = *this; ++*this; return prev; } Counter operator-- (int) { Counter prev = *this; --*this; return prev; } int seconds; }; int main() { Counter c1(20); Counter c2 = c1++; c2.display(); // 20 seconds c1.display(); // 25 seconds --c1; c1.display(); // 20 seconds return 0; }

Counter& operator++ () ( seconds += 5; return *this; )

У функції можна визначити деяку логіку по інкременту значення. У разі кількість секунд збільшується на 5.

Постфіксні оператори повинні повертати значення об'єкта до інкременту, тобто попередній стан об'єкта. Щоб постфіксна форма відрізнялася від префіксної постфіксної версії, отримують додатковий параметр типу int, який не використовується. Хоча, в принципі, ми можемо його використовувати.

Counter operator++ (int) ( Counter prev = *this; ++*this; return prev; )