简介
我们可以假定尝试开始学习面向对象编程(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)。
图 2. CName1 类函数
图 3. CName2 类函数
此示例载于本文随附的 OOP_sProtPriv_1.mq5 文件中。
private 与 protected 分区决定基类函数对其子类的可见性:
//+------------------------------------------------------------------ //| 基本类 | //+------------------------------------------------------------------ class CBase { protected: string ProtectedFunc() { return("CBase ProtectedFunc"); } private: string PrivateFunc() { return("CBase PrivateFunc"); } public: virtual string PublicFunction() { return(""); } }; //+------------------------------------------------------------------ //| 子类 | //+------------------------------------------------------------------ class Class: public CBase { public: string PublicFunction() { // 带此行,所有代码会编译成功 return(ProtectedFunc()); // 如果您未注释此行和注释前一个, 将会遇到编译错误,返回(PrivateFunc()); // 返回(PrivateFunc()); } };
本例中,我们有名为 CBase 的基类和名为 Class 的子类。尝试从子类调用位于protected与 private分区中的基类函数。如果您从protected分区调用函数,一切编译和运行都会正常进行。如果您从private分区调用函数,则会出现一个编译器错误(不能调用私有成员函数)。也就是说,通过 private 分区的函数对子类不可见。
protected分区只会保护来源于类用户的函数,而private分区亦会保护来源于子类的函数。来源于子类的基类函数(位于不同分区)的可见性,请见图 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. 类用户访问函数
除子类中没有对应基类虚函数的函数的情况之外,子类可能拥有“额外”函数(基类中没有同名虚函数的函数)。如果您利用指针将类载入到子类类型,则上述函数可用。如果您利用指针将类载入到基类类型,则上述函数不可用(图 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. 一个列表中类的方法与属性
这些属性的使用方法与变量相同:
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. 结构可纳入数组及其它结构。
附录文件清单
- OOP_CLibArray_1.mqh – 包含文件,应置于 MQL5/Include 文件夹。使用类创建库的示例。protected 与 private 关键词。重载。
- 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 文件夹。protected 与 private 关键词在使用类时的同一性示例。
- OOP_sProtPriv_2.mq5 – 脚本,应置于 MQL5/Scripts 文件夹。protected 与 private 关键词对子类的影响示例。
- 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 Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/351
MyFxtop迈投(www.myfxtop.com)-靠谱的外汇跟单社区,免费跟随高手做交易!
免责声明:本文系转载自网络,如有侵犯,请联系我们立即删除,另:本文仅代表作者个人观点,与迈投财经无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。