
面向对象编程基础
简介
我们可以假定尝试开始学习面向对象编程(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 Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/351
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.






