Наследование

При определении класса разработчик может унаследовать его от другого класса, воплощая тем самым концепцию наследования. Для этого после имени класса ставится двоеточие, необязательный модификатор прав доступа (одно из ключевых слов public, protected, private), и имя родительского класса. Например, вот как мы можем описать класс Rectangle, производный от Shape:

class Rectangle : public Shape
{
};

Модификаторы доступа в заголовке класса управляют "видимостью" членов родительского класса, включенных в класс наследника:

  • public — все унаследованные члены сохраняют свои права и ограничения;
  • protected — меняет права унаследованных public-членов на protected;
  • private — делает все унаследованные члены закрытыми (private).

Модификатор public используется в подавляющем большинстве определений. Два остальных варианта имеет смысл применять только в исключительных случаях, потому что они нарушают базовый принцип наследования: объекты производного класса должны обычно "являться" ("is a") полноценными представителями родительского семейства, а если мы "урезаем" их права, они утрачивают часть своих характеристик. Структуры также можно наследовать друг от друга по аналогичной схеме. Наследовать классы от структур или структуры от классов запрещено.

В отличие от C++, MQL5 не поддерживает множественное наследование. Родителей у класса может быть не больше одного.

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

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

class Rectangle : public Shape
{
public:
   Rectangle(int pxint pycolor back) :
      Shape(pxpyback)
   {
      Print(__FUNCSIG__" ", &this);
   }
};

В данном случае список инициализации превратился в одиночный вызов конструктора Shape. В списке инициализации нельзя напрямую устанавливать переменные базового класса, потому что за их инициализацию отвечает базовый конструктор. Однако, при необходимости, мы могли бы изменить protected-поля базового класса из тела конструктора Rectangle (инструкции в теле функции выполняются уже после того, как отработал базовый конструктор в списке инициализации).

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

class Rectangle : public Shape
{
protected:
   int dxdy// размеры (ширина, высота)
   
public:
   Rectangle(int pxint pyint sxint sycolor back) :
      Shape(pxpyback), dx(sx), dy(sy)
   {
   }
};

Важно отметить, что в объектах Rectangle в неявном виде присутствует унаследованная от Shape функция toString (впрочем, как и draw, но та пока пуста). Поэтому корректен следующий код:

void OnStart()
{
   Rectangle r(1002005075clrBlue);
   Print(r.toString());
};

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

Конструктор по умолчанию (без параметров) отсутствует в классе Rectangle. Это означает, что пользователь класса не может создавать объекты-прямоугольники простым способом, без аргументов:

   Rectangle r// 'Rectangle' - wrong parameters count

Компилятор выдаст ошибку "Неверное количество аргументов".

Создадим еще один дочерний класс — Ellipse. Пока он ничем не будет отличаться от Rectangle, кроме имени. Позднее мы внесем в них различия.

class Ellipse : public Shape
{
protected:
   int dxdy// размеры (большой и малый радиусы)
public:
   Ellipse(int pxint pyint rxint rycolor back) :
      Shape(pxpyback), dx(rx), dy(ry)
   {
      Print(__FUNCSIG__" ", &this);
   }
};

Поскольку количество классов увеличивается, было бы здорово выводить имя класса в методе toString. В разделе Специальные операторы sizeof и typename мы описывали оператор typename. Попробуем его использовать.

Напомним, что typename ожидает один параметр, для которого и возвращается название типа. Например, если мы создадим пару объектов s и r, соответственно классов Shape и Rectangle, то можем узнать их тип следующим образом:

void OnStart()
{
   Shape s;
   Rectangle r(1002007550clrRed);
   Print(typename(s), " "typename(r));      // Shape Rectangle
}

Но нам нужно каким-то образом получить это имя внутри класса. Для этой цели добавим в параметрический конструктор Shape строковый параметр и будем сохранять его в новом строковом поле type (обратите внимание на секцию protected и модификатор const: это поле скрыто от внешнего мира и не может редактироваться после создания объекта):

class Shape
{
protected:
   ...
   const string type;
   
public:
   Shape(int pxint pycolor backstring t) :
      coordinates(pxpy),
      backgroundColor(back),
      type(t)
   {
      Print(__FUNCSIG__" ", &this);
   }
   ...
};

В конструкторах производных классов станем заполнять этот параметр базового конструктора с помощью typename(this):

class Rectangle : public Shape
{
   ...
public:
   Rectangle(int pxint pyint sxint sycolor back) :
      Shape(pxpybacktypename(this)), dx(sx), dy(sy)
   {
      Print(__FUNCSIG__" ", &this);
   }
};

Теперь мы можем усовершенствовать метод toString с использованием поля type.

class Shape
{
   ...
public:
   string toString() const
   {
      return type + " " + (string)coordinates.x + " " + (string)coordinates.y;
   }
};

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

void OnStart()
{
   Shape s;
   // настройка объекта цепочкой вызовов через 'this'
   s.setColor(clrWhite).moveX(80).moveY(-50);
   Rectangle r(1002007550clrBlue);
   Ellipse e(200300100150clrRed);
   Print(s.toString());
   Print(r.toString());
   Print(e.toString());
}

В результате получим примерно следующие записи в журнале (пустые строки добавлены намеренно, чтобы разделить вывод от разных объектов):

Pair::Pair(int,int) 0 0
Shape::Shape() 1048576
   
Pair::Pair(int,int) 100 200
Shape::Shape(int,int,color,string) 2097152
Rectangle::Rectangle(int,int,int,int,color) 2097152
   
Pair::Pair(int,int) 200 300
Shape::Shape(int,int,color,string) 3145728
Ellipse::Ellipse(int,int,int,int,color) 3145728
   
Shape 80 -50
Rectangle 100 200
Ellipse 200 300
   
Ellipse::~Ellipse() 3145728
Shape::~Shape() 3145728
Pair::~Pair() 200 300
   
Rectangle::~Rectangle() 2097152
Shape::~Shape() 2097152
Pair::~Pair() 100 200
   
Shape::~Shape() 1048576
Pair::~Pair() 80 -50

По логу понятно, в какой последовательности вызываются конструкторы и деструкторы.

Для каждого объекта сначала создаются описанные в нем поля-объекты (если они есть), затем вызывается базовый конструктор и все конструкторы производных классов по цепочке наследования. Если в производном классе встречаются собственные (добавленные) поля неких объектных типов, конструкторы для них будут вызваны непосредственно перед конструктором этого производного класса. Когда объектных полей несколько, они создаются в той последовательности, в которой описаны в классе.

Деструкторы вызываются строго в обратном порядке.

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

class Rectangle : public Shape
{
   ...
   Rectangle(const Rectangle &other) :
      Shape(other), dx(other.dx), dy(other.dy)
   {
   }
   ...
};

А область применения слегка расширяется. Объект производного класса можно использовать для копирования в базовый класс (потому что производный содержит все данные для базового). Правда при этом, разумеется, игнорируются поля, добавленные в производном классе.

void OnStart()
{
   Rectangle r(1002007550clrBlue);
   Shape s2(r);         // ok: копируем производный в базовый
   
   Shape s;
   Rectangle r4(s);     // error: no one of the overloads can be applied 
                        // требуется явная перегрузка конструктора
}

Для копирования в обратном направлении нужно предоставить в базовом классе вариант конструктора со ссылкой на производный (что, в принципе, противоречит принципам ООП), иначе возникнет ошибка компиляции "нет подходящей перегрузки функции" ("no one of the overloads can be applied to the function call").

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

void OnStart()
{
   Rectangle r(1002005075clrBlue);
   Ellispe e(1002005075clrGreen);
   r.draw();
   e.draw();
};

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