Печатать книгуПечатать книгу

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

Алгоритмические конструкции

Как вам уже известно из курса информатики, любой алгоритм может быть записан с использованием трех базовых алгоритмических конструкций: следование, цикл и ветвление (пример 3.1).

Команды, составляющие алгоритмическую конструкцию следование, выполняются последовательно, друг за другом, в том порядке, в котором они записаны. Команды цикла и ветвления управляют порядком выполнения других команд в программе и относятся к командам управления (управляющим конструкциями).

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

Алгоритмическая конструкция повторение (цикл) представляет собой последовательность действий, выполняемых многократно. Саму последовательность называют телом цикла. Оператор цикла — это команда, реализующая алгоритмическую конструкцию повторения на языке программирования.

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

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

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

Пример 3.1 Блок-схемы алгоритмических конструкций.

Следование

Ветвление

Цикл

  • Цикл с параметром (значение параметра изменяется от 1 до N):

  • Цикл с предусловием:

  • Цикл с постусловием:

Кроме блок-схем, для графического представления алгоритмов используют структурограммы (NS-диаграммы, диаграммы Насси — Шнейдермана).

Примеры структурограмм

Команда ветвления:

Команда цикла с предусловием:

Сайт: Профильное обучение
Курс: Информатика. 11 класс (Повышенный уровень)
Книга: § 3. Перегрузка операций
Напечатано:: Гость
Дата: Четверг, 2 Май 2024, 18:10

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;

}

 

3.2. Логический тип данных

Для стандартных типов данных таких как int или double ввод и вывод реализован через стандартные операторы >> и <<. Эти же операторы перегружены для ввода-вывода строк. Для пользовательских типов данных, имеющих множество полей, также можно перегрузить операторы ввода-вывода. Перегрузка операторов << и >> намного упрощает процесс вывода объекта на экран и получение пользовательского ввода с записью данных в объект класса.

Рассмотрим перегрузку оператора <<. Оператор является бинарным оператором, поэтому его перегрузка будет аналогична перегрузке оператора «+», рассмотренной в примерах 3.5 и 3.6. Левым операндом у оператора << является объект cout, а правым — объект класса, который нужно вывести. Переопределяемый оператор должен возвращать значение типа ostream, объектом которого является cout. Параметры, описываемой функции, должны быть ссылками (примеры 3.7 и 3.8). Также ссылкой должен быть и возвращаемый результат, поскольку ostream запрещает свое копирование.

Перегрузка оператора ввода происходит аналогично. Отличие в том, что cin является объектом типа istream (примеры 3.9 и 3.10). Ссылка на объект класса, являющийся вторым параметром, не может быть константой, поскольку объект изменяется при вводе.

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

Пример 3.7. Перегрузка оператора вывода << для класса Vect, определенного в примере 3.5.

Объявление:

friend ostream & operator << 

(ostream &, const Vect &);

Описание:

ostream & operator << 

(ostream &out, const Vect &p)

{

  out << p.<< " " << p.<< endl;

}

Пример 3.8. Перегрузка оператора вывода << для класса Matrix, определенного в примере 3.6.

Объявление:

friend ostream & operator << 

(ostream &, const Matrix  &);

Описание:

ostream & operator << 

(ostream & out, const Matrix &d)

{

  for (int i = 0; i < d.n; i++){

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

      out << setw(4) << d.data[i][j];

    out << endl;

  }

  out << endl;

Пример 3.9. Перегрузка оператора вывода >> для класса Vect, определенного в примере 3.5.

Объявление:

friend istream & operator >> 

(istream &, Vect &);

Описание:

span style="font-size: medium;">istream & operator >> 
(istream &in, Vect &p)

{

    in >> p.>> p.y;

}

Пример 3.10. Перегрузка оператора вывода >> для класса Matrix, определенного в примере 3.6.

Объявление:

friend istream & operator >> 

(istream &, Matrix &);

Описание:

istream & operator >> 

(istream & in, Matrix &d)

{

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

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

      in >> d.data[i][j];

}

Вопросы к параграфу

1. Для чего используется перегрузка операций?

2. Какие операции можно перегрузить в С++?

3. Какие операции нельзя перегрузить в С++?

4. Какими способами можно определить функцию для перегрузки операторов?

 

Упражнения

 

1. Для программы из примера 3.5 выполните следующее:

    1. Дополните описание класса перегрузкой операторов ввода и вывода, приведенное в примерах 3.7 и 3.9.
    2. Измените функции для перегрузки операторов сравнения так, чтобы они были дружественными.
    3. Как будет реагировать программа, если использовать знак «<» для сравнения длин векторов? Почему?
    4. Перегрузите операции «<» и «!=». 
    5. Перегрузите оператор «*» еще раз, считая, что он используется и для нахождения скалярного произведения двух векторов [1].


[1] Скалярным произведением двух векторов   и   является число .

2. Для программы из примера 3.6 выполните следующее:

    1. Реализуйте многофайловый проект, разделите объявление и определение класса. Создайте файлы Matrix.h и Matrix.cpp.
    2. Дополните описание класса перегрузкой операторов ввода и вывода, приведенное в примерах 3.8 и 3.10.
    3. Измените функции для перегрузки операторов так, чтобы они были дружественными.
    4. Перегрузите операцию «==», которая будет возвращать true, в том случае, если матрицы имеют одинаковые размеры и false в противном случае.
    5. * Перегрузите оператор «*» еще раз, считая, что он используется и для нахождения произведения матриц[1]

      [1] О том, как умножать матрицы, можно почитать здесь: https://www.webmath.ru/poleznoe/formules_6_6.php.

    6. Продемонстрируйте работу методов и операторов описанного класса.

3. Измените описание класса Parallelepiped (задание 4, §2), добавив перегрузку операторов сравнения параллелепипедов по объему.

4. Добавьте в класс Rect (задание 6, §2) перегрузку операций «+» для получения наименьшего прямоугольника, содержащего два заданных прямоугольника, и «*» — для получения прямоугольника, являющегося общей частью (пересечением) двух прямоугольников.

5. Создать класс Drob. Конструктор принимает два числа: числитель и знаменатель дроби. Перегрузить операции: сложение, умножение, деление, вычитание. Реализовать методы: сокращение (использовать private функцию для нахождения НОД по алгоритму Евклида), перевод обыкновенной дроби в десятичную. Продемонстрировать работу всех функций класса и перегруженных операций на примерах.

6. Создать базовый класс Progressii и ее наследников — арифметическую и геометрическую прогрессии. Реализовать методы: n-ый член прогрессии, проверка прогрессии на убывание. Перегрузить операции: «+» (первый операнд — переменная типа прогрессии, второй — число (количество элементов)) для вычисления суммы арифметической прогрессии; «*» — аналогично для вычисления суммы геометрической прогрессии. Продемонстрировать работу всех функций класса и перегруженных операций на примерах.

7. Описать класс Polinom для работы с многочленами. Полями класса является степень многочлена и массив коэффициентов. Перегрузить операции «+», «-», «*», а также ввод и вывод многочлена. Реализовать метод вычисления значения многочлена для заданного значения переменной[1]. *Используя метод двоичного деления, найти корень многочлена на заданном промежутке (корень должен быть на этом промежутке единственным).

[1] Рекомендуется использовать для вычисления значения многочлена схему Горнера — https://intuit.ru/studies/professional_retraining/941/courses/67/lecture/1966?page=2

8*. Описать класс BigNumber для работы с «большими» числами (числа, которые не помещаются в стандартные типы). Перегрузите операции «+», «-», «*», сравнения, ввода и вывода.

Пример описания некоторых членов класса:

///Класс "большое число", описывает способ хранения большого числа и сложение

class BigNumber

{

private:

    vector < int > cifr;

///нормализация числа моделирует перенос в следующий разряд, если цифра > 10

    void norm();

public:

///Конструктор по умолчанию ("пустое" число)

    BigNumber() {};

///Конструктор, конвертирует строку в большое число

    BigNumber(string str)

 

    BigNumber operator + (const BigNumber &);

    friend ostream & operator << (ostream &, const BigNumber &);

};

BigNumber::BigNumber(string str)

{

   ///Записываем цифры с конца строки

   for (int i = str.size() - 1; i >= 0; i --)

     cifr.push_back(str[i] - '0');

}

 

///Оператор +, выполняет сложение больших чисел

BigNumber BigNumber::operator + (const BigNumber &num)

{

    BigNumber res;

    int r1 = min(cifr.size(), num.cifr.size());

    res.cifr.resize(r1);

/// сложение цифр

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

        res.cifr[i] = cifr[i] + num.cifr[i];

///если в одном из чисел цифры закончились

    for (int i = r1; i < cifr.size(); i++)

        res.cifr.push_back(cifr[i]);

    for (int i = r1; i < num.cifr.size(); i++)

        res.cifr.push_back(num.cifr[i]);

    res.norm();

    return res;

}

 

void BigNumber::norm()

{

  if (cifr.size() > 1) {

    for (int i = 0; i < cifr.size()-1; i++) {

       cifr[+ 1] += cifr[i] / 10;

       cifr[i] =  cifr[i] % 10;

    }

    if (cifr[cifr.size() - 1] >= 10) {

      int t = cifr[cifr.size() - 1] / 10;

      cifr[cifr.size() - 1] = cifr[cifr.size() - 1] % 10;

      cifr.resize(cifr.size() + 1);

      cifr[cifr.size()-1] =  t;

    }

  }

}

 

///Перегрузка оператора << для вывода

ostream & operator << (ostream &cout_bn,  const BigNumber &num)

{

    for (int i = num.cifr.size() - 1; i >= 0; i--)

        cout_bn << num.cifr[i];

}

 

Пример использования:

int main() {

    BigNumber n1("9999999999999999");

    BigNumber n2("1");

    cout << n1 << endl;

    cout << n2 << endl;

    BigNumber n3 = n1 + n2;

    cout << n3 << endl;

    return 0;