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

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

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

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

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

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

Рассмотрим наследование на примере геометрических фигур. Для описания всего многообразия простых фигур (круг, треугольник, прямоугольник, квадрат и так далее) лучше всего создать базовый класс (АТД), который является прародителем всех производных классов.

Создадим базовый класс CShape, в котором есть только самые общие члены, описывающие фигуру. Эти члены описывают свойства, присущие любой фигуре – тип фигуры и координаты основной точки привязки.

Пример:

//--- Базовый класс Фигура
class CShape
  {
protected:
   int       m_type;                   // тип фигуры
   int       m_xpos;                   // X - координата точки привязки
   int       m_ypos;                   // Y - координата точки привязки
public:
             CShape(){m_type=0; m_xpos=0; m_ypos=0;} // конструктор
   void      SetXPos(int x){m_xpos=x;} // установим X
   void      SetYPos(int y){m_ypos=y;} // установим Y
  };

Далее создадим от базового класса производные классы, в которых добавим необходимые поля, уточняющие каждый конкретный класс. Для фигуры Circle (круг) необходимо добавить член, который содержит значение радиуса. Фигура Quadrate (квадрат) характеризуется значением стороны квадрата. Поэтому производные классы, унаследованнные от базового класса CShape, будут объявлены таким образом:

//--- производный класс Круг
class CCircle : public CShape           // после двоеточия указывается базовый класс,
  {                                     // от которого производится наследование
private:
   int             m_radius;            // радиус круга
 
public:
                   CCircle(){m_type=1;} // конструктор, тип равен 1
  };

Для квадрата объявление класса выглядит аналогично:

//--- производный класс Квадрат
class CSquare : public CShape          // после двоеточия указывается базовый класс,
  {                                    // от которого производится наследование 
private:
   int            m_square_side;       // сторона квадрата
 
public:
                  CSquare(){m_type=2;} // конструктор, тип равен 2
  };

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

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

Синтаксис создания производного класса от уже существующего выглядит следующим образом:

class имя_класса : 
          (public | protected | privateopt  имя_базового_класса
  {                                    
    объявления членов
  };

Одним из аспектов производного класса является видимость (открытость) его членов-наследников. Ключевые слова public, protected и private используются для указания того, насколько члены базового класса будут доступны из производного. Использование в заголовке производного класса ключевого класса public, следующего за двоеточием, означает, что защищенные и открытые (protected и public) члены базового класса CShape должны наследоваться как защищенные и открытые члены производного класса CCircle.

Закрытые члены базового класса недоступны производному классу. Открытое наследование означает также, что производные классы (CCircle и CSquare) являются CShape. То есть, квадрат (CSquare) является фигурой (CShape), но фигура не обязательно должна быть квадратом.

Производный класс является модификацией базового класса; он наследует защищенные и открытые члены базового класса. Не могут только наследоваться конструкторы и деструкторы базового класса. Часто в производный класс добавляются новые члены в дополнение к членам базового класса.

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

При защищенном наследовании открытые и защищенные члены базового класса становятся защищенными членами производного класса. При закрытом наследовании открытые и защищенные члены базового класса становятся закрытыми членами производного класса.

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

Необходимо понимать, что тип наследования (public, protected или private) никак не влияет на способы доступа к членам базовых классов в иерархии наследования из потомка (наследника). При любом типе наследования из производных классов будут доступны только члены базового класса, объявленные со спецификаторами доступа  public и protected. Рассмотрим вышесказанное на примере:

#property copyright "Copyright 2000-2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Класс-пример с несколькими типами доступа                        |
//+------------------------------------------------------------------+
class CBaseClass
  {
private:             //--- закрытый член недоступен из потомков
   int               m_member;
protected:           //--- защищенный метод доступен из базового класса и его потомков
   int               Member(){return(m_member);}
public:              //--- конструктор класса доступен всем
                     CBaseClass(){m_member=5;return;};
private:             //--- закрытый метод для присвоения значения члену m_member
   void              Member(int value) { m_member=value;};
 
  };
//+------------------------------------------------------------------+
//| Производный класс с ошибками                                     |
//+------------------------------------------------------------------+
class CDerived: public CBaseClass // public наследование можно не указывать, оно по умолчанию
  {
public:
   void Func() // определим в потомке функцию с обращениями к членам базового класса 
     {
      //--- попытка модификации закрытого члена базового класса
      m_member=0;        // ошибка, закрытый член базового класса никому не доступен
      Member(0);         // ошибка, закрытый метод базового класса не доступен в потомках
      //--- чтение члена базового класса
      Print(m_member);   // ошибка, закрытый член базового класса никому не доступен
      Print(Member());   // нет ошибки, защищенный метод доступен из базового класса и его потомков
     }
  };

В приведенном примере класс CBaseClass имеет только один публичный метод – конструктор. Конструкторы вызываются автоматически при создании объекта класса. Поэтому извне никак нельзя обратиться ни к закрытому члену m_member, ни к защищенному методу Member(). Но при этом при открытом (public) наследовании метод Member() базового класса будет доступен из потомков.

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

//+------------------------------------------------------------------+
//| Класс-пример с несколькими типами доступа                        |
//+------------------------------------------------------------------+
class CBaseMathClass
  {
private:             //--- закрытый член недоступен из потомков
   double            m_Pi;
public:              //--- получение и установка значения для m_Pi
   void              SetPI(double v){m_Pi=v;return;};
   double            GetPI(){return m_Pi;};
public:              // конструктор класса доступен всем
                     CBaseMathClass() {SetPI(3.14);  PrintFormat("%s",__FUNCTION__);};
  };
//+------------------------------------------------------------------+
//| Производный класс, в котором нельзя уже изменить m_Pi            |
//+------------------------------------------------------------------+
class CProtectedChildClass: protected CBaseMathClass // защищенное наследование
  {
private:
   double            m_radius;
public:              //--- открытые методы в потомке
   void              SetRadius(double r){m_radius=r; return;};
   double            GetCircleLength(){return GetPI()*m_radius;};
  };
//+------------------------------------------------------------------+
//| Функция запуска скрипта                                          |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- при создании потомка констурктор базового класса вызовется автоматически
   CProtectedChildClass pt;
//--- укажем радиус
   pt.SetRadius(10);
   PrintFormat("Length=%G",pt.GetCircleLength());
//--- если раскомментировать строку ниже, то получим ошибку на этапе компиляции,так как метод SetPi() стал защищенным
// pt.SetPI(3); 
 
//--- а теперь объявим переменную базового класса и попробуем задать константу Pi равной 10
   CBaseMathClass bc;
   bc.SetPI(10);
//--- посмотрим что получилось
   PrintFormat("bc.GetPI()=%G",bc.GetPI());
  }

В данном пример показано, что методы SetPI() и GetPi() в базовом классе CBaseMathClass являются открытыми и доступны для вызова из любого места программы. Но в то же время для его потомка CProtectedChildClass вызовы этих методов можно делать только из методов самого класса CProtectedChildClass или его потомков.

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

В MQL5 нет множественного наследования.

Смотри также

Структуры и классы