Структуры, классы и интерфейсы

Структуры

Структура является набором элементов произвольного типа (кроме типа void). Таким образом, структура объединяет логически связанные данные разных типов.

Объявление структуры

Структурный тип данных определяется следующим описанием:

struct имя_структуры 
  {
   описания_элементов
  };

Имя структуры нельзя использовать в качестве идентификатора (имени переменной или функции). Следует иметь ввиду, что в MQL5 элементы структуры следуют непосредственно друг за другом без выравнивания. В языке C++ такое указание делается компилятору с помощью инструкции

#pragma pack(1)

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

Пример:

struct trade_settings
  {
   uchar  slippage;     // значение допустимого проскальзывания -размер 1 байт
   char   reserved1;    // 1 байт пропуска
   short  reserved2;    // 2 байта пропуска
   int    reserved4;    // еще 4 байта пропуска. Обеспечили выравнивание на границу 8 байт
   double take;         // значения цены фиксации прибыли
   double stop;         // значение цены защитного стопа
  };

Такое описание выровненных структур необходимо только для передачи в импортированные dll-функции.

Внимание: данный пример иллюстрирует неправильно спроектированные данные. Лучше было бы сначала объявить данные take и stop большего размера типа double, а затем объявить член slippage типа uchar. В этом случае внутреннее представление данных будет всегда одинаково независимо от значения, указанного в #pragma pack().

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

Простые структуры

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

Копирование простых структур допускается только в двух случаях:

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

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

      //--- копировать простые структуры разных типов запрещено
      my_tick1=last_tick;               // компилятор здесь выдаст ошибку
     
      //--- приводить структуры разного типа друг к другу тоже нельзя
      my_tick1=(CustomMqlTick)last_tick;// компилятор здесь выдаст ошибку

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

      CustomMqlTick my_tick1,my_tick2;
      //--- а вот так копировать объекты одной и той же структуры CustomMqlTick можно
      my_tick2=my_tick1;
     
      //--- создадим массив из объектов простой структуры CustomMqlTick и запишем в неё значения
      CustomMqlTick arr[2];
      arr[0]=my_tick1;
      arr[1]=my_tick2;

В качестве проверки вызывается функция ArrayPrint() для вывода в журнал значения массива arr[].

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- создадим такую же структуру, как встроенная MqlTick
   struct CustomMqlTick
     {
      datetime          time;          // Время последнего обновления цен
      double            bid;           // Текущая цена Bid
      double            ask;           // Текущая цена Ask
      double            last;          // Текущая цена последней сделки (Last)
      ulong             volume;        // Объем для текущей цены Last
      long              time_msc;      // Время последнего обновления цен в миллисекундах
      uint              flags;         // Флаги тиков     
     };
   //--- получим значения последнего тика
   MqlTick last_tick;
   CustomMqlTick my_tick1,my_tick2;
//--- попытаемся скопировать данные из MqlTick в CustomMqlTick
   if(SymbolInfoTick(Symbol(),last_tick))
     {
      //--- копировать неродственные простые структуры запрещено
      //1. my_tick1=last_tick;               // компилятор здесь выдаст ошибку
     
      //--- приводить неродственные структуры друг к другу тоже нельзя
      //2. my_tick1=(CustomMqlTick)last_tick;// компилятор здесь выдаст ошибку
     
      //--- поэтому копируем члены структуры поэлементно     
      my_tick1.time=last_tick.time;
      my_tick1.bid=last_tick.bid;
      my_tick1.ask=last_tick.ask;
      my_tick1.volume=last_tick.volume;
      my_tick1.time_msc=last_tick.time_msc;
      my_tick1.flags=last_tick.flags;
     
      //--- а вот так копировать объекты одной и той же структуры CustomMqlTick можно
      my_tick2=my_tick1;
     
      //--- создадим массив из объектов простой структуры CustomMqlTick и запишем в неё значения
      CustomMqlTick arr[2];
      arr[0]=my_tick1;
      arr[1]=my_tick2;
      ArrayPrint(arr);
//--- пример вывода значений массива, содержащего объекты типа CustomMqlTick
      /*
                       [time]   [bid]   [ask]   [last] [volume]    [time_msc] [flags]
      [0] 2017.05.29 15:04:37 1.11854 1.11863 +0.00000  1450000 1496070277157       2
      [1] 2017.05.29 15:04:37 1.11854 1.11863 +0.00000  1450000 1496070277157       2           
      */
     }
   else
      Print("SymbolInfoTick() failed, error = ",GetLastError());
  }

Второй пример показывает возможности копирования простых структур по линии наследования. Пусть у нас есть базовая стурктура Animal, от которой порождены наследованием структуры Cat и Dog. Мы можем копировать между собой объекты Animal и Cat, Animal и Dog, но не можем копировать между собой Cat и Dog – хотя оба являются потомками структуры Animal.

//--- структура для описания собак
struct Dog: Animal
  {
   bool              hunting;       // охотничья порода
  };
//--- структура для описания кошек
struct Cat: Animal
  {
   bool              home;          // домашняя порода
  };
//--- создадим объекты дочерних структур
   Dog dog;
   Cat cat;
//--- можно копировать от предка к потомку (Animal ==> Dog)
   dog=some_animal;
   dog.swim=true;    // собаки умеют плавать
//--- копировать объекты дочерних структур нельзя (Dog != Cat)
   cat=dog;        // компилятор здесь выдаст ошибку

Полный код примера:

//--- базовая структура для описания животных
struct Animal
  {
   int               head;          // кол-во голов
   int               legs;          // кол-во ног
   int               wings;         // кол-во крыльев
   bool              tail;          // наличие хвоста
   bool              fly;           // летает
   bool              swim;          // плавает  
   bool              run;           // бегает
  };
//--- структура для описания собак
struct Dog: Animal
  {
   bool              hunting;       // охотничья порода
  };
//--- структура для описания кошек
struct Cat: Animal
  {
   bool              home;          // домашняя порода
  };
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- создадим объект базового типа Animal и опишем его
   Animal some_animal;
   some_animal.head=1;
   some_animal.legs=4;
   some_animal.wings=0;
   some_animal.tail=true;
   some_animal.fly=false;
   some_animal.swim=false;
   some_animal.run=true;
//--- создадим объекты дочерних типов
   Dog dog;
   Cat cat;
//--- можно копировать от предка к потомку (Animal ==> Dog)
   dog=some_animal;
   dog.swim=true;    // собаки умеют плавать
//--- копировать объекты дочерних структур нельзя (Dog != Cat)
   //cat=dog;        // компилятор здесь выдаст ошибку
//--- поэтому можно копировать только поэлементно
   cat.head=dog.head;
   cat.legs=dog.legs;
   cat.wings=dog.wings;
   cat.tail=dog.tail;
   cat.fly=dog.fly;
   cat.swim=false;   // кошки не умеют плавать
//--- копировать значения от потомка к предку можно 
   Animal elephant;
   elephant=cat;
   elephant.run=false;// слоны не умеют бегать
   elephant.swim=true;// слоны плавают
//--- создадим массив
   Animal animals[4];
   animals[0]=some_animal;
   animals[1]=dog;  
   animals[2]=cat;
   animals[3]=elephant;
//--- выведем на печать
   ArrayPrint(animals);
//--- результат выполнения
/*
       [head] [legs] [wings] [tail] [fly] [swim] [run]
   [0]      1      4       0   true false  false  true
   [1]      1      4       0   true false   true  true
   [2]      1      4       0   true false  false false
   [3]      1      4       0   true false   true false
*/  
  }

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

Доступ к членам структуры

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

Пример:

struct trade_settings
  {
   double take;         // значения цены фиксации прибыли
   double stop;         // значение цены защитного стопа
   uchar  slippage;     // значение допустимого проскальзывания
  };
//--- создали и проинициализировали переменную типа trade_settings
trade_settings my_set={0.0,0.0,5};  
if (input_TP>0) my_set.take=input_TP;

pack для выравнивания полей структур и классов #

Cпециальный атрибут pack позволяет задать выравнивание полей структуры или класса.

 pack([n])

где n  – одно из следующих значений 1,2,4,8 или 16. Может отсутствовать.

Пример:

   struct pack(sizeof(long)) MyStruct
     {
      // члены структуры будут выровнены на границу 8 байт
     };
или
   struct MyStruct pack(sizeof(long))
     {
      // члены структуры будут выровнены на границу 8 байт
     };

По умолчанию для структур используется pack(1). Это означает, что в памяти члены структуры располагаются друг за другом и размер структуры равен сумме размеров её членов.

Пример:

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- простая структура без выравнивания
   struct Simple_Structure
     {
      char              c; // sizeof(char)=1
      short             s; // sizeof(short)=2
      int               i; // sizeof(int)=4
      double            d; // sizeof(double)=8
     };
   //--- объявим экземпляр простой структуры   
   Simple_Structure s;  
//--- выведем размер каждого члена структуры   
   Print("sizeof(s.c)=",sizeof(s.c));
   Print("sizeof(s.s)=",sizeof(s.s));
   Print("sizeof(s.i)=",sizeof(s.i));
   Print("sizeof(s.d)=",sizeof(s.d));
//--- убедимся, что размер POD-структуры равен сумме размеров её членов
   Print("sizeof(simple_structure)=",sizeof(simple_structure));
/*
   Результат:
   sizeof(s.c)=1
   sizeof(s.s)=2
   sizeof(s.i)=4
   sizeof(s.d)=8
   sizeof(simple_structure)=15 
*/    
  }

Выравнивание полей структуры может понадобится при обмене данными со сторонними библиотеками (*.DLL), в которых такое выравнивание применяется.

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

//--- простая структура без выравнивания
   struct Simple_Structure pack() // размер не указан, будет выравнивание на границу в 1 байт
     {
      char              c; // sizeof(char)=1
      short             s; // sizeof(short)=2
      int               i; // sizeof(int)=4
      double            d; // sizeof(double)=8
     };
//--- объявим экземпляр простой структуры   
   Simple_Structure s;

Поля структуры будут располагаться в памяти друг за другом, согласно порядку объявления и размеру типа. Размер структуры равен 15, смещение к полям структуры в массивах будет неопределённым.

simple_structure_alignment

Объявим теперь эту же структуру с выравниванием в 4 байта и запустим код.

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- простая структура с выравниванием в 4 байта
   struct Simple_Structure pack(4)
     {
      char              c; // sizeof(char)=1
      short             s; // sizeof(short)=2
      int               i; // sizeof(int)=4
      double            d; // sizeof(double)=8
     };
   //--- объявим экземпляр простой структуры   
   Simple_Structure s;  
//--- выведем размер каждого члена структуры   
   Print("sizeof(s.c)=",sizeof(s.c));
   Print("sizeof(s.s)=",sizeof(s.s));
   Print("sizeof(s.i)=",sizeof(s.i));
   Print("sizeof(s.d)=",sizeof(s.d));
//--- убедимся, что размер POD-структуры теперь не равен сумме размеров её членов
   Print("sizeof(simple_structure)=",sizeof(simple_structure));
/*
   Результат:
   sizeof(s.c)=1
   sizeof(s.s)=2
   sizeof(s.i)=4
   sizeof(s.d)=8
   sizeof(simple_structure)=16 // размер структуры изменился
*/    
  }

Размер структуры изменился таким образом, чтобы все члены размером 4 байта или больше имели смещение от начала структуры кратное 4 байтам. Члены меньшего размера будут выравниваться на границу своего размера (например 2 для short). Вот как это выглядит, добавленный байт показан серым цветом.

simple_structure_alignment_pack

В данном случае после члена s.c добавлен 1 байт, чтобы поле s.s (sizeof(short)==2) имело границу 2 байта (выравниваение для типа short).

Смещение к началу структуры в массиве также будет выравнено на границу 4 байт, т.е. для Simple_Structure arr[], адреса элементов a[0], a[1], a[n] будут кратными 4 байтам.

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

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- простая структура с выравниванием на границу 4 байта
   struct CharShortInt pack(4)
     {
      char              c; // sizeof(char)=1
      short             s; // sizeof(short)=2
      int               i; // sizeof(double)=4
     };
//--- объявим экземпляр простой структуры   
   CharShortInt ch_sh_in;
//--- выведем размер каждого члена структуры   
   Print("sizeof(ch_sh_in.c)=",sizeof(ch_sh_in.c));
   Print("sizeof(ch_sh_in.s)=",sizeof(ch_sh_in.s));
   Print("sizeof(ch_sh_in.i)=",sizeof(ch_sh_in.i));
 
//--- убедимся, что размер POD-структуры равен сумме размеров её членов
   Print("sizeof(CharShortInt)=",sizeof(CharShortInt));
/*
   Результат:
   sizeof(ch_sh_in.c)=1
   sizeof(ch_sh_in.s)=2
   sizeof(ch_sh_in.i)=4
   sizeof(CharShortInt)=8
*/   
  }

Как видим, размер структуры равен 8 и состоит из двух блоков по 4 байта. В первом блоке размещаются поля с типами char и short, во втором блоке находится поле с типом int.

charshortint

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

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- простая структура с выравниванием на границу 4 байта
   struct CharIntShort pack(4)
     {
      char              c; // sizeof(char)=1
      int               i; // sizeof(double)=4
      short             s; // sizeof(short)=2
     };
//--- объявим экземпляр простой структуры   
   CharIntShort ch_in_sh;
//--- выведем размер каждого члена структуры   
   Print("sizeof(ch_in_sh.c)=",sizeof(ch_in_sh.c));
   Print("sizeof(ch_in_sh.i)=",sizeof(ch_in_sh.i));
   Print("sizeof(ch_in_sh.s)=",sizeof(ch_in_sh.s));
//--- убедимся, что размер POD-структуры равен сумме размеров её членов
   Print("sizeof(CharIntShort)=",sizeof(CharIntShort));
/*
   Результат:
   sizeof(ch_in_sh.c)=1
   sizeof(ch_in_sh.i)=4
   sizeof(ch_in_sh.s)=2
   sizeof(CharIntShort)=12
*/   
  }

Хотя сам состав структуры практически не изменился, но изменение порядка членов привело к увеличению размера самой структуры.

charintshort

При наследовании также необходимо учитывать выравнивание. Покажем на примере простой структуры Parent, которая имеет только один член типа char. Размер такой структуры без выравнивания равен 1.

   struct Parent
     {
      char              c;    // sizeof(char)=1
     };

Создаём дочерний класс Children с добавлением члена типа short (sizeof(short)=2).

   struct Children pack(2) : Parent
     {
      short             s;   // sizeof(short)=2
     };

В результате при установке выравнивания в 2 байта размер структуры будет равен 4, хотя размер самих членов в ней равен 3. В данном примере под родительский класс Parent будет выделено 2 байта, чтобы доступ к полю short дочернего класса был выравнен на 2 байта.

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

В Стандартной библиотеке в каталоге MQL5\Include\WinAPI представлены функции для работы с функциями WinAPI. Эти функции используют структуры с заданным выравниванием для тех случаев, когда это требуется для работы с WinAPI.  

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

//--- объявим переменную типа Children
   Children child;  
//--- узнаем смещения от начала структуры 
   Print("offsetof(Children,c)=",offsetof(Children,c));
   Print("offsetof(Children,s)=",offsetof(Children,s));  
/*
   Результат:
   offsetof(Children,c)=0
   offsetof(Children,s)=2
*/   

Спецификатор final #

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

struct settings final
  {
  //--- тело структуры
  };
 
struct trade_settings : public settings
  {
  //--- тело структуры
  };

При попытке наследования от структуры с спецификатором final, как показано в примере выше, компилятор выдаст ошибку:

cannot inherit from 'settings' as it has been declared as 'final'
see declaration of 'settings'

Классы #

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

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

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

Пример:

struct trade_settings
  {
   double take;         // значения цены фиксации прибыли
   double stop;         // значение цены защитного стопа
   uchar  slippage;     // значение допустимого проскальзывания
   //--- конструктор
          trade_settings() { take=0.0; stop=0.0; slippage=5; }
   //--- деструктор
         ~trade_settings() { Print("Это конец"); } 
  };
//--- компилятор выдаст ошибку с сообщением о невозможности инициализации
trade_settings my_set={0.0,0.0,5};  

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

Конструктор - это специальная функция, которая вызывается автоматически при создании объекта структуры или класса и обычно используется для инициализации членов класса. Далее мы будем говорить только о классах, при этом все сказанное относится и к структурам, если не оговорено иное. Имя конструктора должно совпадать с именем класса. Конструктор не имеет возвращаемого типа (можно указать тип 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'2009.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 имеет три конструктора:

  1. явный конструктор по умолчанию, который позволяет создавать массив объектов данного класса;
  2. конструктор с одним параметром, который принимает в качестве параметра полное имя и разделяет его на имя и фамилию по найденному пробелу;
  3. конструктор с двумя параметрами, который содержит список инициализации. Инициализаторы – 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 или нет.

Определение методов классов

Функции-методы класса могут быть определены как внутри класса, так и за пределами объявления класса. Если метод определяется внутри класса, то его тело следует непосредственно после объявления метода.

Пример:

class CTetrisShape
  {
protected:
   int               m_type;
   int               m_xpos;
   int               m_ypos;
   int               m_xsize;
   int               m_ysize;
   int               m_prev_turn;
   int               m_turn;
   int               m_right_border;
public:
   void              CTetrisShape();
   void              SetRightBorder(int border) { m_right_border=border; }
   void              SetYPos(int ypos)          { m_ypos=ypos;           }
   void              SetXPos(int xpos)          { m_xpos=xpos;           }
   int               GetYPos()                  { return(m_ypos);        }
   int               GetXPos()                  { return(m_xpos);        }
   int               GetYSize()                 { return(m_ysize);       }
   int               GetXSize()                 { return(m_xsize);       }
   int               GetType()                  { return(m_type);        }
   void              Left()                     { m_xpos-=SHAPE_SIZE;    }
   void              Right()                    { m_xpos+=SHAPE_SIZE;    }
   void              Rotate()                   { m_prev_turn=m_turn; if(++m_turn>3) m_turn=0; }
   virtual void      Draw()                     { return;                }
   virtual bool      CheckDown(int& pad_array[]);
   virtual bool      CheckLeft(int& side_row[]);
   virtual bool      CheckRight(int& side_row[]);
  }; 

Функции с SetRightBorder(int border) по Draw() объявлены и определены прямо внутри класса CTetrisShape.

Конструктор CTetrisShape() и методы CheckDown(int& pad_array[]), CheckLeft(int& side_row[]) и CheckRight(int& side_row[]) только объявлены внутри класса, но пока не определены. Определения этих функций должны следовать далее по коду. Для того чтобы определить метод вне класса используется операция разрешения контекста, в качестве контекста используется имя класса.

Пример:

//+------------------------------------------------------------------+
//| Конструктор базового класса                                      |
//+------------------------------------------------------------------+
void CTetrisShape::CTetrisShape()
  {
   m_type=0;
   m_ypos=0;
   m_xpos=0;
   m_xsize=SHAPE_SIZE;
   m_ysize=SHAPE_SIZE;
   m_prev_turn=0;
   m_turn=0;
   m_right_border=0;
  }
//+------------------------------------------------------------------+
//| Проверка возможности двигаться вниз (для палки и куба)           |
//+------------------------------------------------------------------+
bool CTetrisShape::CheckDown(int& pad_array[])
  {
   int i,xsize=m_xsize/SHAPE_SIZE;
//---
   for(i=0; i<xsize; i++)
     {
      if(m_ypos+m_ysize>=pad_array[i]) return(false);
     }
//---
   return(true);
  }

Спецификаторы доступа public, protected и private

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

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

Пример:

class CTetrisField
  {
private:
   int               m_score;                            // счёт
   int               m_ypos;                             // текущее положение фигуры
   int               m_field[FIELD_HEIGHT][FIELD_WIDTH]; // матрица стакана
   int               m_rows[FIELD_HEIGHT];               // нумерация рядов стакана 
   int               m_last_row;                         // последний свободный ряд
   CTetrisShape     *m_shape;                            // тетрисная фигура
   bool              m_bover;                            // игра закончена
public:
   void              CTetrisField() { m_shape=NULL; m_bover=false; }
   void              Init();
   void              Deinit();
   void              Down();
   void              Left();
   void              Right();
   void              Rotate();
   void              Drop();
private:
   void              NewShape();
   void              CheckAndDeleteRows();
   void              LabelOver();
  }; 

Любые члены и методы класса, объявленные после спецификатора public: (и до следующего спецификатора доступа), доступны при любом обращении программы к объекту этого класса. В данном примере это следующие члены: функции CTetrisField(), Init(),  Deinit(), Down(), Left(), Right(), Rotate() и Drop().

Любые члены класса, объявленные после спецификатора доступа private: (и до следующего спецификатора доступа), доступны только функциям-членам этого класса. Спецификаторы доступа к элементам всегда заканчиваются двоеточием (:) и могут появляться в определении класса много раз.

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

class A
  {
protected:
   //--- оператор копирования доступен только внутри класса A и его наследников
   void operator=(const A &)
     {
     }
  };
class B
  {
   //--- обьявлен объект класса A
   A                 a;
  };
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   //--- объявим две переменные типа B
   B b1b2;
   //--- попытка скопировать один объект в другой
   b2=b1;
  }

При компиляции этого кода будет получено сообщение об ошибке — попытка вызвать удаленный оператор копирования:

attempting to reference deleted function 'void B::operator=(const B&)'   trash3.mq5   32   6

И второй строкой ниже дается более подробное описание — оператор копирования в классе B был явно удален, так как вызывается недоступный оператор копирования класса A:

   function 'void B::operator=(const B&)' was implicitly deleted because it invokes inaccessible function 'void A::operator=(const A&)' 

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

Спецификатор delete

Спецификатор delete помечает функции-члены класса, которые нельзя использовать. Это означает, что если программа ссылается явно или неявно на такую функцию, то будет получена ошибка уже на этапе компиляции. Например, данный спецификатор позволяет сделать недоступными родительские методы в дочернем классе. Такого же результата можно добиться, если в родительском классе объявить функцию в приватной области (объявления в секции private). Использование delete в данном случае делает код более читаемым и управляемым на уровне наследников.

class A
  {
public:
                     A(void) {value=5;};
   double            GetValue(void) {return(value);}
private:
   double            value;
  };
class Bpublic A
  {
   double            GetValue(void)=delete;
  };
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- объявим переменную типа A
   A a;
   Print("a.GetValue()="a.GetValue());
//--- попытка получить значение из переменной типа B
   B b;
   Print("b.GetValue()="b.GetValue()); // компилятор выдаст ошибку на этой строке
  }

Сообщение компилятора:

attempting to reference deleted function 'double B::GetValue()'   
   function 'double B::GetValue()' was explicitly deleted here   

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

class A
  {
public:
   void              SetValue(double v) {value=v;}
   //--- запрещаем вызов с типом int
   void              SetValue(int) = delete;
   //--- запрещаем оператор копирования
   void              operator=(const A&) = delete;
private:
   double            value;
  };
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- объявим две переменные типа A
   A a1a2;
   a1.SetValue(3);      // ошибка!
   a1.SetValue(3.14);   // OK
   a2=a1;               // ошибка!
  }

При попытке компиляции получим сообщения об ошибках:

attempting to reference deleted function 'void A::SetValue(int)' 
   function 'void A::SetValue(int)' was explicitly deleted here 
attempting to reference deleted function 'void A::operator=(const A&)'  
   function 'void A::operator=(const A&)' was explicitly deleted here  

Спецификатор final #

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

class CFoo final
  {
  //--- тело класса
  };
 
class CBar : public CFoo
  {
  //--- тело класса
  };

При попытке наследования от класса с спецификатором final, как показано в примере выше, компилятор выдаст ошибку:

cannot inherit from 'CFoo' as it has been declared as 'final'
see declaration of 'CFoo'

Объединение (union) #

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

union LongDouble
{
  long   long_value;
  double double_value;
};

Но в отличие от структуры, разные члены объединения относятся к одному и тому же участку памяти. В данном примере объявлено объединение LongDouble, в котором значение типа long и значение типа double разделяют одну и ту же область памяти. Важно понимать -  невозможно сделать так, чтобы это объединение хранило одновременно целочисленное значение long и вещественное double (как это было бы в структуре), поскольку переменные long_value и double_value накладываются (в памяти) друг на друга. Но зато MQL5-программа в любой момент может обрабатывать информацию, содержащуюся в этом объединении, как целочисленное значение (long) или как вещественное (double).  Следовательно, объединение позволяет получить два (или больше) варианта представления одной и той же последовательности данных.

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

union LongDouble
{
  long   long_value;
  double double_value;
};
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   LongDouble lb;
//--- получим недействительное число -nan(ind) и выведем его
   lb.double_value=MathArcsin(2.0);
   printf("1.  double=%f                integer=%I64X",lb.double_value,lb.long_value);
//--- наибольшее нормализованное число (DBL_MAX)
   lb.long_value=0x7FEFFFFFFFFFFFFF;
   printf("2.  double=%.16e  integer=%I64X",lb.double_value,lb.long_value);
//--- наименьшее положительное нормализованное (DBL_MIN)
   lb.long_value=0x0010000000000000;    
   printf("3.  double=%.16e  integer=%.16I64X",lb.double_value,lb.long_value);
  }
/*  Результат выполнения
    1.  double=-nan(ind)                integer=FFF8000000000000
    2.  double=1.7976931348623157e+308  integer=7FEFFFFFFFFFFFFF
    3.  double=2.2250738585072014e-308  integer=0010000000000000
*/

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

Объединения не могут участвовать в наследовании, а также они не могут иметь статических членов по определению. В остальном union ведёт себя как структура, у которой все члены имеют нулевое смещение. При этом членами объединения не могут быть следующие типы:

Также как и классы, объединение может иметь конструкторы и деструкторы, а также и методы. По умолчанию члены объединения имеют тип доступа public, для создания закрытых элементов необходимо использовать ключевое слово private. Все эти возможности представлены в примере, который показывает как преобразовать цвет, имеющий тип color, в представление ARGB, как это делает функция ColorToARGB().

//+------------------------------------------------------------------+
//| Объединение для конвертации color(BGR) в представление ARGB      |
//+------------------------------------------------------------------+
union ARGB
  {
   uchar             argb[4];
   color             clr;
   //--- конструкторы
                     ARGB(color col,uchar a=0){Color(col,a);};
                    ~ARGB(){};
   //--- публичные методы
public:
   uchar   Alpha(){return(argb[3]);};
   void    Alpha(const uchar alpha){argb[3]=alpha;};
   color   Color(){ return(color(clr));};
   //--- закрытые методы
private:
   //+------------------------------------------------------------------+
   //| установка цвета и значения альфа-канала                          |
   //+------------------------------------------------------------------+
   void    Color(color col,uchar alpha)
     {
      //--- установим цвет в член clr
      clr=col;
      //--- установим значение компонента Alpha - уровня непрозрачности
      argb[3]=alpha;
      //--- переставим местами байты компонент R и B (Red и Blue)     
      uchar t=argb[0];argb[0]=argb[2];argb[2]=t;
     };
  };
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- значение 0x55 означает 55/255=21.6 % (0% - полностью прозрачный)
   uchar alpha=0x55; 
//--- тип color имеет представление 0x00BBGGRR
   color test_color=clrDarkOrange;
//--- сюда будем принимать значения байтов из объединения ARGB
   uchar argb[]; 
   PrintFormat("0x%.8X - так выглядит тип color для %s, BGR=(%s)",
               test_color,ColorToString(test_color,true),ColorToString(test_color));
//--- тип ARGB представлен как 0x00RRGGBB, переставлены местами RR и BB компоненты
   ARGB argb_color(test_color);
//--- скопируем массив байтов
   ArrayCopy(argb,argb_color.argb);
//--- посмотрим как выглядит в представлении ARGB   
   PrintFormat("0x%.8X - представление ARGB c альфа-каналом=0x%.2x, ARGB=(%d,%d,%d,%d)",
               argb_color.clr,argb_color.Alpha(),argb[3],argb[2],argb[1],argb[0]);
//--- добавим значение непрозрачности
   argb_color.Alpha(alpha);
//--- попробуем вывести ARGB как тип color
   Print("ARGB как color=(",argb_color.clr,")  альфа-канал=",argb_color.Alpha());
//--- скопируем массив байтов
   ArrayCopy(argb,argb_color.argb);
//--- а вот как выглядит в представлении ARGB
   PrintFormat("0x%.8X - представление ARGB c альфа-каналом=0x%.2x, ARGB=(%d,%d,%d,%d)",
               argb_color.clr,argb_color.Alpha(),argb[3],argb[2],argb[1],argb[0]);
//--- сверим с тем, что выдает функция ColorToARGB()
   PrintFormat("0x%.8X - результат ColorToARGB(%s,0x%.2x)",ColorToARGB(test_color,alpha),
               ColorToString(test_color,true),alpha);
  }
/* Результат выполнения 
   0x00008CFF - так выглядит тип color для clrDarkOrange, BGR=(255,140,0)
   0x00FF8C00 - представление ARGB c альфа-каналом=0x00, ARGB=(0,255,140,0)
   ARGB как color=(0,140,255)  альфа-канал=85
   0x55FF8C00 - представление ARGB c альфа-каналом=0x55, ARGB=(85,255,140,0)
   0x55FF8C00 - результат ColorToARGB(clrDarkOrange,0x55)
*/ 

Интерфейсы #

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

Определяется интерфейс с помощью ключевого слова interface, как показано в примере:

//--- базовый интерфейс для описания животных
interface IAnimal
  {
//--- методы интерфейса по умолчанию имеют public-доступ
   void Sound();  // звук, который издает животное
  };
//+------------------------------------------------------------------+
//|  класс CCat наследуется от интерфейса IAnimal                    |
//+------------------------------------------------------------------+
class CCat : public IAnimal
  {
public:
                     CCat() { Print("Cat was born"); }
                    ~CCat() { Print("Cat is dead");  }
   //--- реализуем метод Sound интерфейса IAnimal
   void Sound(){ Print("meou"); }
  };
//+------------------------------------------------------------------+
//|  класс CDog наследуется от интерфейса IAnimal                    |
//+------------------------------------------------------------------+
class CDog : public IAnimal
  {
public:
                     CDog() { Print("Dog was born"); }
                    ~CDog() { Print("Dog is dead");  }
   //--- реализуем метод Sound интерфейса IAnimal
   void Sound(){ Print("guaf"); }
  };
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- массив указателей на объекты типа IAnimal
   IAnimal *animals[2];
//--- породим потомков IAnimal и сохраним указатели на них в массив    
   animals[0]=new CCat;
   animals[1]=new CDog;
//--- вызовем метод Sound() базового интерфейса IAnimal для каждого потомка  
   for(int i=0;i<ArraySize(animals);++i)
      animals[i].Sound();
//--- удалим объекты
   for(int i=0;i<ArraySize(animals);++i)
      delete animals[i];
//--- результат выполнения
/*
   Cat was born
   Dog was born
   meou
   guaf
   Cat is dead
   Dog is dead
*/
  }

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

Интерфейс нельзя объявить внутри объявления класса или структуры, но при этом указатель на интерфейс можно сохранить в переменную типа void *. Вообще говоря, в переменную типа void * можно сохранить указатель на объект любого класса. Для того чтобы преобразовать указатель void * в указатель на объект конкретного класса, необходимо использовать оператор dynamic_cast.  В случае, когда преобразование невозможно, результатом операции dynamic_cast будет NULL.

Смотри также

Объектно-ориентированное программирование