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

Dmitry Fedoseev | 6 декабря, 2011

 

Введение

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

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

 

Создание библиотек функций

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

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

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

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

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

 

Программа в программе

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

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

Важной особенностью класса является его отделенность от пространства, в котором он находится. Класс подобен программе, работающей в операционной системе: одновременно может работать несколько программ, но все они работают самостоятельно, независимо друг от друга. Поэтому и класс, поскольку он отделен от пространства, в котором находится, можно назвать "программой в программе".

 

Как выглядит класс

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

class CName 
  {
   // Здесь располагается весь код класса
  };
Внимание! Не забывайте ставить точку с запятой после закрывающей фигурной скобки.

 

Видимое и скрытое (инкапсуляция)

Если взять любую программу, известно, что она включает в себя различные функции. Эти функции можно разделить на два типа: основные и вспомогательные. Основные функции - это функции, из которых собственно и создается программа. Но для их работы может потребоваться множество других функций, про которые пользователю не обязательно знать. Например, в терминале для открытия позиции трейдеру нужно открыть окно ордера, ввести объем, значения Стоп Лосс и Тейк Профит, а затем нажать кнопку "Buy" или "Sell".

Но что же происходит на самом деле между нажатием кнопки и открытием позиции точно могут знать только разработчики терминала. Можно предположить, что терминал при этом совершает немало действий: проверка объема позиции, проверка значений Стоп Лосс, Тейк Профит, проверка связи и т.п. Очень многое скрыто внутри от глаз пользователя или, иными словами, инкапсулировано. Подобным образом и в классе можно разделить части кода (функции и переменные), которые будут доступны при использовании класса и которые будут скрыты.

Уровни инкапсуляции определяются ключевыми словами private (закрытый), protected (защищенный) и public (открытый). Отличие protected от private рассмотрим несколько позже, сначала коснемся только private и public - закрытый и открытый. Таким образом, простейший шаблон класса приобретает следующий вид:

class CName 
  {
private:
   // Здесь располагаются переменные и функции, доступные только внутри класса
public:
   // Здесь располагаются переменные и функции, доступные также вне класса
  };
Это достаточный минимум, чтобы уже воспользоваться преимуществами ООП в очень значительном объеме. Вместо того чтобы писать свой код непосредственно в файле эксперта (скрипта или индикатора), сначала создаем класс, а потом все пишем внутри этого класса. Далее рассмотрим различие секций private и public на практическом примере.

 

Пример создания библиотеки

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

Функцию для добавления элемента к массиву назовем AddToEnd(), функцию добавления при условии отсутствия добавляемого элемента - AddToEndIfNotExists(). В функции AddToEndIfNotExists() сначала потребуется проверить массив на существование элемента и если его нет - воспользоваться функцией AddToEnd(). Функцию проверки существования элемента будем считать вспомогательной и, поэтому, разместим ее в секции private, а остальные функции - в секции public. В результате получим следующий класс:

class CLibArray 
  {
private:
   // Проверка существования в массиве элемента с заданным значением
   int Find(int &aArray[],int aValue) 
     {
      for(int i=0; i<ArraySize(aArray); i++) 
        {
         if(aArray[i]==aValue) 
           {
            return(i); // Элемент существует, возвращаем индекс элемента
           }
        }
      return(-1);  // Нет такого элемента, возвращаем -1
     }
public:
   // Добавка в конец массива
   void AddToEnd(int &aArray[],int aValue) 
     {
      int m_size=ArraySize(aArray);
      ArrayResize(aArray,m_size+1);
      aArray[m_size]=aValue;
     }
   // Добавка в конец массива при условии отсутствия в массиве такого значения
   void AddToEndIfNotExistss(int &aArray[],int aValue) 
     {
      if(Find(aArray,aValue)==-1) 
        {
         AddToEnd(aArray,aValue);
        }
     }
  };
 

Загрузка класса

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

#include <OOP_CLibArray_1.mqh>

а затем загрузить класс. Загрузка класса подобна объявлению переменной:

CLibArray ar;

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

Рис.1. Список функций
Рис.1. Список функций

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

В случае простого коллекционирования функций при вводе нескольких начальных букв названия функции в открывающемся списке будут располагаться функции из всех подключенных библиотек, а при использовании классов - только функции, относящиеся к указанному классу. Еще обратите внимание на отсутствие в списке функции Find() - в этом состоит отличие секции private от секции public. Функция записана в секции private и поэтому недоступна.

 

Делаем библиотеку универсальной для разных типов данных (перегрузка)

На данный момент наша библиотека включает функции, работающие только с массивами типа int. Кроме массивов типа int может потребоваться применение функций библиотеки к массивам типа uint, long, ulong и пр. Для массивов других типов потребуется написать свои функции. Но давать этим функциям другие имена не обязательно - нужная функция будет выбираться автоматически, в зависимости от типа передаваемых параметров или набора параметров (в данном примере, в зависимости от типа параметров). Дополним класс функциями работы с массивами типа long:

class CLibArray 
  {
private:
   // Для int. Проверка существования в массиве элемента с заданным значением
   int Find(int &aArray[],int aValue)
     {
      for(int i=0; i<ArraySize(aArray); i++) 
        {
         if(aArray[i]==aValue) 
           {
            return(i); // Элемент существует, возвращаем индекс элемента
           }
        }
      return(-1); // Нет такого элемента, возвращаем -1
     }
   // Для long. Проверка существования в массиве элемента с заданным значением
   int Find(long &aArray[],long aValue) 
     {
      for(int i=0; i<ArraySize(aArray); i++) 
        {
         if(aArray[i]==aValue) 
           {
            return(i); // Элемент существует, возвращаем индекс элемента
           }
        }
      return(-1); // Нет такого элемента, возвращаем -1
     }
public:
   // Для int. Добавка в конец массива
   void AddToEnd(int &aArray[],int aValue) 
     {
      int m_size=ArraySize(aArray);
      ArrayResize(aArray,m_size+1);
      aArray[m_size]=aValue;
     }
   // Для long. Добавка в конец массива
   void AddToEnd(long &aArray[],long aValue) 
     {
      int m_size=ArraySize(aArray);
      ArrayResize(aArray,m_size+1);
      aArray[m_size]=aValue;
     }
   // Для int. Добавка в конец массива при условии отсутствия в массиве такого значения
   void AddToEndIfNotExistss(int &aArray[],int aValue) 
     {
      if(Find(aArray,aValue)==-1) 
        {
         AddToEnd(aArray,aValue);
        }
     }
   // Для long. Добавка в конец массива при условии отсутствия в массиве такого значения
   void AddToEndIfNotExistss(long &aArray[],long aValue) 
     {
      if(Find(aArray,aValue)==-1) 
        {
         AddToEnd(aArray,aValue);
        }
     }
  };
Теперь, используя одно и то же имя, мы имеем различную функциональность. Такие функции называются перегруженными (англ. overloaded), т.е. одно имя нагружено не одной функциональной обязанностью, а несколькими, т.е. перегружено.

Этот пример находится в приложении в файле OOP_CLibArray_1.mqh.

 

Еще один способ записи класса

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

class CLibArray 
  {
private:
   int               Find(int  &aArray[],int  aValue);
   int               Find(long &aArray[],long aValue);
public:
   void              AddToEnd(int  &aArray[],int  aValue);
   void              AddToEnd(long &aArray[],long aValue);
   void              AddToEndIfNotExistss(int  &aArray[],int  aValue);
   void              AddToEndIfNotExistss(long &aArray[],long aValue);
  };
//---
int CLibArray::Find(int &aArray[],int aValue) 
  {
   for(int i=0; i<ArraySize(aArray); i++) 
     {
      if(aArray[i]==aValue) 
        {
         return(i);
        }
     }
   return(-1);
  }
//---
int CLibArray::Find(long &aArray[],long aValue) 
  {
   for(int i=0; i<ArraySize(aArray); i++) 
     {
      if(aArray[i]==aValue) 
        {
         return(i);
        }
     }
   return(-1);
  }
//---
void CLibArray::AddToEnd(int &aArray[],int aValue) 
  {
   int m_size=ArraySize(aArray);
   ArrayResize(aArray,m_size+1);
   aArray[m_size]=aValue;
  }
//---
void CLibArray::AddToEnd(long &aArray[],long aValue) 
  {
   int m_size=ArraySize(aArray);
   ArrayResize(aArray,m_size+1);
   aArray[m_size]=aValue;
  }
//---
void CLibArray::AddToEndIfNotExistss(int &aArray[],int aValue) 
  {
   if(Find(aArray,aValue)==-1) 
     {
      AddToEnd(aArray,aValue);
     }
  }
//---
void CLibArray::AddToEndIfNotExistss(long &aArray[],long aValue) 
  {
   if(Find(aArray,aValue)==-1) 
     {
      AddToEnd(aArray,aValue);
     }
  }

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

Этот пример находится в приложении в файле OOP_CLibArray_2.mqh.

 

Объявление переменных в классе

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

int Var = 123;

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

Рассмотрим это на практическом примере.

 

Пример преобразования скрипта в класс

Допустим, есть скрипт для удаления отложенных ордеров (см. файл OOP_sDeleteOrders_1.mq5 в приложении).

// Подключение файла для использования класса CTrade из комплекта терминала
#include <Trade/Trade.mqh>

// Внешние параметры

// Выбор символа. true  - удалять ордера всех символов, 
//                false - только с символом графика, на котором выполняется скрипт
input bool AllSymbol=false;

// Включение типов удаляемых ордеров
input bool BuyStop       = false;
input bool SellStop      = false;
input bool BuyLimit      = false;
input bool SellLimit     = false;
input bool BuyStopLimit  = false;
input bool SellStopLimit = false;

// Загрузка класса CTrade
CTrade Trade;
//---
void OnStart()
  {
// Переменная для проверки результата работы функции
   bool Ret=true;
// По всем ордерам в терминале
   for(int i=0; i<OrdersTotal(); i++)
     {
      ulong Ticket=OrderGetTicket(i); // Выделение ордера и получение его тикета
                                      // Удалось выделить
      if(Ticket>0)
        {
         long Type=OrderGetInteger(ORDER_TYPE);
         // Проверки типа ордера
         if(Type == ORDER_TYPE_BUY_STOP && !BuyStop) continue;
         if(Type == ORDER_TYPE_SELL_STOP && !SellStop) continue;
         if(Type == ORDER_TYPE_BUY_LIMIT && !BuyLimit) continue;
         if(Type == ORDER_TYPE_SELL_LIMIT && !SellLimit) continue;
         if(Type == ORDER_TYPE_BUY_STOP_LIMIT && !BuyStopLimit) continue;
         if(Type == ORDER_TYPE_SELL_STOP_LIMIT && !SellStopLimit) continue;
         // Проверка символа
         if(!AllSymbol && Symbol()!=OrderGetString(ORDER_SYMBOL)) continue;
         // Удаление
         if(!Trade.OrderDelete(Ticket))
           {
            Ret=false; // Не удалось удалить
           }
        }
      // Не удалось выделить ордер, результат неизвестен,
      // значит функция отработала с ошибкой
      else
        {
         Ret=false;
         Print("Ошибка выделения ордера");
        }
     }

   if(Ret)
     {
      Alert("Работа скрипта завершена успешно");
     }
   else    
     {
      Alert("При работе скрипта произошла ошибка, подробности см. в журнале");
     }
  }

У скрипта имеются внешние параметры, позволяющие включать различные типы ордеров и выбирать символ, на котором удаляются ордера (все символы или символ графика, на котором выполняется скрипт).

Переделаем этот скрипт в класс с именем COrderDelete. В секции private объявим те же самые переменные, что объявлены в скрипте, как внешние параметры, но только добавим к именам переменных префикс "m_" (от слова member, т.е. член класса). Добавление префикса не обязательно, но очень удобно, поскольку позволяет отличать переменные. Так мы сможем точно знать, что имеем дело с переменными, находящимися в ограниченном классом пространстве. Кроме того, при компиляции не будут выдаваться предупреждения о том, что объявление переменной скрывает переменную, объявленную на глобальном уровне.

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

Получаем такой класс:

#include <Trade/Trade.mqh> 

class COrderDelete 
  {

private:
   // Переменные для параметров
   bool              m_AllSymbol;
   bool              m_BuyStop;
   bool              m_SellStop;
   bool              m_BuyLimit;
   bool              m_SellLimit;
   bool              m_BuyStopLimit;
   bool              m_SellStopLimit;
   // Загрузка класса CTrade
   CTrade            m_Trade;
public:
   // Функция для установки параметров
   void Init(bool aAllSymbol,bool aBuyStop,bool aSellStop,bool aBuyLimit,bool aSellLimit,bool aBuyStopLimit,bool aSellStopLimit) 
     {
      // Установка параметров
      m_AllSymbol    =aAllSymbol;
      m_BuyStop      =aBuyStop;
      m_SellStop     =aSellStop;
      m_BuyLimit     =aBuyLimit;
      m_SellLimit    =aSellLimit;
      m_BuyStopLimit =aBuyStopLimit;
      m_SellStopLimit=aSellStopLimit;
     }
   // Основная функция для удаления ордеров
   bool Delete() 
     {
      // Переменная для проверки результата работы функции
      bool m_Ret=true;
      // По всем ордерам в терминале
      for(int i=0; i<OrdersTotal(); i++) 
        {
         // Выделение ордера и получение его тикета
         ulong m_Ticket=OrderGetTicket(i);
         // Удалось выделить
         if(m_Ticket>0) 
           {
            long m_Type=OrderGetInteger(ORDER_TYPE);
            // Проверки типа ордера
            if(m_Type == ORDER_TYPE_BUY_STOP && !m_BuyStop) continue;
            if(m_Type == ORDER_TYPE_SELL_STOP && !m_SellStop) continue;
            if(m_Type == ORDER_TYPE_BUY_LIMIT && !m_BuyLimit) continue;
            if(m_Type == ORDER_TYPE_SELL_LIMIT && !m_SellLimit) continue;
            if(m_Type == ORDER_TYPE_BUY_STOP_LIMIT && !m_BuyStopLimit) continue;
            if(m_Type == ORDER_TYPE_SELL_STOP_LIMIT && !m_SellStopLimit) continue;
            // Проверка символа
            if(!m_AllSymbol && Symbol()!=OrderGetString(ORDER_SYMBOL)) continue;
            // Удаление
            if(!m_Trade.OrderDelete(m_Ticket)) 
              {
               m_Ret=false; // Не удалось удалить
              }
           }
         // Не удалось выделить ордер, результат неизвестен,
         // значит функция отработала с ошибкой
         else 
           {
            m_Ret=false;
            Print("Ошибка выделения ордера");
           }
        }
      // Возвращение результата работа функции
      return(m_Ret);
     }
  };
Пример этого класса находится в приложении в файле OOP_CDeleteOrder_1.mqh. Скрипт с использованием этого класса сокращается до минимума (внешние параметры, загрузка класса, вызов методов Init() и Delete()):
// Внешние параметры

// Выбор символа. true  - удалять ордера всех символов, 
//                false - только с символом графика на котором выполняется скрипт
input bool AllSymbol=false;

// Включение типов удаляемых ордеров
input bool BuyStop       = false;
input bool SellStop      = false;
input bool BuyLimit      = false;
input bool SellLimit     = false;
input bool BuyStopLimit  = false;
input bool SellStopLimit = false;

// Подключение файла с классом
#include <OOP_CDeleteOrder_1.mqh> 

// Загрузка класса
COrderDelete od;
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnStart() 
  {
// Передача классу внешних параметров
   od.Init(AllSymbol,BuyStop,SellStop,BuyLimit,SellLimit,BuyStopLimit,SellStopLimit);
// Удаление ордеров
   bool Ret=od.Delete();
// Обработка результата работы функции удаления
   if(Ret) 
     { 
       Alert("Работа скрипта завершена успешно"); 
     }
   else    
     { 
       Alert("При работе скрипта произошла ошибка, подробности см. в журнале"); 
     }
  }

Пример этого скрипта находится в приложении в файле OOP_sDeleteOrders_2.mq5. Большую часть скрипта занимает обработка результатов работы функции Delete() для уведомления о результатах работы скрипта.

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

 

Немого автоматики (конструктор и деструктор)

Работу программы можно разделить на три этапа: запуск программы, процесс ее работы и завершение ее работы. Важность такого разделения очевидна: при запуске выполняется подготовка программы (например, загрузка и установка параметров, с которыми программа будет работать), при завершении работы программа должна, фигурально выражаясь, "убрать за собой" (например, удалить графические объекты с графика).

Для разделения этих этапов в экспертах и индикаторах предусмотрены специальные функции: OnInit() (работает при запуске) и OnDeinit() (работает при завершении работы). Аналогичные возможности имеются и у классов: в класс можно добавить такие функции, которые будут автоматически выполняться при загрузке класса и его выгрузке. Эти функции называются конструктором и деструктором. Чтобы добавить конструктор, необходимо добавить в класс функцию с точно таким же именем как у имени класса. Для добавления деструктора нужно выполнить все то же самое, что и для конструктора, но имя функции начинается со знака тильда "~".

Скрипт, демонстрирующий работу конструктора и деструктора:

// Класс
class CName 
  {
public:
   // Конструктор
                     CName() { Alert("Конструктор"); }
   // Деструктор
                    ~CName() { Alert("Деструктор"); }

   void Sleep() { Sleep(3000); }
  };

// Загрузка класса
CName cname;
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnStart() 
  {
// Делаем паузу
   cname.Sleep();
  }

В этом классе фактически только одна функция Sleep(), выполняющая паузу в 3 секунды. При запуске скрипта открывается окно с сообщением "Конструктор", затем после трехсекундной паузы открывается окно с сообщением "Деструктор". Это происходит несмотря на то, что функции CName() и ~CName() нигде не вызываются в явном виде.

Этот пример находится в приложении в файле OOP_sConstDestr_1.mq5.

 

Передача параметров в конструктор

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

COrderDelete(bool aAllSymbol     = false,
             bool aBuyStop       = false,
             bool aSellStop      = false,
             bool aBuyLimit      = false,
             bool aSellLimit     = false,
             bool aBuyStopLimit  = false,
             bool aSellStopLimit=false) 
  {
   Init(aAllSymbol,aBuyStop,aSellStop,aBuyLimit,aSellLimit,aBuyStopLimit,aSellStopLimit);
  }

Функция Init() осталась, как и была, а из конструктора выполняется ее вызов. Все параметры конструктора необязательные, чтобы классом можно было пользоваться как и прежде: загружать класс без параметров и вызывать функцию Init().

После создания конструктора доступен еще один вариант использования этого класса. При загрузке класса в него можно сразу передать параметры, при этом нет необходимости вызывать функцию Init():

COrderDelete od(AllSymbol,BuyStop,SellStop,BuyLimit,SellLimit,BuyStopLimit,SellStopLimit);

Функция Init() отставлена в секции public для обеспечения возможности переинициализации класса. В процессе работы программы (эксперта), в одном случае может потребоваться удалить только стоп ордера, в другом случае может потребоваться удаление только лимитных ордеров. Для этого можно вызывать Init() с различными параметрами, после чего функцией Delete() будут удаляться различные наборы ордеров.

Этот пример находится в приложении в файлах OOP_CDeleteOrder_2.mqh и OOP_sDeleteOrders_3.mq5.

 

Использование нескольких экземпляров класса

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

Например, заранее известно, что в процессе работы эксперта в некоторых случаях потребуется удалять ордера типа BuyStop и BuyLimit, а в других случаях - SellStop и SellLimit. В таком случае можно загрузить два экземпляра класса.

Для удаления BuyStop и BuyLimit:

COrderDelete DeleteBuy(false,true,false,true,false,false,false);

Для удаления SellStop и SellLimit:

COrderDelete DeleteSell(false,false,true,false,true,false,false);

Теперь, когда надо удалить отложенные ордера Buy, используем один экземпляр класса:

DeleteBuy.Delete();

Когда надо удалить отложенные ордера Sell - другой:

DeleteSell.Delete();

 

Массив объектов

Не всегда может быть заранее известно количество экземпляров класса, которое потребуется в процессе работы программы. В таком случае можно создать массив с экземплярами классов (объектами). Рассмотрим это на примере класса с конструктором и деструктором. Немного переделав класс, добавим передачу параметра в конструктор, чтобы можно было проконтролировать работу каждого экземпляра класса:

// Класс
class CName 
  {
private:
   int               m_arg; // Переменная для номера экземпляра

public:
   // Конструктор
   CName(int aArg) 
     {
      m_arg=aArg;
      Alert("Конструктор "+IntegerToString(m_arg));
     }
   // Деструктор
  ~CName() 
     { 
      Alert("Деструктор "+IntegerToString(m_arg)); 
     }
   //---
   void Sleep() 
     { 
      Sleep(3000); 
     }
  };
Применяем этот класс. Можно объявить массив определенного размера, например десять элементов:
CName* cname[10];

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

Можно использовать динамический массив (без заранее указанного размера, не путать динамический массив с динамическим указателем):

CName* cname[];

В этом случаем потребуется его масштабирование (выполняется внутри какой-нибудь функции, если в скрипте, то в функции OnStart()):

ArrayResize(cname,10);

Теперь проходим в цикле по всем элементам массива и в каждый загружаем экземпляр класса. Для этого используется ключевое слово new:

ArrayResize(cname,10);
for(int i=0; i<10; i++) 
  {
   cname[i]=new CName(i);
  }
Делаем паузу:
cname[0].Sleep();

Проверяем работу скрипта. Запускаем его и видим, что выполнено десять конструкторов, но ни одного деструктора. При использовании динамических указателей классы сами не выгружаются при завершении работы программы. Кроме того, что не выполнены деструкторы, на вкладке "Эксперты" еще можно увидеть сообщение об утечке памяти (leaked memory). Необходимо самостоятельно выполнить удаление:

for(int i=0; i<10; i++) 
  {
   delete(cname[i]);
  }

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

Этот пример находится в приложении в файле OOP_sConstDestr_2.mq5.

 

Использование ООП для изменения логики работы программы (виртуальные функции, полиморфизм)

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

Возьмем простой пример - сравнение двух значений. Может быть пять вариантов сравнения: больше (>), меньше (<), больше или равно (>=), меньше или равно (<=), равно (==).

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

class CCheckVariant 
  {
public:
   virtual bool CheckVariant(int Var1,int Var2) 
     {
      return(false);
     }
  };

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

Пишем пять потомков:

//+------------------------------------------------------------------+
//|   >                                                              |
//+------------------------------------------------------------------+

class CVariant1: public CCheckVariant
  {
   bool CheckVariant(int Var1,int Var2)
     {
      return(Var1>Var2);
     }
  };
//+------------------------------------------------------------------+
//|   <                                                              |
//+------------------------------------------------------------------+
class CVariant2: public CCheckVariant
  {
   bool CheckVariant(int Var1,int Var2)
     {
      return(Var1<Var2);
     }
  };
//+------------------------------------------------------------------+
//|   >=                                                             |
//+------------------------------------------------------------------+
class CVariant3: public CCheckVariant
  {
   bool CheckVariant(int Var1,int Var2)
     {
      return(Var1>=Var2);
     }
  };
//+------------------------------------------------------------------+
//|   <=                                                             |
//+------------------------------------------------------------------+
class CVariant4: public CCheckVariant
  {
   bool CheckVariant(int Var1,int Var2)
     {
      return(Var1<=Var2);
     }
  };
//+------------------------------------------------------------------+
//|   ==                                                             |
//+------------------------------------------------------------------+
class CVariant5: public CCheckVariant
  {
   bool CheckVariant(int Var1,int Var2)
     {
      return(Var1==Var2);
     }
  };

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

CVariant1 var; // Загрузка класса для проверки условия ">"

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

CCheckVariant* var;

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

// Номер варианта
int Variant=5; 
// В зависимости от номера варианта будет использоваться один из пяти классов-потомков
switch(Variant) 
  {
    case 1: 
       var = new CVariant1;
       break;
    case 2: 
       var = new CVariant2;
       break;
    case 3: 
       var = new CVariant3;
       break;
    case 4: 
       var = new CVariant4;
       break; 
    case 5: 
       var = new CVariant5;
       break; 
 }

Проверяем условия:

bool rv = var.CheckVariant(1,2);

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

Этот пример находится в приложении в файле OOP_sVariant_1.mq5.

 

Еще об инкапсуляции (private, protected, public)

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

//+------------------------------------------------------------------+
//|   Класс с protected                                              |
//+------------------------------------------------------------------+
class CName1
  {
protected:
   int ProtectedFunc(int aArg)
     {
      return(aArg);
     }
public:
   int PublicFunction(int aArg)
     {
      return(ProtectedFunc(aArg));
     }
  };
//+------------------------------------------------------------------+
//|   Класс с private                                                |
//+------------------------------------------------------------------+
class CName2
  {
private:
   int PrivateFunc(int aArg)
     {
      return(aArg);
     }
public:
   int PublicFunction(int aArg)
     {
      return(PrivateFunc(aArg));
     }
  };

CName1 c1; // Загрузка класса с protected
CName2 c2; // Загрузка класса с private
В этом примере имеется два класса: CName1 и CName2. У каждого класса по две функции, одна функция располагается в секции public, вторая функция располагается в секции protected (у класса CName1) или в секции private (у класса CName2). При этом у обоих классов в списке функций будет только по одной функции из секции public (рис. 2 и 3).

Рис.2. Функции класса CName1
Рис.2. Функции класса CName1

Рис.3. Функции класса CName2
Рис.3. Функции класса CName2

Этот пример находится в приложении в файле OOP_sProtPriv_1.mq5.

Секции private и protected определяют видимость функции базового класса потомками:

//+------------------------------------------------------------------+
//|   Базовый класс                                                  |
//+------------------------------------------------------------------+
class CBase
  {
protected:
   string ProtectedFunc()
     {
      return("CBase ProtectedFunc");
     }
private:
   string PrivateFunc()
     {
      return("CBase PrivateFunc");
     }
public:
   virtual string PublicFunction()
     {
      return("");
     }
  };
//+------------------------------------------------------------------+
//|   Класс потомок                                                  |
//+------------------------------------------------------------------+

class Class: public CBase
  {
public:
   string PublicFunction()
     {
      // С этой строкой все компилируется работает
      return(ProtectedFunc());
      // Если раскомментировать эту строку, а предыдущую закомментировать, будет ошибка компиляции
      // return(PrivateFunc()); 
     }
  };

В этом примере имеем базовый класс CBase и класс-потомок Class. Из функции класса-потомка выполняется попытка вызвать функции базового класса, располагающиеся в секциях protected и private. При вызове функции из секции protected - все компилируется и работает. При вызове функции из секции private выдается ошибка компиляции (cannot call private member function). Т.е. функция из секции private не видна потомку.

Секция protected защищает функции только от пользователя, а секция private - еще и от потомков. Видимость функций базового класса, расположенных в различных секциях, из класса-потомка показана на рис. 4.

Рис.4. Видимость функций базового класса из класса-наследника
Рис.4. Видимость функций базового класса из класса-наследника
Синие стрелки - функции доступны, серая - недоступны.

Этот пример находится в приложении в файле OOP_sProtPriv_2.mq5.

 

Виртуальная функция по умолчанию и наследование

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

//+------------------------------------------------------------------+
//|   Базовый класс                                                  |
//+------------------------------------------------------------------+
class CBase
  {
public:
   virtual string Function()
     {
      string str="";
      str="Функция ";
      str=str+"базового ";
      str=str+"класса";
      return(str);
     }
  };
//+------------------------------------------------------------------+
//|   Класс потомок - 1                                              |
//+------------------------------------------------------------------+
class Class1: public CBase
  {
public:
   string Function()
     {
      string str="";
      str="Функция ";
      str=str+"потомка ";
      return(str);
     }
  };
//+------------------------------------------------------------------+
//|   Класс потомок - 2                                              |
//+------------------------------------------------------------------+
class Class2: public CBase
  {

  };

Class1 c1; // Загрузка класса 1
Class2 c2; // Загрузка класса 2
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnStart()
  {
   Alert("1: "+c1.Function()); // Выполняется функция из Class1
   Alert("2: "+c2.Function()); // Выполняется функция из CBase
  }

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

void OnStart() 
   {
    Alert("1: " + c1.Function()); // Выполняется функция из Class1
    Alert("2: " + c2.Function()); // Выполняется функция из CBase
   }

С позиции пользователя класса, при использовании потомка, будут доступны все функции базового класса из секции public. Данное явление называется наследованием. Если же в базовом классе функция объявлена как виртуальная, то она будет заменяться на функцию потомка в случае ее наличия у потомка (рис. 5).

Рис.5. Доступ к функциям пользователем класса
Рис.5. Доступ к функциям пользователем класса

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

Рис.6. Видимость "лишней" функции
Рис.6. Видимость "лишней" функции (красная стрелка) определяется
типом указателя, с помощью которого загружен класс.

Этот пример находится в приложении в файле OOP_sDefaultVirtual_1.mq5.

 

Еще немного о загрузке классов

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

Class1 c1; // Загрузка класса 1
Class2 c2; // Загрузка класса 2

Если заранее неизвестно какой класс-потомок будет использоваться, то используется динамический указатель с типом базового класса, а загрузка класса выполняется ключевым словом new:

CBase *c; // Динамический указатель 
void OnStart() 
   {
      c=new Class1; // Загрузка класса
      ...

Если использовать автоматический указатель на базовый класс,

CBase c; // Автоматический указатель

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

 

Обработка объектов в функции

В этом разделе, пожалуй, достаточно одного заголовка. Указатели на объекты можно передавать в функции, а в функции обращаться к функциям объекта. Параметр функции может быть объявлен с типом базового класса, что делает функцию универсальной. Указатель на класс может передаваться в функцию только по ссылке (указывается знаком &):

//+------------------------------------------------------------------+
//|   Базовый класс                                                  |
//+------------------------------------------------------------------+
class CBase
  {
public:
   virtual string Function()
     {
      return("");
     }
  };
//+------------------------------------------------------------------+
//|   Класс потомок - 1                                              |
//+------------------------------------------------------------------+
class Class1: public CBase
  {
public:
   string Function()
     {
      return("Класс 1");
     }
  };
//+------------------------------------------------------------------+
//|   Класс потомок - 2                                              |
//+------------------------------------------------------------------+
class Class2: public CBase
  {
public:
   string Function()
     {
      return("Класс 2");
     }
  };

Class1 c1; // Загрузка класса 1
Class2 c2; // Загрузка класса 2
//+------------------------------------------------------------------+
//|   Функция для обработки объектов                                 |
//+------------------------------------------------------------------+
void Function(CBase  &c)
  {
   Alert(c.Function());
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnStart()
  {
// Обрабатываем объекты используя одну функцию.
   Function(c1);
   Function(c2);
  }
Этот пример находится в приложении в файле OOP_sFunc_1.mq5.

 

Функции и методы, переменные и свойства

В этой статье пока использовалось только слово "функция", однако в ООП вместо слово "функция" часто используется слово "метод". Если смотреть на класс изнутри, с точки зрения программиста, пишущего класс, все функции так и остаются функциями. Если же смотреть на класс с точки зрения программиста, использующего готовый класс, то функции, входящие в интерфейс класса и расположенные в секции public (доступны в раскрывающемся списке после нажатия точки), называются методами.

Кроме методов в интерфейс класса могут входить свойства класса. В секции public могут располагаться не только функции, но и переменные (в том числе и массивы).

class CMethodsAndProperties 
   {
    public:
        int               Property1; // Свойство 1
        int               Property2; // Свойство 2
        void Function1() 
           {
            //...
            return;
           }
        void Function2() 
           {
            //...
            return;
           }
   };

Эти переменные будут называться свойствами класса и также будут доступны в раскрывающемся списке (рис. 7).

Рис.7. Методы и свойства класса в одном списке
Рис.7. Методы и свойства класса в одном списке

Используются эти свойства точно так же, как переменные:

void OnStart() 
   {
    c.Property1 = 1; // Установка свойства 1
    c.Property2 = 2; // Установка свойства 2

    // Чтение свойств
    Alert("Property1 = " + IntegerToString(c.Property1) + ", Property2 = " + IntegerToString(c.Property2));
   }

Этот пример находится в приложении в файле OOP_sMethodsAndProperties.mq5.

 

Структуры данных

Структуры данных подобны классам, только несколько проще. Хотя, можно сказать и так: классы подобны структурам данных, но несколько сложнее. Отличие в том, что структуры данных могут включать в себя только переменные. В связи с этим отсутствует необходимость в делении на секции public, private и protected. Все содержимое структуры уже как бы находится в секции public. Структура данных начинается со слова struct, затем идет имя структуры, внутри фигурных скобок объявляются переменные.

struct Str1 
   {
    int    IntVar;
    int    IntArr[];
    double DblVar[];
    double DblArr[];
   };

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

Str1 s1;

Можно объявить и массив структур:

Str1 sar1[];

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

struct Str2 
   {
    int    IntVar;
    int    IntArr[];
    double DblVar[];
    double DblArr[];
    Str1   Str;
   };

В этом случае для обращения к переменным структуры 1, входящей в структуру 2, нужно использовать две точки:

s2.Str.IntVar=1;

Этот пример находится в приложении в файле OOP_Struct.mq5.

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

 

Заключение

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

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

class CName 
  {
private:

protected:

public:
  };

2. Функции и переменные класса могут располагаться в одной из трех секций: private, protected и public. Функции и переменные из секции private доступны только внутри класса. Функции и переменные из секции protected доступны внутри класса и доступны классам-потомкам. Функции из секции public доступы всем.

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

void ClassName::FunctionName() { ... }

4. Класс может загружаться с использованием автоматического или динамического указателя. При использовании динамического указателя класс необходимо загружать с помощью ключевого слова new. В этом случае по завершению работы программы требуется удалить объект с помощью ключевого слова delete.

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

class Class : public CBase { ... }

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

7. Виртуальные функции объявляются ключевым словом virtual. При наличии у класса-потомка одноименной функции выполняется она, в случае отсутствия - выполняется виртуальная функция базового класса.

8. Указатели на классы можно передавать в функции. Допускается объявлять параметры функции с типом базового класса для возможности передавать в функцию указатель на любого потомка.

9. В секции public могут располагаться не только функции (методы), но и переменные (свойства).

10. Структуры могут включать в себя массивы и другие структуры.

 

Список файлов приложения

После экспериментов все файлы, кроме OOP_CDeleteOrder_2.mqh и OOP_sDeleteOrders_3.mq5, можно смело удалять. Файлы OOP_CDeleteOrder_2.mqh и OOP_sDeleteOrders_3.mq5 вполне могут пригодиться в практической деятельности.