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

Инкапсуляция, класс, объект

В разделе «Работа со временем» рассматривался способ ограничения работы эксперта по времени суток. Для решения данной задачи требовалось перевести время суток заданное часами и минутами в секунды, прошедшие от начала суток:

int StartTime=3600*StartHour+60*StartMinute; int EndTime=3600*EndHour+60*EndMinute;

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

bool TradeTime(int StartHour, int StartMinute, int EndHour, int EndMinute){ int StartTime=3600*StartHour+60*StartMinute;
int EndTime=3600*EndHour+60*EndMinute; int CurTime=(int)(TimeCurrent()%86400); if(StartTime<EndTime){
return(CurTime>=StartTime && CurTime<EndTime);
}
else{
return(CurTime>=StartTime || CurTime<EndTime);
}
}

Примеры кода, относящиеся к данной теме, располагаются в папке «018 OOP», а этот и следующий код располагается в скрипте с именем «001 Class».

При каждом вызове функции TradeTime() выполняется один и тот же расчет переменных StartTime и EndTime, хотя очевидно, что их значение не будет меняться в процессе работы эксперта. Было бы достаточно произвести их вычисление один раз – на запуске эксперт. Значит надо объявить две глобальных переменных, и при инициализации эксперта выполнить их вычисление, а потом еще вызывать функцию проверки времени где-то в другом месте кода.

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

Основное понятие ООП – это класс. Класс – это объединение функций и переменных, предназначенных для решения одной задачи или нескольких сходных задач. Вместо термина «объединение» в ООП чаще используется термин «инкапсуляция» (от латинского in capsula – помещение в капсулу). Второе основное понятие в ООП – это объект. Сам по себе класс – это просто описание. Это описание представляет собой обычный программный код, но он сам по себе не выполняется в процессе работы программы. Для того что бы использовать этот код, надо

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

Описание класса начинается с ключевого слова «class», за которым следует имя класса, затем, между фигурных скобок располагаются все переменные и функции этого класса:

class CTradeTime{
// здесь переменные и функции этого класса
};

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

Поля, методы

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

class CTradeTime{ protected:
// скрытые поля и методы
public:
// доступные поля и методы
};

Теперь добавляем поля и методы в соответствии с решением поставленной задачи. В секцию protected добавляем переменные StartTime и EndTime. В секцию public добавляем метод для вычисления значений этих переменных, который будет вызваться один раз на запуске эксперта, назовем его Init(). Еще один метод в секции public будет использоваться непосредственно для проверки времени, его назовем Check(). В итоге получаем такой класс:

class CTradeTime{ protected:
int StartTime; int EndTime;
public:
void Init(int StartHour, int StartMinute, int EndHour, int EndMinute){ StartTime=3600*StartHour+60*StartMinute; EndTime=3600*EndHour+60*EndMinute;
}
bool Check(){
int CurTime=(int)(TimeCurrent()%86400);

if(StartTime<EndTime){
return(CurTime>=StartTime && CurTime<EndTime);
}
else{
return(CurTime>=StartTime || CurTime<EndTime);
}
}
};

Методы могут находиться как внутри класса (как в примере выше), так и вне его. В этом случае внутри класса необходимо записать сигнатуру метода, а перед именем метода, необходимо поставить два двоеточия и указать имя класса:

class CD{
public:
void fun1();
int fun2(int arg);
};

void CD::fun1(void){
//…
}

int CD::fun2(int arg){
//… return(arg);
}

Создание объекта

Прежде чем использовать класс, надо создать объект. Существует два способа создания объекта: автоматический и динамический. Автоматическое создание объекта выполняется точно так же, как объявляется переменная: имя класса является типом, за ним следует имя объекта:

CTradeTime tt;

В зависимости от цели объект может создаваться в общей секции (быть глобальным) и непосредственно в функции (быть локальным) – так же как с переменными. Создание глобального объекта может выполняться только после описания класса, то есть вышеприведенная строка кода располагается после описания класса CTradeTime.

В функции OnStart() наберите имя объекта tt и добавьте точку. Сразу после постановки точки должен открыться список доступных методов объекта (рис. 70):

Список доступных метолов класса tt

Рис. 70. Список доступных метолов класса tt

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

int StartHour=14; int StartMinute=0; int EndHour=16; int EndMinute=0;

tt.Init(StartHour,StartMinute,EndHour,EndMinute);

Теперь проверка, можно ли сейчас отрывать позицию:

if(tt.Check()){
Alert("Торговля разрешена");
}
else{
Alert("Торговля запрещена");
}

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

CTradeTime* ttd;

Затем, посредством оператора new, создается объект:

ttd=new CTradeTime();

После этого объект можно использовать точно так же, как объект созданный автоматически:

ttd.Init(StartHour,StartMinute,EndHour,EndMinute);

bool CanTrade=ttd.Check(); Alert("CanTrade=",CanTrade);

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

delete(ttd);

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

«Эксперты» появится несколько сообщений, например, такие: «1 undeleted objects left» (остался один не удаленный объект), «1 object of type CTradeTime left» (остался один объект типа CTradeTime), 28 bytes of leaked memory (утечка памяти 28 байтов).

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

Обращение к своим полям и методам внутри класса может выполняться непосредственно – как выполнялось обращение к полям StartTime и EndTime в классе CTradeTime, а может выполняться посредством ключевого слова this:

class CA{
protected:

int var1; void fun1(){
};
public:
void fun2(){ this.var1=0; this.fun1();
}
};

Существует еще один способ автоматического создания объекта, так называемое создание временного объекта. Класс вызывается как функция – записывается имя класса, а в конце ставятся круглые скобки. Для того, чтобы компилятор отличил такую запись от вызова функции, ее всю необходимо заключить в круглые скобки, затем ставится точка и вызывается какой-нибудь метод.

Класс:

class CB{
public:
void fun(){
Alert("Класс CB, метод fun()");/
}
};

Создание объекта и вызов метода:

(CB()).fun();

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

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

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

Следующий код располагается в файле «002 Constructor». Пример класса с конструктором:

class CA{
public:
CA(){
Alert("Класс CA, конструктор");
}
};

Создание объекта этого класса:

CA a;

При выполнении этого кода откроется окно с сообщением «Класс CA, конструктор». Значит в этом методе – в конструкторе и следует выполнять присвоение переменным начальных значений:

class CB{
protected:
int val; public:
CB(){
val=0;
}
};

Класс может иметь еще один специальный метод – деструктор. Этот метод выполняется, когда объект завершает свое существование. Если объект создан автоматически, то он уничтожается при завершении работы программы, а если динамически, то при вызове оператора delete. Деструктор создается точно так же, как конструктор – это метод с именем, соответствующим имени класса, но у деструктора в начало добавляется знак «~».

Класс с конструктором и деструктором:

class CD{
public:
CD(){
Alert( FUNCSIG );
}
~CD(){
Alert( FUNCSIG );
}
};

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

Объявим переменную-указатель:

CB* d;

В функции OnStart() создаем объект и уничтожаем:

d=new CD(); delete d;

В результате работы этого кода будет выведено два сообщения: «CD::CD()» – сообщение конструктора и «CD::~CD()» – сообщение деструктора.

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

class CE{
protected:
int val1;
const int val2;

public:
CE():val1(0),val2(25){
}
};

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

class CF{
protected:
int val; public:
CF(){
val=1;
}
int F(int arg) const{ return(arg+val);
}
};

Если же в методе F() попробовать присвоить переменной val значение, при компиляции возникнет ошибка «member of the constant object cannot be modified» (член константного объекта не может быть изменен).

Если же переменная объявляется в методе, ее можно инициализировать:

class CG{
protected: public:
void F() const{ int tmp=25;
}
};

Если класс имеет конструктор, то при вызове класса как функции (рассмотрено в конце предыдущего раздела), сначала вызывается конструктор, даже если не вызывать никакие методы. Также, если у класса есть деструктор, он будет вызван при уничтожении объекта. Класс:

class CH{
public:
void CH(){
Alert( FUNCSIG );
}
void ~CH(){
Alert( FUNCSIG );
}
};

Создание объекта:

(CH());

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

Статические члены

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

class CA{
protected:
static int val; public:
};
int CA::val=0;

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

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

class CB{
protected:
int VarA;
static int VarB; public:
CB(){
VarA=0;
}
void setVarA(int val){ VarA=val;
}
int getVarA(){ return(VarA);
}
static void setVarB(int val){ VarB=val;
}
static int getVarB(){ return(VarB);
}
};

Метод setVarA() – установка значения полю VarA, метод getVarA() – получение значения поля VarA, метод setVarB() – установка значения полю VarB, метод getVarB() – получение значения поля VarA. Чтобы этот код откомпилировался без ошибок, обязательно надо инициализировать статическую переменную:

int CB::VarB=0;

Создадим два объекта этого класса:

CB b1;
CB b2;

Установим всем полям этих классов разные значения:

b1.setVarA(1); b2.setVarA(2); b1.setVarB(3); b2.setVarB(4);

Посмотрим их значения:

Alert(b1.getVarA()," ",b2.getVarA()," ",b1.getVarB()," ",b2.getVarB());

Результат: «1 2 4 4» – как видим, значение статического поля у каждого класса одно и тоже, и оно равно значению, которое было присвоено ему последним. Значит, статическое поле является общим для всех объектов. Статические методы тоже являются общими для всех объектов. Более того, можно сказать, что они вообще существуют независимо от объектов. Можно не создавать объекты, но вызвать статически методы. В этом случае вызов метода выполняется через имя класса, а не объекта, а вместо точки используется два двоеточия. Вызов статического метода для установки значения статическому полю:

CB::setVarB(5);

Теперь получим значение статического поля напрямую и от объектов: Alert(CB::getVarB(),» «,b1.getVarB(),» «,b2.getVarB()); Результат: «5 5 5» – как опять видим, статическое поле является общим.

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

class CD{
protected:
public:
static int Plus(int a,int b){ return(a+b);
}
static int Minus(int a,int b){ return(a-b);
}
};

Используется такой класс без создания объекта:

Alert(CD::Plus(1,2));

Результат: «3».

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

class CE{
protected: public:
int Count(){
static int cnt=0; cnt++; return(cnt);
}
};

В методе Count() этого класса объявлена статическая переменная cnt. При вызове этого метода выполняется увеличение значения переменной cnt и возвращение ее нового значения. Попробуем создать два счетчика на основе этого класса. Код в функции OnStart():

CE c1;
CE c2;/
int r1=c1.Count(); int r2=c2.Count();
Alert("r1=",r1,", r2=",r2);

Результат: «r1=1, r2=2» – как видим, не получилось создать два независимых счетчика. Поэтому, если нужно, чтобы метод сохранял какие-то данные, следует использовать обычный член класса, но не статическую переменную в методе:

class CF{
protected:
int cnt; public:
CF(){
cnt=0;
}
int Count(){ cnt++; return(cnt);
}
};

В функции OnStart():

CF c21;
CF c22;
r1=c21.Count(); r2=c22.Count(); Alert("r21=",r1,", r22=",r2);

Результат: «r1=1, r2=1» – теперь счетчики работают независимо.

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

class CG{
protected:
int ar[]; public:

CG(){
static int tmp[]={1,2,3}; ArrayCopy(ar,tmp);
}
};

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

Конструктор с параметрами

Конструктор класса может иметь параметры. А за счет возможностей перегрузки класс может иметь несколько конструкторов, что делает использование этих классов удобным и гибким:

class CA{
public:
CA(){
Alert( FUNCSIG );
}
CA(int arg){
Alert( FUNCSIG );
}
};

Код этого класса располагается в скрипте с именем «004 Constructor2». Автоматическое создание объекта этого класса может выполняться как обычно: CA a1;Или так:

CA a1();

В этих случаях срабатывает конструктор без параметров. А можно создать объект вот так:

CA a2(1);

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

CA * a3=new CA();
CA * a4=new CA(1);

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

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

class CB{
protected:
int var; public:
CB(){
}
CB(int arg){ Init(arg);
}
void Init(int arg){ var=arg;
}
};

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

CB b1();

Затем, если это советник, то в функции его инициализации вызывается метод Init():

b1.Init(1);

При динамическом создании на глобальном уровне объявляется указатель:

CB* b2;

Затем, при создании объекта, используется конструктор с параметрами:

b2=new CB(1);

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

Примеры кода, рассматриваемого в этом разделе, находятся в скрипте с именем «005 Inheritance».

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

class CF{
protected:
int cnt; public:
CF(){
cnt=0;
}
int Count(){ cnt++; return(cnt);
}
};

Вдруг появилась необходимость выполнять сброс этого счетчика. Задача решается добавление одного метода:

void Reset(){ cnt=0;
}

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

Значит, пишем свой класс, но не отдельный, а являющийся расширением (наследником) класса CF. Для этого после имени класса ставится двоеточие, слово public, а затем имя родительского класса. В этом классе создаем всего лишь один метод, обнуляющий переменную cnt:

class CG:public CF{ public:
void Reset(){ cnt=0;
}
};

Создаем объект этого класса:

CG c;

Если раскрыть список методов этого объекта (набрать имя объекта и поставить точку), то в нем можно увидеть методы: СD(), Count() и Reset(). То есть объект CG наследовал все свойства объекта CF.

Заметьте, в классе CG нет переменной cnt, она в классе CF, но с ней можно работать из класса CG, потому что она находится в секции protected. Если бы она находилась в секции private, к неё не было бы доступа – вот поэтому было рекомендовано использовать только секцию protected. Секция private может быть полезна при командной разработке, когда программистам, пишущим производные классы, надо гарантировано запретить доступ к некоторым членам базовых классов.

Переопределение методов

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

class CBase{ public:
virtual int fun1(int a,int b){ return(a+b);
}
int fun2(int a,int b){ return(a*b);
}
};

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

class CChild:public CBase{ public:
virtual int fun1(int a,int b){ return(a-b);

}
};

Проверяем:

CChild c;
Alert(c.fun1(2,3));

Результат: «-1» – как видим, использовался метод из производного класса. Примеры этого раздела находятся в скрипте с именем «006 Virtual».

Полиморфизм

Примеры кода, рассматриваемого в этом разделе, находятся в скрипте с именем «007 Polymorphism».

Допустим, есть базовый класс с одним методом:

class CBase{ protected: public:
void fun1(){
}
};

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

class CA:public CBase{ protected:
public:
void fun2(){

}
};

class CB:public CBase{ protected:
public:
void fun3(){

}
};

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

CA* a=new CA();
CB* b=new CB();

Однако это же самое можно сделать, используя указатель базового типа:

CBase* a2=new CA();
CBase* b2=new CB();

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

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

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

void Plant(int type,CBase* &object){ switch(type){
case 1:
object=new CA(); break;
case 2:
object=new CB(); break;
}
}

Впрочем, указатель можно и непосредственно функцией возвращать:

CBase* Plant(int type){ switch(type){
case 1:
return(new CA()); break;
case 2:
return(new CB()); break;
}
return(NULL);
}

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

Использование первого варианта функции Plant():

int type=1;
CBase* c;
Plant(type,c);

Использование второго варианта функции Plant():

type=2;
CBase* d;
d=Plant(type);

Указатели на объекты c и d имеют тип CBase. В классе CBase имеется только один метод fun1() – с его вызовом нет проблем:

c.fun1();
d.fun1();

А вот вызов метода fun2(), принадлежащего классу CA, и метода fun3(), принадлежащего классу

CB так просто не получится сделать. Необходимо выполнить приведение типов:

((CA*)c).fun2();
((CB*)d).fun3();

Обратите внимание, в круглые скобки помещен тип «(CA*)», это тип, к которому выполняется приведение указателя с, а потом вся конструкция помещается в круглые скобки, после чего ставится точка и указывается вызываемый метод. Так же и с указателем CB.

Можно поступить другим способом – использовать указатель соответствующего типа:

CA* c2=c; c2.fun2();

CB* d2=d; d2.fun3();

Композиция и агрегация

Примеры кода, рассматриваемого в этом разделе, находятся в скрипте с именем «008 Composition».

Если во вновь создаваемом классе необходимо воспользоваться возможностями другого класса, не всегда целесообразно использовать наследование и создавать производный класс. Необходимый класс можно включить в состав нового класса. Такой подход называется композицией. Допустим, есть класс CA, нужно воспользоваться его возможностями в классе CB. Класс CA:

class CA{
protected:
int var1; public:
CA(){
}
void fun(){
}
};

Класс CB:

class CB{
protected:
int var1;
CA a;
public:
CB(){
}
void fun1(){
}
void fun2(){ a.fun();
}
};

В классе CB в секции protected выполняется автоматическое создание объекта класса CА, а через метод fun2() выполняется доступ к его методу fun().

Если объект класса создавать в секции public, то всего его методы будут доступны без создания дополнительных метолов:

class CD{
protected:
int var1; public:
CA a;
CD(){
}
void fun1(){
}
};

Создание объекта класса CD:

CD d;

Доступ к его методам:

d.fun1();
d.a.fun();

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

class CE{
protected:
int var1;
CA* a;
public:
CE(){
}
void setA(CA* arg){ a=arg;
}
void fun2(){ a.fun();
}
};

Создание объектов:

CA a;
CE e;

Передача ссылки на объект класса CA в объект класса CE:

e.setA(GetPointer(a));

Вызов метода:

e.fun2();

Функция GetPointer() используется для получения указателя на объект, сорзданный автоматически. Передачу ссылки можно записать другим, более компактным образом:

e.setA(&a);

Если бы объект а создавался динамически, то не было бы необходимости использовать функцию

GetPointer() или добавлять амперсанд. Чуть позже об этом будет более подробно.

Опережающее объявление

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

class CG;

После этого в коде можно писать указатель на этот тип:

class CF{
protected:
CG* g; public:
void setG(CG* arg){ g=arg;
}
};

Второй класс:

class CG{
protected:
CF* f; public:
void setF(CF* arg){ f=arg;
}
};

Создание объектов:

CF f;
CG g;

Обмен ссылками:

f.setG(&g);
g.setF(&f);

Передача объекта в функцию или метод

Примеры кода, рассматриваемого в этом разделе, находятся в скрипте с именем «009 Parameters».

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

Класс:

class CA{
protected: public:

CA(){
}
void fun(){
}
};

Автоматическое создание объекта:

CA a1;

Объявление переменной-указателя:

CA* a2;

Динамическое создание объекта:

a2=new CA();

Функция, принимающая объект по ссылке:

void f1(CA &a){ a.fun();
}

В эту функция можно передать как объект a1, так и а2:

f1(a1);
f1(a2);

Функция может принимать ссылку на объект:

void f2(CA* a){ a.fun();
}

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

f2(&a1);
f2(a2);

В функцию можно передавать указатель по ссылке:

void f3(CA* &a){ a.fun();
}

В такую функцию можно передать только динамически созданный объект:

f3(a2);

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

CA* tmp=&a1; f3(tmp);

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

void f(CA &a){ f(&a);
}

void f(CA* a){
// работа с объектом
}

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

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

Примеры кода, рассматриваемого в этом разделе, находятся в скрипте с именем «010 Class2».

Иногда, для решения какой-то конкретной и узкой задачи, бывает нужно создание класса, используемого только совместно с другим классом. То есть, описание класса на глобальном уровне не целесообразно. Описание класса можно выполнить внутри другого класса. Создадим класс CB внутри класса CA:

class CA{
protected:
class CB{
protected:
public:
void fun(){
Alert( FUNCSIG );
}
};
CB b;
public:
void fun(){ b.fun();
}
};

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

class CD{
protected:
public:
} d;

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

Примеры кода, рассматриваемого в этом разделе, находятся в скрипте с именем «011 Array».

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

class CA{
protected:
public:
CA(){Alert( FUNCSIG );}
~CA(){Alert( FUNCSIG );}
} a[];

Установим массиву размер 2, потом сократим его до 1:

Alert("=1=");
ArrayResize(a,2);
Alert("=2=");
ArrayResize(a,1);
Alert("=3=");

В результате работы этого кода будут выведены сообщения: «=1=», «CA::CA()», «CA::CA()», «=2=», «CA::~CA()», «=3=», «CA::~CA()» – сначала два конструктора, потом один деструктор (при уменьшении размера массива). По завершению работы скрипта был вызван еще один деструктор.

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

Конструктор производного класса

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

Разберемся когда, в каком порядке и какие конструкторы срабатывают у базового и производного классов. За одно, посмотрим и на работу деструкторов. Примеры кода, рассматриваемого в этом разделе, находятся в скрипте с именем «012 Constructor2». Базовый класс с двумя конструкторами, один их них без параметров, второй с параметром:

class CBase{ protected:
public:
CBase(){
Alert( FUNCSIG );
}
CBase(int arg){
Alert( FUNCSIG );
}
~CBase(){
Alert( FUNCSIG );
}
};

Производный класс, тоже с двумя такими же конструкторами:

class CChild: public CBase{ protected:
public:
CChild(){

Alert( FUNCSIG );
}
CChild(int arg){ Alert( FUNCSIG );
}
~CChild(){
Alert( FUNCSIG );
}
};

В функции OnStart() создаем производный объект. Сначала без параметров, затем с параметром:

Alert("=1=");
CChild* c1=new CChild(); delete(c1);

Alert("=2=");
CChild* c2=new CChild(1); delete(c2);

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

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

Первое отличие класса от структуры в том, что по умолчанию область класса является закрытой, то есть представляет собой секцию private. А область структуры по умолчанию соответствует секции public. Второе и наиболее существенное отличие в том, что динамическое создание объектов работает только с классами. Со структурами работает только автоматическое создание. Наследование со структурами работает, но без виртуальных методов:

struct CBase{ int var1;
};

struct CChild: public CBase{ int var2;
};

CChild s;

В функции OnStart():

CChild s; s.var1=0; s.var2=1;

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

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

Перегрузка операторов

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

Наиболее наглядный пример для изучения перегрузки операторов, это арифметические действия над векторами или комплексными числами. Слишком глубоко в данную тему погружаться не будем, рассмотрим только сложение и вычитание. Один вектор определяется двумя числами – величинами проекций на ось X и на ось Y. При суммировании, для получения результирующего вектора необходимо выполнить сложение проекций отдельно по осям. Например, один вектор определяется парой чисел (1, 2), второй вектор определяется парой числе (5, 3). Для получения суммарного вектора складываем 1 и 5, а также 2 и 3, получаем (6, 5). Результат подобных действий подобен складыванию векторов силы на школьных уроках физики.

Примеры кода данного раздела располагаются в скрипте с именем «014 Operator». Структура, определяющая один вектор, включает в себя поля x и y:

struct SVector{ int x;
int y;
};

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

SVector operator+(SVector & a){ SVector tmp;
// действия над полями
return(tmp);
}

В итоге получаем такую структуру:

struct SVector{ int x;
int y;
SVector operator+(SVector & a){ SVector tmp;
tmp.x=x+a.x; tmp.y=y+a.y;

return(tmp);
}
};

В функции OnStrat() объявим три переменные с типом SVector. Первым двум присвоим значения, а третья будет использоваться для результата:

SVector v1,v2,v3; v1.x=1;
v1.y=2;

v2.x=5; v2.y=3;

Выполняем сложение и выводим результат:

v3=v1+v2;
Alert("1: ",v3.x," ",v3.y);

Результат: «1: 6 5».

Рассмотрим подробно, как происходит это сложение. В выражении «v3=v1+v2» часть «v1+» – это вызов метода «operator+» структуры v1. А «v2» – это параметр, передаваемый в вызываемый метод. Результат, возвращаемый методом «operator+» структуры v1 присваивается структуре v3. Как видим, по сути, это вызов метода, но записанный иначе.

Вообще можно перегрузить следующие операторы: «+» (сложение), «-» (вычитание), «/» (деление), «*» (умножение), «%» (остаток от деления). Могут быть перегружены операторы сравнения: «==» (равно), «!=» (не равно) , «<» (меньше), «>» (больше), «<=» (меньше или равно), «>=» (больше или равно). А также «&&» – логическое «и», «||» – логическое «или». В этом случае метод должен возвращать переменную типа bool. Впрочем, это не обязательно, поскольку при перегрузке оператора программист наделят оператор своим смыслом. Поэтому, оператор, например, «>» может означать вовсе не сравнение «больше-меньше», подразумевающее результат типа bool, а выполнение каких угодно других действий с каким угодно типом результата. Добавим в структуру метод для сравнения на равенство:

bool operator==(SVector & a){ return(x==a.x && a.y);
}

Так же могут быть перегружены знаки выполнения побитовых операций: «<<» (сдвиг влево), «>>» (сдвиг вправо), «&» (побитовое «и»), | (побитовое «или»), «^» (исключающее «или»). Для операции сдвига параметром метода может быть как целое число, так и структура, определяющая свою величину сдвига для каждого поля.

В другую категорию перегружаемых операторов входит знак присвоения «=», конкатенации («++» и «—»), а также присвоение с действиям («+=», «-=» и т.п). В этом случае в методе не нужна временная переменная, а результат действия сразу присваивается полям той структуры, у которой вызван метод. Метод для присвоения:

void operator=(SVector & a){ x=a.x;
y=a.y;

Вызов:

v1=v2;
Alert("3: ",v1.x," ",v1.y);

Результат: «3: 5 3».

Метод для присвоения со сложением:

void operator+=(SVector & a){ x+=a.x;
y+=a.y;
}

Вызов:

v1+=v2;
Alert("4: ",v1.x," ",v1.y);

Результат: «4: 10 6».

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

void operator++(int){ x++;
y++;
}

Вызов:

v1++;
Alert("5: ",v1.x," ",v1.y);

Результат: «5: 11 7».

Для перегрузки операторов «~» и «!» метод должен принимать параметр и возвращать его после изменения:

SVector operator~(){ SVector tmp; tmp.x=~x; tmp.y=~y; return(tmp);
}

Вызов:

v1=~v2;
Alert("5: ",v1.x," ",v1.y);

Результат: «5: -6 -4».

Еще один перегружаемый оператор – это квадратные скобки. При его использовании вызов метода может выглядеть как обращение к массиву. Метод:

void operator[](int i){ Alert("i=",i);

v1[1];

Результат: «i=1».

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

struct S1{
protected:
struct S0{
int i1;
int operator [](int i2){ return(i1*10+i2);
}
} s0;
public:
S0 operator [](int a){ s0.i1=a; return(s0);
}
};

Вызов:

int d=c[2][3];
Alert(d);

Результат» «23».

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

class C1{
public:
class C0{
public:
C1* parent; int i1;
int operator [](int i2){ return(parent.fun(i1,i2));
}
void setParent(C1* p){ parent=p;
}
} c0;
C1(){
c0.setParent(&this);
}
C0* operator [](int a){ c0.i1=a; return(&c0);
}
int fun(int i1,int i2){

return(i1*10+i2);
}
};

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

struct SVector2{ int x;
int y;
void set(SVector2 & a){ x=a.x;
y=a.y;
}
};

Использование:

SVector2 v21,v22; v21.x=7; v21.y=8;
v22.set(v21);

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

void add(SVector2 & a){ x+=a.x;
y+=a.y;
}

Вообще, следует избегать подхода, требующего создания временных структур для возврата их из методов и функций (как в примере с перегрузкой оператора «+»). С использованием временных структур метод получается более медленным, поскольку выполняются лишние действия по присвоению значений полям временной структуры, а потом их значения присваиваются полям другой структуры для результата. Лучше результат возвращать через параметр по ссылке.

Использование метода, возвращающего указатель, открывает возможность вызовов нескольких методов подряд у временных объектов. Для этого из каждого метода надо возвращать указатель на себя. Таким образом, после каждого вызова метода можно вызвать еще один метод и т.д. Класс:

class CArithmetic{ protected:
double result; public:
void CArithmetic(double arg){ result=arg;
}
CArithmetic * Plus(double arg){ result+=arg;
return(&this);
}
CArithmetic * Minus(double arg){ result-=arg;
return(&this);

}
CArithmetic * Multiply(double arg){ result*=arg;
return(&this);
}
CArithmetic * Divide(double arg){ result/=arg;
return(&this);
}
double Result(){ return(result);
}
};

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

double res=(CArithmetic(1)).Plus(2).Multiply(5).Minus(3).Divide(2).Result(); Alert("res=",res);

Результат: «res=6» – вычислено выражение ((1+2)*5-3)/2.

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

class CArithmetic2{ protected:
double result; public:
void CArithmetic2(double arg){ result=arg;
}
void Plus(double arg){ result+=arg;
}
void Minus(double arg){ result-=arg;
}
void Multiply(double arg){ result*=arg;
}
void Divide(double arg){ result/=arg;
}
double Result(){ return(result);
}
};

Функция:

double Res(CArithmetic2 & a){ a.Plus(2);

a.Multiply(5); a.Minus(3);
a.Divide(2); return(a.Result());
}

Вызов функции Res() из функции OnStart():

res=Res(CArithmetic2(1)); Alert("res2=",res);

Результат: «res2=6».