当前位置:首页 > 学习资料 > EA那些事 > 正文

面向对象的程序设计

2019-12-18 12:19:04

面向对象的程序设计简单介绍:面向对象的程序设计(OOP)主要是针对数据或者与数据密不可分行文的程序设计。数据和行为合起来称为类,对象就

面向对象的程序设计简单介绍:

面向对象的程序设计(OOP)主要是针对数据或者与数据密不可分行文的程序设计。数据和行为合起来称为类,对象就是类的实例。 面向对象处理组件:
  • 类型封装和扩展性
  • 继承机制
  • 多态性
  • 重载
  • 虚拟函数
OOP把计算当成行为建模。模型项是抽象计算代表的对象。假设我们想编写一个著名的游戏"Tetris"。我们必须学习如何用四个正方形随机组成的图形来建模。我们还需要调节图形下降的速度,定义图形旋转变换的操作。屏幕上图形移动受文档对象模型边框限制,这也需要建模。此外,消除填满的行,然后获得相应的得分。

 

因此,这个简单易懂的游戏需要创建几个模型-图形模型,文档对象模型,移动模型等等。所有这些模型都是抽象的,通过电脑运算表示。描述这些模型,抽象数据类型ADT(或者复杂数据类型)概念被使用。严格说来,DOM中“图形”运动模型并不是数据类型,但是它是使用“DOM”数据类型限制条件,“图形”数据类型全部操作。 对象是类变量。面向对象的程序设计允许您轻松创建使用ADT。面向对象程序设计使用继承机制。其优势是允许从用户已经定义的数据类型中获得衍生类型。 例如,创建俄罗斯方块图形,首先创建一个基本类图形很方便;代表所有可能图形的其他七个类可以从基本图形衍生出来。图形行为在基本类中就确定了,而执行每个独立图形的行为在衍生类中定义。 在OOP中对象对其行为负责。ADT开发人员包括描述相关对象期望的行为代码。实际上,对象对其行为负责,显著简化了对象用户程序设计的任务。 如果我们想要在屏幕上画一个图,我们需要知道中心点在哪里,以及如何画它们。如果独立的图形知道如何画自己,当使用这个图形时程序员可以发送“draw”消息。 MQL5语言类似于C++,也有ADT的封装机制。一方面,封装与内部特殊类型结合,另一方面,也与影响这类型对象的可接入外部函数连结。执行细节对于使用这个类型的程序很难达到。 OOP概念有一系列相关概念,包括以下内容:
  • 模拟真实世界操作
  • 用户定义数据类型的有效性
  • 隐藏执行细节
  • 通过继承机制重新使用代码的可能性
  • 执行期解释调用函数
 一些概念非常不明确,有一些很抽象,另一些又很大众化。
首先让我们来了解一下什么是对象?

 

        没错! 要了解面向对象我们肯定需要先知道对象到底是什么玩意儿。 关于对象的理解很简单,在我们的身边,每一种事物的存在都是一种对象。 总结为一句话也就是: 对象就是事物存在的实体。 下面举个简单的例子,比如人类就是一个对象,然而对象是有属性和方法的,那么身高,体重,年龄,姓名,性别这些是每个人都有的特征可以概括为属性,当然了我们还会思考,学习,这些行为相当于对象的方法。 不过,不同的对象有不同的行为  面向对象的特征

 

封装: 就是把属性私有化,提供公共方法访问私有对象。 举个简单的例子,我们去Apple店里买个iPhoneX,我们不需要知道它是怎么制造的,我们只需要知道它能用来打电话,上网和用来装B就行了。 对于程序设计,用户只需要知道类中某个方法实现了什么样的功能,需要该功能的时候直接去调用就行了,不用去关心方法内部的实现细节

 

继承: 当多个类具有相同的特征(属性)和行为(方法)时,可以将相同的部分抽取出来放到一个类中作为父类,其它类继承这个父类。 继承后子类自动拥有了父类的属性和方法,比如猫,狗,熊猫他们共同的特征都是动物,有颜色,会跑,会叫等特征。 我们可以把这些特征抽象成我一个Animal类(也就是父类)。 然而他们也有自己独特的特性,比如猫会抓老鼠,喵喵叫,熊猫有黑眼圈,能吃竹子,狗会汪汪。 于是我们就根据这些独特的特征分别抽象出来Cat,Dog,Panda类等。 他们拥有Animal类的一般属性和方法,也拥有自己特有的某些属性和方法。

 

但特别注意的是,父类的私有属性(private)和构造方法不能被继承。 另外子类可以写自己特有的属性和方法,目  的 是实现功能的扩展,子类也可以复写父类的方法,即方法的重写。 子类不能继承父类中访问权限private的成员变量和方法 

 

 

 

多态: 简单来说就是“一种定义,多种实现”。 同一类事物表现出多种形态。 Java语言中有方法重载和对象多态两种形式的多态

 

      方法重载: 在一个类中,允许多个方法使用同一个名字,但是方法的参数不同,完成的功能也不同

 

      对象多态: 子类对象可以与父类对象进行相互转换,而且根据其使用的子类的不同,完成的功能也不同

 

 

 

抽象: 抽象是从许多事物中,舍弃个表的,非本质的属性,抽取出共同的,本质的属性的过程。 例如教师,学生和工人,他们共同的特质是人类,既然是人类就有共同的属性: 性别,年龄,身高,体重等。 抽象的过程就是比较的过程,通过比较找出事物之间的共同属性,通过比较区分本质。

 

 

 

类与对象的关系

 

    所有的事物都可以看做是一个对象,是对象就具有一定的属性和功能,这些对象是可以建立起联系的,而且这些对象是由类来构造的。 类是具有属性和方法的一组对象的集合,对象是实际存在的该类事物的个体

 

   在面向对象中,类和对象是最基本和最重要的组成单元。 类实际上是表示一个客观世界某类群体的一些基本特征抽象,对象就表示一个个具体的东西,对象是以类模板创建的。

 

   举个简单的例子: 兰博基尼跑车,在工厂里首先要由设计师设计出一个汽车图纸,然后再根据图纸去生产兰博基尼,这样生产出来的每一辆跑车结构和功能都是一样的。 但是不同的款式有不同的特征,比如车的颜色,内部装饰,马力等。 在这个例子中,设计图纸就是一个类,它规定看跑车应该拥有的基本部件。 而根据这张图纸生产出来的每一辆跑车就是一个个实时存在的对象。 它们的初始状态是一模一样的,如果其中某一辆颜色,发动机重新改了之后并不影响其他的跑车。

 

 

 

stati修饰符:

 

       static关键字的中文含义是静态的意思。 使用static修饰的成员变量,常量,方法和代码分别称为静态变量,静态常量,静态方法,静态代码块,它们统称为静态成员。 静态成员归整个类所有,不依赖特定的实例,被类的所有实例所共享的。 只要被JVM加载就可以根据类名在全局数据区内找到

 

类的成员变量分为两种:

 

实例变量    (ps: 也就是没有被static关键字修饰的变量) 静态变量 两者之间的区别:

 

我们可以假定尝试开始学习面向对象编程(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(公用) 。 protected  与  private  之间的区别我们稍后再想,我们首先来看看关键词  private  与  public 。因此,一个简单的类模板会采用下述形式:

class CName
  {
private :
   // 变量和函数仅在类内部可用
public :
   // 变量和函数在类外部可用
  };

想充分利用 OOP,这就足够了。不再直接于“EA 交易”(脚本或指标)中编写您的代码,而是首先创建一个类,然后再于此类中编写一切内容。接下来,我们再根据一个实例研究  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;

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

现在,使用同一个名称,我们却得到了不同的函数性。上述函数被称为重载函数,因为一个名称利用一个以上的函数性载入,亦即重载。

此示例载于本文随附的 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  分区已经十分明朗 - 其包含类用户务必可见的函数与变量(我们所说的用户是指利用现成类缩写程序的程序员。)从类用户的角度来看, protected  与  private  分区之间没有区别 - 上述分区中的函数和变量不适用于用户:

//+------------------------------------------------------------------
//|   受保护的关键字的类                               |
//+------------------------------------------------------------------
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)。

 

 

此示例载于本文随附的 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());
       // 如果您未注释此行和注释前一个, 将会遇到编译错误,返回(PrivateFunc());
       // 返回(PrivateFunc());
     }
  };

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

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

 

此示例载于本文随附的 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  分区的所有函数都将可用。此即谓继承性。如果基类的函数是作为虚函数声明,则只要子类中存在同名函数,就用子类的函数将其替换


 

除子类中没有对应基类虚函数的函数的情况之外,子类可能拥有“额外”函数(基类中没有同名虚函数的函数)。如果您利用指针将类载入到子类类型,则上述函数可用。如果您利用指针将类载入到基类类型,则上述函数不可用


 

 

 

更多有关类载入

在您使用虚函数以及相应的基类和子类时,如果您知道应使用哪个子类,您就可以使用对应该子类的指针:

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. 一个列表中类的方法与属性

这些属性的使用方法与变量相同:

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;
};

本例中,想要从作为结构 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. 结构可纳入数组及其它结构。

关键词:

上一篇: ea:cci策略ea

下一篇: EA:专做非农数据ea

声明本站分享的文章旨在促进信息交流,不以盈利为目的,本文观点与本站立场无关,不承担任何责任。部分内容文章及图片来自互联网或自媒体,版权归属于原作者,不保证该信息(包括但不限于文字、图片、图表及数据)的准确性、真实性、完整性、有效性、及时性、原创性等,如无意侵犯媒体或个人知识产权,请来电或致函告之,本站将在第一时间处理。未经证实的信息仅供参考,不做任何投资和交易根据,据此操作风险自担。本站拥有对此声明的最终解释权。