§ 3. Перегрузка операций

3.1. Перегрузка арифметических операций

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

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

С перегруженными операторами вы уже работали. В классе string перегружены операторы сравнения и оператор «+». Для сравнения строк используются те же знаки, что и для сравнения чисел, однако сравнение строк выполняется не так, как сравниваются числа. Сложение строк также имеет смысл, отличный от сложения чисел.

Большинство операций в С++ можно перезагрузить (пример 3.1)[1].

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

Синтаксис перегрузки операций:

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

где @ — знак перегружаемой операции,

тип — тип возвращаемого значения.

Перегружать можно только операции, для которых хотя бы один аргумент представляет тип данных, определенный пользователем (например, класс). Функция для перегрузки операции должна быть определена либо как функция-член класса (пример 3.3), либо как внешняя функция, но дружественная классу (пример 3.4). Один и тот же оператор можно перегрузить несколько раз.

Если бинарная[2] операция перегружается с использованием метода класса, то первым операндом она получает переменную класса, которая неявно передается через указатель this на объект. Вторым операндом является аргумент функции. Таким образом, бинарная операция, перегружаемая методом класса, имеет фактически один параметр (правый операнд), а левый передается неявно через указатель this.

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

Пример 3.5. Вектор на плоскости задается парой координат. Описать класс Vect для представления вектора на плоскости. Реализовать метод, вычисляющий длину вектора, перегрузить операторы «+» (сложение двух векторов), «*» (умножения вектора на число), «==» (сравнение векторов), «>» (сравнение векторов по длине).

Этапы выполнения задания

1. Полями класса будут координаты (x, y) вектора на плоскости.

2. Для вычисления длинны вектора в методе класса dl используется формула вычисления длины отрезка.

3. Реализовать перегрузку операций.

    1. Для перегрузки операций «==» и «>» нужно описать соответствующие функции. Функции должны иметь тип bool. Функции реализовать как методы класса.
    2. Для перегрузки операции «*» нужно описать соответствующую функцию. Функция должна иметь тип Vect. Функцию реализовать как метод класса.
    3. Для перегрузки операции «+» реализовать дружественную функцию, возвращающую тип Vect.

Любой перегруженный оператор можно вызвать с использованием функциональной формы записи (функции-операции):

Vect v3 = operator + (v1, v2);

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

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

Пример 3.6. Описать класс Matrix. Перегрузить операции сложения, вычитания и умножения матрицы на число.

Этапы выполнения задания

1.  Полями класса будут целые числа m, n — количество строк и столбцов в матрице и двумерный вектор для хранения самой матрицы.

2. Реализовать метод rand_data, который будет заполнять матрицу случайными числами.

3. Перегрузка операций аналогична перегрузке операций, которые реализованы в примере 3.4.

    1. При перегрузке операции умножения на число умножим на это число каждый элемент матрицы.
    2. При перегрузке операции «+» просуммируем элементы с одинаковыми номерами строки и столбца.
    3. При перегрузке операции «-» воспользуемся тем, что
      AB = A + B * (–1).

[1] Более подробно о перегрузке операторов можно почитать, например здесь: https://habr.com/ru/post/489666/

[2] Операция является бинарной, если для ее выполнения необходимы два операнда. Например, операция сложения (a + b). Операция с одним операндом является унарной. Например, операция смены знака у числа (-a).

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

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

<операнд1> <знакОперации> <операнд2>

аналогична вызову функции <знакОперации>(<операнд1>,<операнд2>).

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

Пример 3.1. Операции, которые можно перегрузить в С++:

+   -   *  /    %  ^   & |     ~  !    =  <  >  

+=     -=     *=      /=      %=    ^=     &=     |=

<<     >>    >>=    <<=   ==     !=     <=      >=

&&    ||       ++      --       []       ()

Пример 3.2. Операции, которые нельзя перегрузить в С++:

  • оператор выбора члена класса «.»;
  • оператор разыменования указателя на член класса «.*»;
  • тернарный оператор «? :»;
  • операция указания области видимости «::»;
  • операция вычисления размера в байтах sizeof.

Дружественная функция — это функция, не являющаяся членом класса, но имеющая доступ к его закрытым членам. Ею может быть как обычная функция, так и метод другого класса. Для объявления дружественной функции используется ключевое слово friend перед объявлением функции, которая станет дружественной классу. Функция может быть объявлена как в разделе public, так и разделе private.

Пример 3.3. Перегрузка операции «+» с применением метода класса:

class Vect

{

  ...

public:

  Vect operator + (const Vect &);

  ...

};

Пример 3.4. Перегрузка операции «+» с применением внешней функции, дружественной классу:

class Vect

{

  ...

public:

  friend Vect operator + (Vect &, Vect &);

  ...

};

Пример 3.5. Программа.

#include <iostream>

#include <math.h>

#include <windows.h>

 

using namespace std;

 

class Vect ///класс, описывающий вектор на плоскости

{

    double x,y;

  public:

    ///конструкторы класса

    Vect() {= 0; y = 0;}

    Vect (double z1, double z2):

             x(z1), y(z2) {}

 

    ///возврат координат

    double get_x(){return x;}

    double get_y(){return y;}

    ///длина вектора

    double dl();

    /// оператор сравнения двух векторов

    bool operator == (Vect &);

    /// оператор сравнения двух векторов по длине

    bool operator > (Vect &);

    /// оператор умножения вектора на число

    Vect operator *(int);

    ///оператор сложения двух векторов

    friend Vect operator +(Vect &, Vect &);

};

 

double Vect::dl()

{

  return sqrt(* x + y * y);

}

 

bool Vect::operator == (Vect &p)

{

  return  (== p.&& y == p.y);

}

 

bool Vect::operator > (Vect &p)

{

  return (dl() > p.dl()) ? true : false;

}

 

Vect Vect::operator *(int k)

{

  return Vect(* k, y * k);

}

 

Vect operator +(Vect &p, Vect &q)

{

  return Vect(p.+ q.x, p.+ q.y);

}

 

int main()

{

  SetConsoleCP(1251);

  SetConsoleOutputCP(1251);

  double a,b,c,d;

  cout << "координаты:" << endl;

  cin >> a >> b >> c >> d;

  Vect v1(a, b), v2(c, d);

  cout << "длина 1-го = " ;

  cout << v1.dl() << endl;

  cout << "длина 2-го = " ;

  cout << v2.dl() << endl;

  if (v1 == v2)

    cout << "векторы совпадают" << endl;

  else

    cout << "векторы не совпадают" << endl;

  if (v1 > v2)

    cout << "длина 1-го больше" << endl;

  else

    cout << "длина 1-го не больше" << endl;

  Vect v3 = v1 + v2;

  cout << "координаты вектора v3: " ;

  cout << v3.get_x() << " ";

  cout << v3.get_y() << endl;

  cout << "длина вектора v3 = ";

  cout << v3.dl() << endl;

  Vect v4 = v1 * 5;

  cout << "координаты вектора v4: ";

  cout << v4.get_x() << " ";

  cout << v4.get_y() << endl;

  cout << "длина вектора v4 = " ;

  cout << v4.dl() << endl;

  return 0;

}

Результат работы:

Роб Мюррей, в своей книге «C++ Strategies and Tactics» рекомендовал перегружать унарные операции и операции, совмещенные с присваиванием (+=, *= и др.), как члены класса. Бинарные операции он рекомендовал перегружать с использованием дружественных функций.

Пример 3.6. Описание класса Matrix.

class Matrix

{

    int n, m;

    vector < vector <int> > data;

  public:

    Matrix();

    Matrix(int n_, int m_);

    void rand_data ();

    Matrix operator + (Matrix &);

    Matrix operator * (int);

    Matrix operator - (Matrix &);

};

 

Matrix::Matrix()

{

    n = 1;  m = 1;

    data.resize(n);

    data[0].resize(m, 0);

}

 

Matrix::Matrix(int n_, int m_)

{

    n = n_;

    m = m_;

    data.resize(n);

    for (int i = 0; i < n; i++)

        data[i].resize(m, 0);

}

 

void Matrix::rand_data()

{

    for (int i = 0; i < n; i++)

      for (int j = 0; j < m; j++)

        data[i][j] = rand() % 100;

}

 

Matrix Matrix::operator + (Matrix &d)

{

    Matrix t(n, m);

    for (int i = 0; i < n; i++)

      for (int j = 0; j < m; j++)

         t.data[i][j] = data[i][j] +

                       d.data[i][j];

    return t;

}

 

Matrix Matrix::operator * (int k)

{

    Matrix t(n, m);

    for (int i = 0; i < n; i++)

      for (int j = 0; j < m; j++)

        t.data[i][j] = data[i][j] * k;

    return t;

}

 

Matrix Matrix::operator - (Matrix &d)

{

    Matrix t(n, m);

    t = d * (-1);

    t = *this + t;

    return t;

}