Классы

Классы и объекты в MQL5 являются основными концепциями объектно-ориентированного программирования — ООП. Объектно-ориентированное программирование — расширение структурного программирования, в котором основными концепциями являются понятия классов и объектов.

Чтобы понять, для чего же в действительности нужны классы, проведём аналогию с каким-нибудь объектом из повседневной жизни, например, с велосипедом. Велосипед — это объект, который был построен согласно чертежам. Так вот, эти самые чертежи играют роль классов в ООП. Таким образом классы — это некоторые описания, схемы, чертежи по которым создаются объекты. Теперь ясно, что для создания объекта в ООП необходимо сначала составить чертежи, то есть классы. Классы имеют свои функции, которые называются методами класса. Передвижение велосипеда осуществляется за счёт вращения педалей, если рассматривать велосипед с точки зрения ООП, то механизм вращения педалей — это метод класса. Каждый велосипед имеет свой цвет, вес, различные составляющие — всё это свойства. Причём у каждого созданного объекта свойства могут различаться. Имея один класс, можно создать неограниченно количество объектов (велосипедов), каждый из которых будет обладать одинаковым набором методов, при этом можно не задумываться о внутренней реализации механизма вращения педалей, колёс, срабатывания системы торможения, так как всё это уже будет определено в классе. Разобравшись с назначением класса, дадим ему грамотное определение.

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

Классы имеют ряд отличий от структур:

  • в объявлении используется ключевое слово class;
  • по умолчанию все члены класса имеют спецификатор доступа private, если не указано иное. Члены-данные структуры по умолчанию имеют тип доступа public, если не указано иное;
  • объекты классов всегда имеют таблицу виртуальных функций, даже если в классе не объявлено ни одной виртуальной функции. Структуры не могут иметь виртуальных функций;
  • к объектам класса можно применять оператор new, к структурам этот оператор применять нельзя;
  • классы могут наследоваться только от классов, структуры могут наследоваться только от структур.

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

Конструкторы и деструкторы

Конструктор — это специальная функция, которая вызывается автоматически как только создаётся объект структуры или класса и обычно используется для инициализации членов класса. В дальнейшем мы будем говорить только о классах, хотя  все сказанное относится также и к структурам, если не оговорено иное. Имя конструктора обязательно должно совпадать с именем класса. Конструктор не имеет возвращаемого типа (можно указать тип void, хотя и не обязательно).

Определенные члены класса – строки, динамические массивы и объекты, требующие инициализации – в любом случае будут проинициализированы, независимо от наличия конструктора.

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

Конструктор, не имеющий параметров, называется конструктором по умолчанию. Если в классе не объявлен ни один конструктор, то компилятор сам создаст конструктор по умолчанию при компиляции.

//+------------------------------------------------------------------+
//| Класс для работы с датой |
//+------------------------------------------------------------------+
class MyDateClass
{
  private:
   int m_year; // год
   int m_month; // месяц
   int m_day; // день месяца
   int m_hour; // час в сутках
   int m_minute; // минуты
   int m_second; // секунды
  public:
   //--- конструктор по умолчанию
   MyDateClass(void);
   //--- конструктор с параметрами
   MyDateClass(int h,int m,int s);
};

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

//+------------------------------------------------------------------+
//| Конструктор по умолчанию |
//+------------------------------------------------------------------+
MyDateClass::MyDateClass(void)
{
  //---
   MqlDateTime mdt;
   datetime t=TimeCurrent(mdt);
   m_year=mdt.year;
   m_month=mdt.mon;
   m_day=mdt.day;
   m_hour=mdt.hour;
   m_minute=mdt.min;
   m_second=mdt.sec;
   Print(__FUNCTION__);
}

//+------------------------------------------------------------------+
//| Конструктор с параметрами |
//+------------------------------------------------------------------+
MyDateClass::MyDateClass(int h,int m,int s)
{
   MqlDateTime mdt;
   datetime t=TimeCurrent(mdt);
   m_year=mdt.year;
   m_month=mdt.mon;
   m_day=mdt.day;
   m_hour=h;
   m_minute=m;
   m_second=s;
   Print(__FUNCTION__);
}

В конструкторе по умолчанию заполняются все члены класса с помощью функции TimeCurrent(), в конструкторе с параметрами заполняются только значения часа. Остальные члены класса (m_year, m_month и m_day) будут проинициализированы автоматически текущей датой.

Конструктор по умолчанию имеет специальное назначение при инициализации массива объектов своего класса. Конструктор, все параметры которого имеют значения по умолчанию, не является конструктором по умолчанию.

Пример:

//+------------------------------------------------------------------+
//| Класс с конструктором по умолчанию |
//+------------------------------------------------------------------+
class CFoo
{
   datetime m_call_time; // время последнего обращения к объекту
 public:
   //--- конструктор с параметром, имеющем значение по умолчанию, не является конструктором по умолчанию
   CFoo(const datetime t=0){m_call_time=t;};
   //--- конструктор копирования 
   CFoo(const CFoo &foo){m_call_time=foo.m_call_time;};
 
   string ToString(){return(TimeToString(m_call_time,TIME_DATE|TIME_SECONDS));};
};

//+------------------------------------------------------------------+
//| Script program start function |
//+------------------------------------------------------------------+
void OnStart()
{
   // CFoo foo; // такой вариант использовать нельзя - конструктор по умолчанию не задан
   //--- допустимые варианты создания объекта CFoo
   CFoo foo1(TimeCurrent()); // явный вызов параметрического конструктора
   CFoo foo2(); // явный вызов параметрического конструктора с параметром по умолчанию
   CFoo foo3=D'2017.09.09'; // неявный вызов параметрического конструктора
   CFoo foo40(foo1); // явный вызов конструктора копирования
   CFoo foo41=foo1; // неявный вызов конструктора копирования
   CFoo foo5; // явный вызов конструктора по умолчанию (если конструктор по умолчанию отсутствует,
   // то вызывается параметрический конструктор с параметром по умолчанию)

   //--- допустимые варианты получения указателей CFoo
   CFoo *pfoo6=new CFoo(); // динамическое создание объекта и получение указателя на него
   CFoo *pfoo7=new CFoo(TimeCurrent());// ещё один вариант динамического создания объекта
   CFoo *pfoo8=GetPointer(foo1); // теперь pfoo8 указывает на объект foo1
   CFoo *pfoo9=pfoo7; // pfoo9 и pfoo7 указывают на один и тот же объект
   // CFoo foo_array[3]; // такой вариант использовать нельзя - конструктор по умолчанию не задан

   //--- выведем значения m_call_time
   Print("foo1.m_call_time=",foo1.ToString());
   Print("foo2.m_call_time=",foo2.ToString());
   Print("foo3.m_call_time=",foo3.ToString());
   Print("foo4.m_call_time=",foo4.ToString());
   Print("foo5.m_call_time=",foo5.ToString());
   Print("pfoo6.m_call_time=",pfoo6.ToString());
   Print("pfoo7.m_call_time=",pfoo7.ToString());
   Print("pfoo8.m_call_time=",pfoo8.ToString());
   Print("pfoo9.m_call_time=",pfoo9.ToString());

   //--- удалим динамически созданные объекты
   delete pfoo6;
   delete pfoo7;
   //delete pfoo8; // удалять pfoo8 явно не нужно, так как он указывает на автоматически созданный объект foo1
   //delete pfoo9; // удалять pfoo9 явно не нужно, так как он указывает на тот же объект, что и pfoo7
}

Если раскомментировать в этом примере строки

//CFoo foo_array[3]; // такой вариант использовать нельзя - конструктор по умолчанию не задан

или

//CFoo foo_dyn_array[]; // такой вариант использовать нельзя - конструктор по умолчанию не задан

то компилятор выдаст на них ошибку «default constructor is not defined».

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

//+------------------------------------------------------------------+
//| Класс без конструктора по умолчанию |
//+------------------------------------------------------------------+
class CFoo
{
   string m_name;

  public:
   CFoo(string name) { m_name=name;}
};

//+------------------------------------------------------------------+
//| Script program start function |
//+------------------------------------------------------------------+
void OnStart()
{
   //--- при компиляции получим ошибку "default constructor is not defined"
   CFoo badFoo[5];
}

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

Существует специальный синтаксис для инициализации объекта с помощью конструктора. Инициализаторы конструктора (специальные конструкции для инициализации) для членов структуры или класса могут быть заданы в списке инициализации.

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

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

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

//+------------------------------------------------------------------+
//| Класс для хранения фамилии и имени персонажа |
//+------------------------------------------------------------------+
class CPerson
{
   string m_first_name; // имя 
   string m_second_name; // фамилия

  public:
   //--- пустой конструктор по умолчанию
   CPerson() {Print(__FUNCTION__);};
   //--- параметрический конструктор
   CPerson(string full_name);
   //--- конструктор со списком инициализации
   CPerson(string surname,string name): m_second_name(surname), m_first_name(name) {};
   void PrintName(){PrintFormat("Name=%s Surname=%s",m_first_name,m_second_name);};
};

//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
CPerson::CPerson(string full_name)
{
   int pos=StringFind(full_name," ");
   if(pos>=0)
   {
      m_first_name=StringSubstr(full_name,0,pos);
      m_second_name=StringSubstr(full_name,pos+1);
   }
}

//+------------------------------------------------------------------+
//| Script program start function |
//+------------------------------------------------------------------+
void OnStart()
{
   //--- получим ошибку "default constructor is not defined"
   CPerson people[5];
   CPerson Tom="Tom Sawyer"; // Том Сойер
   CPerson Huck("Huckleberry","Finn"); // Гекльберри Финн
   CPerson *Pooh = new CPerson("Winnie","Pooh"); // Винни Пух

   //--- выведем значения
   Tom.PrintName();
   Huck.PrintName();
   Pooh.PrintName();
 
   //--- удалим динамически созданный объект
   delete Pooh;
}

В нашем случае класс CPerson имеет три конструктора:

  • явный конструктор по умолчанию, который позволяет создавать массив объектов данного класса;
  • конструктор с одним параметром, который принимает в качестве параметра полное имя и разделяет его на имя и фамилию по найденному пробелу;
  • конструктор с двумя параметрами, который содержит список инициализации. Инициализаторы – m_second_name(surname) и m_first_name(name).

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

член_класса (список выражений)

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

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

//+------------------------------------------------------------------+
//| Базовый класс |
//+------------------------------------------------------------------+
class CFoo
{
   string m_name;

  public:
   //--- конструктор со списком инициализации
   CFoo(string name) : m_name(name) { Print(m_name);}
};

//+------------------------------------------------------------------+
//| Потомок класса CFoo |
//+------------------------------------------------------------------+
class CBar : CFoo
{
   CFoo m_member; // член класса является объектом предка

  public:
   //--- конструктор по умолчанию в списке инициализации вызывает конструктор предка
   CBar(): m_member(_Symbol), CFoo("CBAR") {Print(__FUNCTION__);}
};

//+------------------------------------------------------------------+
//| Script program start function |
//+------------------------------------------------------------------+
void OnStart()
{
   CBar bar;
}

В приведенном примере при создании объекта bar будет вызван конструктор по умолчанию CBar(), в котором сначала вызывается конструктор для предка CFoo, а затем конструктор для члена класса m_member.

Деструктор — это специальная функция, которая вызывается автоматически при уничтожении объекта класса. Имя деструктора записывается как имя класса с тильдой (~). Строки, динамические массивы и объекты, требующие деинициализации, в любом случае будут деинициализированы независимо от наличия деструктора. При наличии деструктора, эти действия будут произведены после вызова деструктора.

Деструкторы всегда являются виртуальными, независимо от того, объявлены они с ключевым слово virtual или нет.