English Русский Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
面向对象编程基础

面向对象编程基础

MetaTrader 5示例 | 6 十一月 2013, 12:31
3 693 0
Dmitry Fedoseev
Dmitry Fedoseev

简介

我们可以假定尝试开始学习面向对象编程(OOP)的任何人,首先就碰到了多态性封装性重载继承性这些词汇。也许有人会查看一些现成的类,并尝试理解那些多态性或封装性究竟在哪里……很可能大多数人的 OOP 学习历程就终结于此了。

而实际上,一切都比看上去要简单得多。要使用 OOP,您无需明白那些词汇是什么意思 - 您只要使用 OOP 功能就好,甚至都不需要知道它们都叫什么。当然了,我还是希望本文的每一位读者都能够在知道如何使用 OOP 的同时,也清楚这些词汇的含义。

 

创建函数库

OOP 的第一次、也是最为简单的应用,就是创建您频繁使用函数的自用库。当然,您也可以简单地将这些函数存储于一个包含文件 (mqh) 中。在您确实需要某函数时,只需纳入一份文件再调用此函数。但是,如果您的程序足够长,您可能就会集中大量的函数,这样一来,就很难记住它们的名称和用途。

您可以收集不同文件中的函数,并根据用途将其分成多个类别。比如说,使用数组的函数、使用字符串的函数、计算订单的函数等等。上一句话中的“类别”一词可替换为“类”。意义相同,却更接近我们的主题 - 面向对象编程。

所以,可将函数划分为一个一个的类:使用数组的函数类、使用字符串的函数类、计算订单的函数类等等。因为“类”是 OOP 的基本概念,所以这个词更靠近我们的主题。关于什么是“编程中的类”,您可以检索各种各样的参考书目、字典和百科全书(比如 Wikipedia)。

在面向对象编程领域,类是一种用作编程模型以创建自身实例的结构。

乍听起来,可能觉得和“多态性”、“封装性”之类的词汇差不多。而这次我们所说的“类”,却是指一系列的函数和变量。在使用类创建库的例子中 - 一系列函数和变量按被处理数据的类型或按被处理对象的类型分组:数组、字符串、订单。

 

程序中的程序

曾存在(且未来也会有)大量表单的类似问题 - 如何由“EA 交易”调用一个脚本?虽然说避免使用第三方工具,但此任务还是会通过在“EA 交易”代码中置入脚本代码来完成。实际上,这并不难,但是脚本可能会采用与“EA 交易”相同的变量与函数名称,您也因此需要调整脚本代码。这种改动也并不复杂,只是可能数量庞大。

如果能够将此脚本作为一个独立的程序调用,那该多好!只要您将此脚本作为一个类进行编程,之后再使用这个类,就可能实现。工作量会因短短的几行代码而增加。在这种情况下,类会按照被处理数据的用途(而不是其类型)来组合函数。比如说:删除挂单的类、开仓类或下订单类、使用图形的类等等

类的一项重要功能,就是与其所处的空间区分开来。类就像是操作系统中运行的一个程序:多个程序可同时运行,但是自我运行,彼此独立。因此,类可被称作“程序中的程序”,因为它与其所处的空间区分开来。

 

类的外观和感觉

类创建开始于class一词,接着是类名称,然后才是放入大括号内的整体类代码:

class CName 
  {
   // 这里是类的入口代码
  };
注意! 千万不要忘记在右大括号后加上一个分号。

 

可见与隐藏(封装)

如果您取任何程序,我们都知道其中会包含大量的函数。这些函数可被划分为两类:主函数与辅函数。主函数是指真正构成一个程序的函数。而这些函数可能又需要许许多多用户无需了解的其它函数。比如说,想在客户端打开某头寸的交易者,则需要打开New Order(新建订单)对话框,输入交易量、Stop Loss(止损)和Take Profit(获利)值,再点击 "Buy" (买)或 "Sell" (卖)。

但是在点击按钮与开仓之间究竟发生了什么 - 就只有终端开发人员能够提供确定的答案了。我们可以假定终端执行了大量的动作:检查持仓量,检查 Stop Loss 值与 Take Profit 值,检查网络连接等。有许许多多的流程被隐藏,或者说,被封装。同样,您可以将某个类中的代码分成多个段(函数与变量) - 使用类时,其中某些可用,某些会被隐藏。

利用下述关键词定义封装等级:private(私有)protected(受保护)public(公用)protectedprivate 之间的区别我们稍后再想,我们首先来看看关键词 privatepublic。因此,一个简单的类模板会采用下述形式:

class CName 
  {
private:
   // 变量和函数仅在类内部可用
public:
   // 变量和函数在类外部可用
  };
想充分利用 OOP,这就足够了。不再直接于“EA 交易”(脚本或指标)中编写您的代码,而是首先创建一个类,然后再于此类中编写一切内容。接下来,我们再根据一个实例研究 privatepublic 分区间的差异。

 

创建库的示例

上面提供的类模板可用于创建一个函数库。我们创建一个类以使用数组。随着数组的使用而产生的最为常见的任务 - 就是向数组添加一个新元素和添加一个新元素,前提是这个带有指定值的元素并不存在于该数组中。

我们将向数组添加一个元素的函数命名为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;

首先是类的名称,然后是引用此实例的指针名称。载入后,类变成一个对象。想要使用某对象的任何函数,则编写指针名称、dot,之后是函数名称。键入dot后,就会有一个类函数的下拉式列表打开(图 1)。

图 1. 函数列表
图 1. 函数列表

多亏有了下拉列表,也就无需记忆那些函数名称了 - 您可以浏览该名称列表,并记住函数的用途。使用类的最大好处,就是能够创建库,而不仅仅是单纯地从文件中收集函数。

在收集函数的例子中,当您键入函数名称的前几个字母时,下拉列表就会显示所有包含库中的所有函数,而当您使用类时 - 则只会显示指定的关联函数。还要注意 Find() 函数并未列出 - 这就是 privatepublic 分区的差别所在。此函数于 private 分区中编写,因此不可用。

 

为不同的数据类型制作一个通用库(重载)

此时,我们的库中包含仅使用 int 类型数组的函数。除 int 类型数组之外,我们可能还需要将库函数应用于下述数组类型:uintlongulong 等。至于其它数据类型的数组,我们则必须编写其各自的函数。但是,您无需赋予这些函数其它名称 - 将会根据传递参数或参数组的类型自动选择正确的函数(本例是根据参数的类型)。我们利用使用 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);
        }
     }
  };
现在,使用同一个名称,我们却得到了不同的函数性。上述函数被称为重载函数,因为一个名称利用一个以上的函数性载入,亦即重载。

此示例载于本文随附的 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;
            // Check symbol/s61>
            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() 函数的结果,由此通知脚本结果。

现在,此脚本的所有基本函数均被设计为位于某独立文件中的一个类,所以您可以通过任何其它程序(“EA 交易”或脚本)使用此类,即,由“EA 交易”调用此脚本。

 

一些自动学(构造函数与析构函数)

程序运行可划分为三个阶段:启动程序、工作过程及其工作的完成。这种划分的重要性显而易见:程序启动时会自行准备(比如载入并设置要使用的参数),程序结束时其必须执行一次 "clean up" (清理,比如移除图表中的图形对象)。

为区分这些阶段,“EA 交易”与指标都有专用函数:OnInit()(启动时运行)和OnDeinit() (关闭时运行)。类拥有类似功能:您可以添加会在类载入和类卸载时自动执行的函数。此类函数被称为“构造函数”和“析构函数”。向类添加一个构造函数即指添加一个与类名称完全相同的函数。想要添加一个析构函数 - 做法与构造函数完全相同,只是函数名称以波浪符 "~" 开始。

一个演示构造函数和析构函数的脚本:

// Class
class CName 
  {
public:
   // 类
                     CName() { Alert("构造函数"); }
   // 析构函数
                    ~CName() { Alert("析构器"); }

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

// 加载类
CName cname;
//+------------------------------------------------------------------
//|                                                                  |
//+------------------------------------------------------------------
void OnStart() 
  {
// 暂停
   cname.Sleep();
  }

此类实际上只有一个可暂停 3 秒钟的 Sleep() 函数。当您运行此脚本时,就会出现一个带有 "Constructor" (构造函数)信息的警报窗口,3 秒钟后,又会出现一个带有 "Destructor" (析构函数)信息的警报窗口。然而事实却是 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 分区中,以允许类重新初始化。使用此程序(“EA 交易”)时,在一种情况下,您可能只需要移除 Stop (停止)订单;而在其它情况下,则只需要移除 Limit (限制)订单。想完成此操作,您可以利用不同的参数调用 Init() 函数,以令 Delete() 函数删除某不同的订单组。

此示例载于本文随附的 OOP_CDeleteOrder_2.mqh 和 OOP_sDeleteOrders_3.mq5 文件中。

 

使用一个类中的多个实例

正如此前章节中提到的,根据初始化期间的参数设定,相同的类可以执行不同的动作。如果能知道您的类的用途,您就可以省略类的重新初始化。想完成此操作,您要载入一些带有不同参数的实例。

比如说,大家都知道,我们的“EA 交易”运行时,在某些情况下我们需要删除 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);

现在,如果您想删除购入挂单,请使用一个类的一个实例:

DeleteBuy.Delete();

如果您想删除卖出挂单 - 则使用另一个实例:

DeleteSell.Delete();

 

对象数组

当程序运行时,您不一定总是能确切地掌握您将需要多少类实例。这种情况下,您可以创建一个类实例数组(对象)。我们就以带有构造函数和析构函数的类为例,研究一下这个对象数组。对此类进行少许改动,我们将参数传递至构造函数,这样我们就能监控此类的每个实例了:

// Class
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();

检查脚本。运行后看到有十个构造函数,却没有析构函数。如果您使用动态指针,则类不会在程序终止时自动卸载。此外,您还可以在 "Experts" 选项卡上看到有关内存泄漏的信息。您应手动删除对象:

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

现在,在脚本的末尾处有十个析构函数运行,且无错误信息。

此示例载于本文随附的 OOP_sConstDestr_2.mq5 文件中。

 

利用 OOP 修改程序逻辑(虚函数,多态性)

多态性 - 很可能是最吸引人且最重大的 OOP 功能,可允许您控制程序的逻辑。它会使用一个带有虚函数和多个子类的基类。一个类可以采用由子类定义的多种形式。

举个简单的例子 - 两个值的对比。可以有五种版本的对比:大于 (>)、小于 (<)、大于等于 (>=)、小于等于 (<=)、等于 (==)。

创建一个带虚函数的基类。虚函数 - 与常规函数完全相同,只是其声明以 virtual 一词开始:

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

虚函数没有代码。它是一种可对接各种装置的连接器。根据装置的类型,它会执行不同的动作。

创建 5 个子类:

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

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 文件中。

 

有关封装的更多内容(私有、受保护、公用)

现在,关于 public 分区已经十分明朗 - 其包含类用户务必可见的函数与变量(我们所说的用户是指利用现成类缩写程序的程序员。)从类用户的角度来看,protectedprivate 分区之间没有区别 - 上述分区中的函数和变量不适用于用户:

//+------------------------------------------------------------------
//|   受保护的关键字的类                               |
//+------------------------------------------------------------------
class CName1
  {
protected:
   int ProtectedFunc(int aArg)
     {
      return(aArg);
     }
public:
   int PublicFunction(int aArg)
     {
      return(ProtectedFunc(aArg));
     }
  };
//+------------------------------------------------------------------
//|   类的私有关键字                                 |
//+------------------------------------------------------------------
class CName2
  {
private:
   int PrivateFunc(int aArg)
     {
      return(aArg);
     }
public:
   int PublicFunction(int aArg)
     {
      return(PrivateFunc(aArg));
     }
  };

CName1 c1; // 加载保护类
CName2 c2; // 加载私有类
本例中有两个类:CName1 和 CName2。每个类有两个函数:一个位于 public 分区,另一个则位于 protected 分区(类 CName1)或位于 private 分区(类 CName2)。两个类都只有一个函数来源于 public 分区的函数下拉列表(图 2 与图 3)。

图 2. CName1 类函数
图 2. CName1 类函数

图 3. CName2 类函数
图 3. CName2 类函数

此示例载于本文随附的 OOP_sProtPriv_1.mq5 文件中。

privateprotected 分区决定基类函数对其子类的可见性:

//+------------------------------------------------------------------
//|   基本类                                                  |
//+------------------------------------------------------------------
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());
      // 如果您未注释此行和注释前一个, 将会遇到编译错误,返回(PrivateFunc()); 
      // 返回(PrivateFunc()); 
     }
  };

本例中,我们有名为 CBase 的基类和名为 Class 的子类。尝试从子类调用位于protectedprivate分区中的基类函数。如果您从protected分区调用函数,一切编译和运行都会正常进行。如果您从private分区调用函数,则会出现一个编译器错误(不能调用私有成员函数)。也就是说,通过 private 分区的函数对子类不可见。

protected分区只会保护来源于类用户的函数,而private分区亦会保护来源于子类的函数。来源于子类的基类函数(位于不同分区)的可见性,请见图 4。

图 4. 来源于子类的基类函数的可见性
图 4. 来源于子类的基类函数的可见性
蓝色箭头 - 函数可用;灰色 - 不可用。

此示例载于本文随附的 OOP_sProtPriv_2.mq5 文件中。

 

默认虚函数与继承性

并非基类中所有的虚函数都必须在子类中拥有对应函数。如果某子类拥有同名函数 - 则会使用这个函数;如果没有 - 则会由基类虚函数运行代码。结合示例研究一下。

//+------------------------------------------------------------------
//|   基本类                                                  |
//+------------------------------------------------------------------
class CBase
  {
public:
   virtual string Function()
     {
      string str="";
      str="Function ";
      str=str+"of base ";
      str=str+"class";
      return(str);
     }
  };
//+------------------------------------------------------------------
//|   子类1                                                  |
//+------------------------------------------------------------------
class Class1: public CBase
  {
public:
   string Function()
     {
      string str="";
      str="Function ";
      str=str+"of child ";
      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("Class 1");
     }
  };
//+------------------------------------------------------------------
//|   子类2                                                  |
//+------------------------------------------------------------------
class Class2: public CBase
  {
public:
   string Function()
     {
      return("Class 2");
     }
  };

Class1 c1; // 加载类 1
Class2 c2; // 加载类 2
//+------------------------------------------------------------------
//|   处理对象的函数                                    |
//+------------------------------------------------------------------
void Function(CBase  &c)
  {
   Alert(c.Function());
  }
//+------------------------------------------------------------------
//|                                                                  |
//+------------------------------------------------------------------
void OnStart()
  {
// 处理对象使用一个函数.
   Function(c1);
   Function(c2);
  }
此示例载于本文随附的 OOP_sFunc_1.mq5 文件中。

 

函数与方法,变量与属性

到目前为止,我们在本文中都是使用“函数”一词。但在 OOP 中,程序员通常都是使用“方法”一词,而非“函数”。如果您是从内部、从编写类的程序员的角度来看类,所有的函数仍然是函数。但如果您是从使用现成类的程序员的角度来看类,则位于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 文件中。

 

数据结构

数据结构与类相似,但要简单一点点。虽然您也可以换用这种说法:类与数据结构相似,但是要更加复杂。差别在于数据结构可能只包含变量。就这一点而言,已经没有必要将其划分为 publicprivateprotected 分区了。结构的所有内容已经位于 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;
   };

本例中,想要从作为结构 2 一部分的结构 1 中调用变量,您必须要用到两个点:

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_CLibArray_1.mqh - 包含文件,应置于 MQL5/Include 文件夹。使用类创建库的示例。protectedprivate 关键词。重载。
  • OOP_CLibArray_2.mqh - 包含文件,应置于 MQL5/Include 文件夹。将类函数置于类外的示例。
  • OOP_sDeleteOrders_1.mq5 - 脚本,应置于 MQL5/Scripts 文件夹。删除挂单的简单脚本。
  • OOP_CDeleteOrder_1.mqh - 包含文件,应置于 MQL5/Include 文件夹。将 OOP_sDeleteOrders_1 脚本转换为类的示例。
  • OOP_sDeleteOrders_2.mq5 - 包含文件,应置于 MQL5/Scripts 文件夹。利用类删除订单的示例。取自 OOP_CDeleteOrder_1.mqh 文件(通过 Init() 函数设置参数)。
  • OOP_sConstDestr_1.mq5 - 脚本,应置于 MQL5/Scripts 文件夹。构造函数与析构函数示范。
  • OOP_CDeleteOrder_2.mqh - 包含文件,应置于 MQL5/Include 文件夹。利用构造函数删除订单及通过构造函数传递参数的类。
  • OOP_sDeleteOrders_3.mq5 - 脚本,应置于 MQL5/Scripts 文件夹。利用类删除订单的示例。取自 OOP_CDeleteOrder_2.mqh 文件(通过构造函数设置参数)。
  • OOP_sConstDestr_2.mq5 - 脚本,应置于 MQL5/Scripts 文件夹。将类载入数组的示例。
  • OOP_sVariant_1.mq5 - 脚本,应置于 MQL5/Scripts 文件夹。含子类基类的示例。虚函数,多态性。
  • OOP_sProtPriv_1.mq5 - 脚本,应置于 MQL5/Scripts 文件夹。protectedprivate 关键词在使用类时的同一性示例。
  • OOP_sProtPriv_2.mq5 - 脚本,应置于 MQL5/Scripts 文件夹。protectedprivate 关键词对子类的影响示例。
  • OOP_sDefaultVirtual_1.mq5 - 脚本,应置于 MQL5/Scripts 文件夹。子类没有与基类虚函数对应的函数的示例。
  • OOP_sFunc_1.mq5 - 脚本,应置于 MQL5/Scripts 文件夹。于某函数中使用对象的示例。
  • OOP_sMethodsAndProperties.mq5 - 脚本,应置于 MQL5/Scripts 文件夹。属性示例。
  • OOP_Struct.mq5 - 脚本,应置于 MQL5/Scripts 文件夹。结构示例。

上述文件中,除 OOP_CDeleteOrder_2.mqh 和 OOP_sDeleteOrders_3.mq5 之外,均可于体验后删除。OOP_CDeleteOrder_2.mqh 和 OOP_sDeleteOrders_3.mq5 两个文件可能会在实践编程时用到。

本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/351

面向对象法建立多时间表及多货币面板 面向对象法建立多时间表及多货币面板
本文讲述如何利用面向对象编程创建 MetaTrader 5 多时间表与多货币面板。主要目标在于建立一个可用于显示多种不同类型数据(比如价格、价格变动、指标值或自定义买/卖条件)、且无需修改面板本身代码的通用面板。
利用MQL5创建您自己的图形面板 利用MQL5创建您自己的图形面板
MQL5程序的可用性,是由其丰富的功能性和制作精细的图形用户界面所决定的。视觉感知有时比快速且稳定的运行更加重要。根据标准库类,您可以自行创建显示面板,以下即逐步操作指南。
6 步创建您自己的交易机器人! 6 步创建您自己的交易机器人!
如果您不清楚交易类如何构造,而且一看到面向对象编程之类的词就害怕,那么,本文正适合您。实际上,那些编写您自己的交易信号模块的细节,您无需知道。只需遵循几条简单法则即可。MQL5 向导会完成所有其余工作,而您则会得到一个即用型的交易机器人!
自定义图形控件。第 3 部分 表单 自定义图形控件。第 3 部分 表单
专门讲解图形控件的有三篇文章,此为最后一篇。其中涵盖主要图形界面组件的创建 - 表单 - 及其同其它控件的搭配使用。除表单类之外,CFrame、CButton、CLabel 类亦已添加到此控件库。