5.3. Перегрузка операторов - StriderAJR/StudentCpp GitHub Wiki

Перегрузка операторов

Понятие оператора рассматривалось еще давно, в самых первых разделах данного вики. Вкратце, оператор - это некая команда, обозначаемая какими-то спецсимволами.

Например, есть оператор +, -, *, /, >>, <<, =, ==, +=, !, &, &&, |, || и т.д. Это все операторы.

Оператор - это в первую очередь "действие", он что-то делает. А все куски кода, которые что-то делают называются функциями. Функции, привязанные к классам, принято называть методами.

На самом деле все именно так: оператор - это ф-ция с особым зарезервированным именем. Имя это выглядит так: operator+, operator-, operator*, operator>>, operator&& и т.д.

Поэтому когда вы пытаетесь сложить два числа типа int

     a + b

то на самом деле компилятором это преобразуется в вызов:

operator+(a, b);

где ф-ция имеет следующий прототип:

int operator(int, int);

т.е. принимает 2 параметра типа данных int и возвращает результат типа данных int. Так вот. В компилятор встроено определенное кол-во операторов и их перегрузок для разных типов данных.

   int a1, a2, a3;
   double b1, b2, b3;
    
   a3 = a1 + a2;
   b3 = b1 + b2;

что компилятором преобразуется в:

   a3 = operator+(a1, a2);
   b3 = operator+(b1, b2);

на самом деле в этих строчках вызываются 2 разные ф-ции operator+. Их сигнатуры выглядят по-разному.

    int operator+(int, int);
    double operator+(double, double);

И таких перегрузок достаточно много. Для всех простых типов данных. Это встроено в компилятор языка С++. А теперь встает вопрос о пользовательских типах данных, т.е. классах. Очень часто нам хотелось бы иметь возможность совершать некоторые операции над целыми объектами. Складывать, вычитать, перемножать, выполнять индексацию (как с массивами) и т.п.

Возьмем тему геометрия. У нас есть несколько сущностей:

  • точка, которая характеризуется 2-мя координатами
  • отрезок, который состоит из двух точек

Сумма двух точек даст нам отрезок. Но разница двух точек даст нам точку, координаты которой являются дельтой координат первой и второй точки.

#include <iostream>
using namespace std;

namespace OperatorOverloading
{
   class PointDummy
   {
      int x, y;
   public:
      PointDummy() : x(0), y(0) {}
      PointDummy(int x, int y) : x(x), y(y){}

      int getX() const { return x; }
      int getY() const { return y; }
      void setX(int x) { this->x = x; }
      void setY(int y) { this->y = y; }
   };

   class LineDummy
   {
      PointDummy p1, p2;
   public:
      LineDummy(const PointDummy& p1, const PointDummy& p2)
      {
         this->p1 = PointDummy(p1.getX(), p1.getY());
         this->p2 = PointDummy(p2.getX(), p2.getY());
      }
   };


   LineDummy& operator+(const PointDummy& p1, const PointDummy& p2)
   {
    
      return *(new LineDummy(p1, p2));
   }

   PointDummy& operator-(const PointDummy& p1, const PointDummy& p2)
   {
      return *(new PointDummy(p1.getX()-p2.getX(), p1.getY()-p2.getY()));
   }

Немножко пояснений, прежде чем вернемся к теме перегрузки операторов.

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

А теперь из интересностей.

 LineDummy(const PointDummy& p1, const PointDummy& p2);

ни у кого уже не должно быть вопросов, почему тут стоит тип данных ссылка и модификатор const. Если у кого-то вопросы все-таки возникли, то идет в раздел "Передача параметров по ссылке и по значению".

    PointDummy p1, p2;

опять же всем уже должно быть понятно, но все-таки еще раз поясню: когда поля создаются таким образом, то при создании объекта LineDummy, сразу же в памяти создаются 2 объекта PointDummy. Из-за того, что PointDummy имеет конструктор

   PointDummy(int x, int y);

конструктор по умолчанию (который без параметров), автоматически создан не будет. Но тут

    PointDummy p1, p2;

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

    PointDummy();

сами, у нас была бы ошибка "no default constructor exists for class PointDummy". Так что пришлось создать и в нем координаты просто занулить. Поэтому на самом деле если задуматься, вот это все в сумме

     PointDummy p1, p2;
     LineDummy(const PointDummy& p1, const PointDummy& p2)
     {
        this->p1 = PointDummy(p1.getX(), p1.getY());
        this->p2 = PointDummy(p2.getX(), p2.getY());
     }

неэффективно. Мы сначала создали 2 пустые координаты с нулевыми значения, а потом все равно в нормальном конструкторе переинициализировали. Конечно, это только если нам не придется создавать много нулевых точек по умолчанию и где-то пользоваться ими. Тогда конструктор по умолчанию у PointDummy имеет смысл.

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

Вот эта штука

    this->p1 = PointDummy(p1.getX(), p1.getY());

а конкретно вызовы

p1.getX()

и

p1.getY()

будут работать только если методы getX и getY помечены как константные.

    PointDummy getFirstPoint() const { return p1; }

Если модификатора const не будет у этих методов, то

     PointDummy(p1.getX(), p1.getY());

будет подчеркивать p1 и говорить, что

"the object has type qualifiers that are not compatible with the member function".

Поясню, потому что новичку это может быть не очевидно. Параметры p1 и p2 в конструкторе

    LineDummy(const PointDummy& p1, const PointDummy& p2)

помечены как константные, т.е. они ни в коем случае не должны изменить в процессе работы метода. А внутри метода мы вызываем методы getX и getY. А вдруг внутри этих методов точки как-то изменяются? А такого допустить никак нельзя.

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

Таким образом, если вы делаете параметры в методе константными, то можно пользоваться только константными методами этих классов. Или объект может измениться где-то в иерархии вызовов методов.

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

     PointDummy getFirstPoint() const { return p1; }
     PointDummy getSecondPoint() const { return p2; }

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

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

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

А теперь возвращаемся к перегрузке операторов. Нам нужно иметь возможность складывать и вычитать две точки.

Сложение двух точек -> отрезок

Вычитание двух точен -> точка

 LineDummy& operator+(const PointDummy& p1, const PointDummy& p2)
  {
     return *(new LineDummy(p1, p2));
  }

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

      return LineDummy(p1, p2);

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

Если тип возвращаемого значения ф-ции изменить с ссылки просто на LineDummy этой проблемы не будет, потому что создастся копия, но зато объект будет создаваться дважды. Чтобы избежать и того, и другого просто возьмем управление памятью на себя.

      LineDummy* newLine = new LineDummy(p1, p2);
      return *newLine;

или еще короче можно записать так:

return *(new LineDummy(p1, p2));

Круто? Круто. Теперь мы можем писать что-нибудь типа:

    PointDummy p1(5, 5);
    PointDummy p2(1, 6);
    LineDummy line1 = p1 + p2;
    PointDummy p3 = p2 - p1;

Последние 2 строчки будут преобразовываться вот в такие:

   LineDummy line1 = operator+(p1, p2);
   PointDummy p3 = operator-(p2, p1);

Обратите внимание, что при вызове оператора, выражение, которое стоит слева от оператора, будет первым параметром, а выражение справа от оператора - вторым параметром.

Поэтому у operator+ первый параметр p1, а второй - p2. А у operator- первый - p2, второй - p1.

Поздравляю мы только что перезагрузили два оператор + и -.

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

Вроде бы, все просто. Но вот тут всплывет один неприятный момент. Начнем писать перегрузку.

   void operator*(LineDummy& line, int number)
   {
      line.p2.setX(line.p2.getX() * number);
      line.p2.setY(line.p2.getY() * number);
   }

И вот неприятность: у нас нет доступа до поля p2 объекта line. Логично: оно приватное, а ф-ция оператора не принадлежит классу. Поэтому вполне ожидаемо, что нас ударили по рукам.

Ну и что делать?

Ответом стала разработка механизма дружественных ф-ций.

Дружественная функция класса - это функция, которой дали доступ до приватных и защищенных полей класса.Дружественная ф-ция должна быть объявлена в классе.

Т.е. нам достаточно в класс LineDummy добавить строчку

    friend void operator*(LineDummy& line, int number);

Это прототип ф-ции, который записан с модификатором friend - друг. Ага, скажи "друг" и тебе откроют. ;)

Чтобы не перемешивать код, продублируем класс LineDummy с дружественной ф-цией.

   class LineDummy2
   {
      PointDummy p1, p2;
   public:
      LineDummy2(const PointDummy& p1, const PointDummy& p2)
      {
         this->p1 = PointDummy(p1.getX(), p1.getY());
         this->p2 = PointDummy(p2.getX(), p2.getY());
      }

      friend void operator*(LineDummy2& line, int number);
   };

   void operator*(LineDummy2& line, int number)
   {
      line.p2.setX(line.p2.getX() * number);
      line.p2.setY(line.p2.getY() * number);
   }

Ну а теперь, собственно, наша дружественная ф-ция с перегрузкой оператора. Обратите внимание, что класс используем уже LineDummy2.

  class LineDummy2
  {
     PointDummy p1, p2;
  public:
     LineDummy2(const PointDummy& p1, const PointDummy& p2)
     {
        this->p1 = PointDummy(p1.getX(), p1.getY());
        this->p2 = PointDummy(p2.getX(), p2.getY());
     }

     friend void operator*(LineDummy2& line, int number);
  };

Вот и славненько. Научились перегружать операторы даже если нам требуются приватные поля класса.

  void operator*(LineDummy2& line, int number)
  {
     line.p2.setX(line.p2.getX() * number);
     line.p2.setY(line.p2.getY() * number);
  }

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

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

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

 

   // Point.h
   class Line;
   class Point
   {
      int x, y;
   public:
      Point();
      Point(int, int);

      int getX() const;
      int getY() const;
      void setX(int);
      void setY(int);

      Line& operator+(const Point&);
      Point& operator-(const Point&);
   };
   // end of Point.h

   // Line.h
   class Point;
   class Line
   {
      Point p1, p2;
   public:
      Line();
      Line(const Point&, const Point&);
      void operator*(int);

      Point getFirstPoint() const;
      Point getSecondPoint() const;
      void setFirstPoint(Point);
      void setSecondPoint(Point);
   };
   // end of Line.h

   // Point.cpp
   Point::Point() : x(0), y(0) {}
   Point::Point(int x, int y) : x(x), y(y) {}
   int Point::getX() const { return x; }
   int Point::getY() const { return y; }
   void Point::setX(int x) { this->x = x; }
   void Point::setY(int y) { this->y = y; }
   Line& Point::operator+(const Point& p2)
   {
      return *(new Line(*this, p2));
   }
   Point& Point::operator-(const Point& p2)
   {
      return *(new Point(this->getX() - p2.getX(), this->getY() - p2.getY()));
   }
   //end of Point.cpp

   // Line.cpp
   Line::Line() : p1(Point()), p2(Point()) {}
   Line::Line(const Point& p1, const Point& p2)
   {
      this->p1 = Point(p1.getX(), p1.getY());
      this->p2 = Point(p2.getX(), p2.getY());
   }
   void Line::operator*(int number)
   {
      this->p2.setX(this->p2.getX() * number);
      this->p2.setY(this->p2.getY() * number);
   }
   Point Line::getFirstPoint() const { return p1; }
   Point Line::getSecondPoint() const { return p2; }
   void Line::setFirstPoint(Point p1) { this->p1 = p1; }
   void Line::setSecondPoint(Point p2) { this->p2 = p2; }
   // end of Line.cpp

  
   ostream& operator<<(ostream& out, const Point& point)
   {
      out << "(" << point.getX() << "; " << point.getY() << ")";
      return out;
   }


   istream& operator>>(istream& in, Point& point)
   {
      int x, y;
      in >> x >> y;
      point.setX(x);
      point.setY(y);

      return in;
   }

   ostream& operator<<(ostream& out, const Line& line)
   {
      Point p1 = line.getFirstPoint();
      Point p2 = line.getSecondPoint();
      out << "p1" << p1 << " p2" << p2;
      
      return out;
   }

   istream& operator>>(istream& in, Line& line)
   {
      Point p1, p2;
      in >> p1 >> p2;
      line.setFirstPoint(p1);
      line.setSecondPoint(p2);

      return in;
   }

   void test()
   {
      Point p1, p2;
      Line line;

      cout << "Введите 2 координаты для p1: ";
      cin >> p1;
      cout << "Введите 2 координаты для p2: ";
      cin >> p2;
      cout << "А теперь введите введите 4 координаты для точек p1 и p2 отрезка: ";
      cin >> line;

      cout << endl << "Спасибо ^_^" << endl << 
         "Вы ввели: " << endl <<
         "p1 = " << p1 << endl << 
         "p2 = " << p2 << endl <<
         "line = " << line << endl;

      cout << endl << ":3" << endl;
   }

   int main()
   {
      PointDummy p1(5, 5);
      PointDummy p2(1, 6);
      LineDummy l = p1 + p2;
      PointDummy p3 = p2 - p1;

      Point p4(1, 1);
      Point p5(2, 2);
      Line line = p4 + p5;
      Point p6 = p5 - p4;
      line * 3;

      test();

      return 0;
   }
}

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

Ну а почему бы и нет? Если оператор это обычная ф-ция, а метод класса - это ф-ция, которая принадлежит классу. Все что не запрещено - разрешено.

Ну и погнали тогда! Полностью перепишем наши классы.

    class Point;  
    class Line; 

Это "голые" объявления класса. Пришлось так сделать, потому что у нас имеется перекрестное использование классов внутри друг друга. А С++ читает код сверху вниз. Когда он дойдет до точки, он еще не знает об отрезко. Если поместить отрезок выше, в нем есть упоминание точки. Из этой ситуации можно выйти только если "пообещать" компилятору, что он найдет эти классы где-то ниже в коде.

По-хорошему вообще нужно было разделить объявления и реализацию классов на заголовочный файл (*.h) и файл реализации (*.cpp). Но я ленивец, поэтому это уж как-нибудь сами.

  // Point.h
  class Line;
  class Point
  {
     int x, y;
  public:
     Point();
     Point(int, int);

     int getX() const;
     int getY() const;
     void setX(int);
     void setY(int);

     Line& operator+(const Point&);
     Point& operator-(const Point&);
  };
  // end of Point.h

А вот сейчас о важном.

Внимательные могли заметить, что прототип ф-ции оператора изменился. Имена LineDummy и PointDummy заменены на Line и Point для удоства сравнения.

При перегрузке дружественной ф-цией было так:

  Line& operator+(const Point& p1, const Point& p2);

А при перегрузке методом класса стало так:

   Line& operator+(const Point& p2);

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

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

Рассматорим развернутую запись

    p1 + p2;

для двух случаев: дружественная ф-ция и метод класса.

Дружественная ф-ция распишется как:

  operator+(p1, p2);

метод класса же:

   p1.operator(p2);

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

<для_продвинутых> На самом деле, указатель this, который всегда доступен внутри методов класса - это передаваемый неявно (т.е. без вашего участия) параметр. Т.е. любой метод класса на самом деле имеет на 1 параметр больше, чем вы записываете. Еще как один параметр передается именно адрес вызываемого объекта.

   class Foo
   {
    public:
       void method1();
       void method2(int num);   
   };

method1 на самом деле имеет не ноль параметров, а один - Foo* this method2 на самом деле имеет не один параметр, а два - int num и Foo* this.

Именно поэтому указатель this доступен внутри методов класса - он неявно передается при вызове метода.

</для_продвинутых>

Поэтому на просто нет необходимости перечислять для перегрузки оператора методом класса оба параметра. Один параметр туда передастся и без наших действий.

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

    class Foo
    {
      int num;
    public:
       void operator+(int anotherNum) 
      {
         num += anotherNum; 
      }
       void operator++()
       {
          num++;
       }
    };

Для сравнения та же логика организованная через дружественные ф-ции будет выглядеть так:

    class Foo
    {
       int num;
       friend void operator+(Foo&, int);
       friend void operator++(Foo& foo);
    };
    
    void operator+(Foo& foo, int number)
    {
       foo.num += number;
    }
   
    void operator++(Foo& foo)
    {
       foo.num++;
    }

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

  // Line.h
  class Point;
  class Line
  {
     Point p1, p2;
  public:
     Line();
     Line(const Point&, const Point&);
     void operator*(int);

     Point getFirstPoint() const;
     Point getSecondPoint() const;
     void setFirstPoint(Point);
     void setSecondPoint(Point);
  };
  // end of Line.h

  // Point.cpp
  Point::Point() : x(0), y(0) {}
  Point::Point(int x, int y) : x(x), y(y) {}
  int Point::getX() const { return x; }
  int Point::getY() const { return y; }
  void Point::setX(int x) { this->x = x; }
  void Point::setY(int y) { this->y = y; }
  Line& Point::operator+(const Point& p2)
  {
     return *(new Line(*this, p2));
  }
  Point& Point::operator-(const Point& p2)
  {
     return *(new Point(this->getX() - p2.getX(), this->getY() - p2.getY()));
  }
  //end of Point.cpp

  // Line.cpp
  Line::Line() : p1(Point()), p2(Point()) {}
  Line::Line(const Point& p1, const Point& p2)
  {
     this->p1 = Point(p1.getX(), p1.getY());
     this->p2 = Point(p2.getX(), p2.getY());
  }
  void Line::operator*(int number)
  {
     this->p2.setX(this->p2.getX() * number);
     this->p2.setY(this->p2.getY() * number);
  }
  Point Line::getFirstPoint() const { return p1; }
  Point Line::getSecondPoint() const { return p2; }
  void Line::setFirstPoint(Point p1) { this->p1 = p1; }
  void Line::setSecondPoint(Point p2) { this->p2 = p2; }
  // end of Line.cpp

Ну вот как-то так. Теперь мы снова можем делать вот такие вещи:

    Point p4(1, 1);
    Point p5(2, 2);
    Line line = p4 + p5;
    Point p6 = p5 - p4;
    line * 3;

Неплохо.

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

И мы действительно можем перегрузить оператор >> и << для работы с потоками и нашими классами.

И вот тут есть один нюанс. Как вы помните при перегрузке методом класса, вызывающий объект, которому и должен принадлежать метод перегрузки стоит СЛЕВА. В записи

    cout << p1;

Вызов оператора будет расписываться:

    cout.operator<<(p1);

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

И вот тут из-за особенностей С++ без дружественных ф-ций не обойтись.

Перегрузка операторов << и >> для потоков возможно ТОЛЬКО с помощью дружественных функций.

Такая перегрузка будет принимать 2 параметра: ссылка на поток и объект, который вы хотите писать/читать в потоке.

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

  ostream& operator<<(ostream& out, const Point& point)
  {
     out << "(" << point.getX() << "; " << point.getY() << ")";
     return out;
  }

Обратите внимание:

ostream& operator<<(ostream& out, const Point point)

Явно не указано, какой именно поток используется. ostream - это абстрактный output stream и не понятно принадлежит ли он консоли, файлу или чему-то другому. Этим потоком может оказаться любой из потоков. Очень удобно.

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

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

  istream& operator>>(istream& in, Point& point)
  {
     int x, y;
     in >> x >> y;
     point.setX(x);
     point.setY(y);

     return in;
  }

  ostream& operator<<(ostream& out, const Line& line)
  {
     Point p1 = line.getFirstPoint();
     Point p2 = line.getSecondPoint();
     out << "p1" << p1 << " p2" << p2;
     
     return out;
  }

<для_продвинутых> На самом деле при перегрзуке операторов << и >> не обязательно, чтобы был тип возаращаемого значения. Вполне можно было бы обойтись и void

      void operator<<(ostream& out, const Point& point);
      void operator>>(istream& in, Point& point);

Но если хочется, чтобы работала цепочечная запись

     out << "p1" << p1 << " p2" << p2;

то перегрузка оператора << для точки должна была обязательно возвращать ostream&

       ostream& operator<<(ostream& out, const Point& point);

Иначе получится, что после первого вывода p1

 out << "p1" << p1;

нам вернется void и следующий оператор << ни к чему не может быть применем.

Alt  Texr

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

То же самое действительно и для оператора >>.

</для_продвинутых>

  istream& operator>>(istream& in, Line& line)
  {
     Point p1, p2;
     in >> p1 >> p2;
     line.setFirstPoint(p1);
     line.setSecondPoint(p2);

     return in;
  }

  void test()
  {
     Point p1, p2;
     Line line;

     cout << "Введите 2 координаты для p1: ";
     cin >> p1;
     cout << "Введите 2 координаты для p2: ";
     cin >> p2;
     cout << "А теперь введите введите 4 координаты для точек p1 и p2 отрезка: ";
     cin >> line;

     cout << endl << "Спасибо ^_^" << endl << 
        "Вы ввели: " << endl <<
        "p1 = " << p1 << endl << 
        "p2 = " << p2 << endl <<
        "line = " << line << endl;

     cout << endl << ":3" << endl;
  }

  int main()
  {
     PointDummy p1(5, 5);
     PointDummy p2(1, 6);
     LineDummy l = p1 + p2;
     PointDummy p3 = p2 - p1;

     Point p4(1, 1);
     Point p5(2, 2);
     Line line = p4 + p5;
     Point p6 = p5 - p4;
     line * 3;

     test();

     return 0;
  }
}
⚠️ **GitHub.com Fallback** ⚠️