Skip to content

Newbranch #21

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
328 changes: 328 additions & 0 deletions content/lab10.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,328 @@
Перегрузка операторов в С++
###########################

:date: 2017-11-07

.. default-role:: code
.. contents:: Содержание

Введение
========
С++ позволяет организовать перегрузку операций. Механизм перегрузки операций позволяет обеспечить более традиционную и удобную запись действий над объектами. Для перегрузки встроенных операторов используется ключевое слова operator.
Синтаксически перегрузка операций осуществляется следующим образом:

.. code-block:: c

тип operator @ (список_параметров-операндов)
{
// ... тело функции ...
}

где @ — знак перегружаемой операции (-, +, * и т. д.),
тип — тип возвращаемого значения.
Операторы бывают бинарные (например, a+b) и унарные (например, i++).

Рассмотрим следующий код:


.. code-block:: c

#include <iostream>
class Point2D {
int x, y;

public:
Point2D() : x(0), y(0) {}
Point2D( int _x, int _y ) : x(_x), y(_y) {}
Point2D operator+(const Point2D & t) { return Point2D(x+t.x, y+t.y); }
Point2D operator=(const Point2D & t) { x = t.x; y = t.y; return *this; }
void show () { std::cout << x << ", " << y << std::endl; }
};

int main() {
Point2D a(1, 2), b(10, 10), c;
a.show();
b.show();
c = a+b;
c.show();
c = a+b+c;
c.show();
c = b = a;
c.show();
b.show ();
return 0;
}


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

.. code-block:: c

1, 2
10, 10
11, 12
22, 24
1, 2
1, 2


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


.. code-block:: c

Point2D operator+(const Point2D & t) { return Point2D(x+t.x, y+t.y); };


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

.. code-block:: c

a + b эквивалентно вызову a.operator+(b)


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

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

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

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

.. code-block:: c

а = b = с = d;


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

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

.. code-block:: c

Point2D & operator++ () { x++; y++; return *this; }
Point2D operator++ (int d) { Point2D p(x,y); ++(*this); return p; }


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


Два способа перегрузки операторов
=================================

Функция-оператор может быть другом класса (friend), а не только его функцией. Поскольку функции-друзья не являются функциями класса, они не могут иметь неявный аргумент this. Поэтому при использовании дружественной функции-оператора оба операнда пе­редаются функции при перегрузке бинарных операторов, а при перегрузке унарных операторов передается один операнд.
Следующие операторы не могут использовать перегрузку с помощью функций-друзей: =, (), [], и ->. Остальные операторы могут быть перегружены как с помощью функций-классов, так с помощью функций-друзей.
В качестве примера ниже рассматрим мо­дифицированную версия класса Point2D, в которой оператор + перегружен с помощью дружественной функции:

.. code-block:: c

#include <iostream>
class Point2D {
int x, y;

friend Point2D operator+(const Point2D & a, const Point2D & b);

public:
Point2D() : x(0), y(0) {}
Point2D( int _x, int _y ) : x(_x), y(_y) {}
Point2D operator=(const Point2D & t) { x = t.x; y = t.y; return *this; }
void show () { std::cout << x << ", " << y << std::endl; }
};

Point2D operator+(const Point2D & a, const Point2D & b) { return Point2D(a.x+b.x, a.y+b.y); }

int main() {
Point2D a(1, 2), b(10, 10), c;
a.show();
b.show();
c = a+b;
c.show();
c = a+b+c;
c.show();
c = b = a;
c.show();
b.show ();
return 0;
}

В данном случае оба операнда передаются функции operator+(). Левый опе­ранд передается в переменной a, а правый — в переменной b.

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

.. code-block:: c

X = X + 2; // будет работать


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

.. code-block:: c

X = 2 + X; // не будет работать

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

.. code-block:: c

X operator+(X & x, int i);
X operator+(int i, X & x);

В зависимости от порядка операндов в выражении будет вызываться подходящий оператор.


Правила перегрузки операций
===========================

Язык C++ не допускает определения для операций нового лексического символа, кроме уже определенных в языке. Например, нельзя определить в качестве знака операции @.
Не допускается перегрузка операций для встроенных типов данных. Нельзя, например, переопределить операцию сложения целых чисел:


.. code-block:: c

int operator +(int i, int j);

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


Следующие операторы могут быть переопределены:

+--------+--------+--------+---------+---------+--------+--------+--------+--------+--------+
| ``+`` | ``*`` | ``/`` | ``%`` | ``^`` | ``&`` | ``\`` | ``|`` | ``~`` | ``!`` |
+--------+--------+--------+---------+---------+--------+--------+--------+--------+--------+
| ``=`` | ``<`` | ``>`` | ``+=`` | ``-=`` | ``*=`` | ``/=`` | ``%=`` | ``^=`` | ``&=`` |
+--------+--------+--------+---------+---------+--------+--------+--------+--------+--------+
| ``|=`` | ``<<`` | ``>>`` | ``>>=`` | ``<<=`` | ``==`` | ``!=`` | ``<=`` | ``>=`` | ``&&`` |
+--------+--------+--------+---------+---------+--------+--------+--------+--------+--------+
| ``||`` | ``++`` | ``--`` | ``[]`` | ``()`` | new | delete | | | |
+--------+--------+--------+---------+---------+--------+--------+--------+--------+--------+


Класс Fraction
==============

Рассмотрим класс Fraction, реализующий базовый функционал над дробями:

.. code-block:: c

#include <iostream>
#include <stdexcept>
#include <cstdlib>
#include <cmath>

class Fraction {
private:
int numerator;
int denominator;

void simplify() {
if (denominator < 0) {
numerator *= -1;
denominator *= -1;
}
if ( abs(numerator) < 2 ) return;
int gcd = getGCD( abs(numerator), denominator );
numerator /= gcd;
denominator /= gcd;
}
public:
Fraction( int n, int d ) : numerator(n), denominator(d) {
simplify();
}

Fraction() : numerator(0), denominator(1) {}
Fraction( const Fraction &other ) : numerator( other.getNumerator() ), denominator( other.getDenominator() ) {}

Fraction( int value ) : numerator(value), denominator(1) {}

int getNumerator() const { return numerator; }
int getDenominator() const { return denominator; }

double getValue() const {
return static_cast<double>(getNumerator()) / static_cast<double>(getDenominator());
}

int compareTo( const Fraction &other ) const {
return getNumerator() * other.getDenominator() - getDenominator() * other.getNumerator();
}

int getGCD( int a, int b ) {
while( a != b ) {
if (a > b) a -= b; else b -= a;
}
return a;
}

Fraction operator-() {
return Fraction(-getNumerator(), getDenominator());
}

Fraction operator+(const Fraction &a) {
int commonDenominator = a.getDenominator() * getDenominator();
int commonNumerator = a.getNumerator() * getDenominator() + getNumerator() * a.getDenominator();
return Fraction(commonNumerator, commonDenominator);
}

Fraction operator*(const Fraction &a) {
return Fraction(getNumerator() * a.getNumerator(), getDenominator() * a.getDenominator());
}

Fraction operator/(const Fraction &a) {
return Fraction(getNumerator() * a.getDenominator(), getDenominator() * a.getNumerator());
}

bool operator==(const Fraction &a) { return compareTo(a) == 0; }
};

std::ostream &operator<<(std::ostream &stream, const Fraction& a) {
return stream << a.getNumerator() << "/" << a.getDenominator();
}

Fraction power(const Fraction &fraction, int power) {
return (power < 0) ?
Fraction((int)pow(fraction.getDenominator(), -power), (int)pow(fraction.getNumerator(), -power)) :
Fraction((int)pow(fraction.getNumerator(), power), (int)pow(fraction.getDenominator(), power));
}

int main(int argc, char **argv) {
Fraction a(-4, 7), b(1, 3), c(0, 4);
std::cout << c << " " << a * c << std::endl;
}

Скопируйте и запустите код, приведенный выше, и убедитесь, что он работает корректно.


Упражнение №1
-------------

Реализуйте следующие операторы для класса Fraction:

.. code-block:: c

bool operator<(const Fraction &a)
bool operator>(const Fraction &a)
bool operator<=(const Fraction &a)
bool operator>=(const Fraction &a)


Упражнение №2
-------------

Реализуйте оператор

.. code-block:: c

Fraction operator-(Fraction &a)

не обращаясь явно к полям numerator и denominator


Упражнение №3
-------------

Реализуйте основные арифметические операторы (+,-,*,/) для Fraction и int.