面向对象编程基础·进阶篇
(2/3)· 多数人在多态、封装字眼前就止步,其实先学会用类装函数就能跑通 OOP
「用类封装数组的去重追加」
在 MT5 里手写 EA 或指标时,往整型数组塞数据最常见两个动作:无脑接在尾部,以及只在该值不存在时才接尾部。把这两件事包进一个类,比每次都重写循环要干净得多。 下面这个类把查重逻辑藏进 private,对外只暴露 AddToEnd 和 AddToEndIfNotExists。Find 函数线性扫一遍数组,命中就返回索引,没命中返回 -1,调用方据此决定是否扩容。 注意原代码里 AddToEndIfNotExistss 函数名多拼了一个 s,编译虽能过但易和声明混淆,实战建议改名对齐。外汇与贵金属脚本涉及数组高频读写,封装后出错概率会低一些,但高频调用仍要留意 ArrayResize 的内存开销。
class CLibArray { class="kw">private: class=class="str">"cmt">// 检查一个带数值元素是否存在于数组里 class="type">int Find(class="type">int &aArray[],class="type">int aValue) { for(class="type">int i=class="num">0; i<ArraySize(aArray); i++) { if(aArray[i]==aValue) { class="kw">return(i); class=class="str">"cmt">//元素存在, 返回元素索引 } } class="kw">return(-class="num">1); class=class="str">"cmt">// 无此元素, 返回 -class="num">1 } class="kw">public: class=class="str">"cmt">// 加入到数组末端 class="type">void AddToEnd(class="type">int &aArray[],class="type">int aValue) { class="type">int m_size=ArraySize(aArray); ArrayResize(aArray,m_size+class="num">1); aArray[m_size]=aValue; } class=class="str">"cmt">// 如果数组中无此值,则加入到数组末端 class="type">void AddToEndIfNotExistss(class="type">int &aArray[],class="type">int aValue) { if(Find(aArray,aValue)==-class="num">1) { AddToEnd(aArray,aValue); } } };
◍ 类实例的载入与调用路径
在 MT5 里,类不是直接能用的,得先载入。若类写在独立 .mqh 文件里,要用 #include 把它拉进当前文件,再用「类名 + 指针名」的方式声明实例,这一步和变量声明长得很像,但载入后它就成了对象。 对象的函数调用靠「指针名.函数名」完成。输入点号之后,MT5 编辑器会弹出该类的成员下拉列表,不用硬背函数名,直接翻用途就行。 类相比散装函数最大的实惠是可做库封装。散装函数输入前几个字母会把全局库函数全列出来,类只列当前实例关联的公开函数。注意下拉里看不到的 Find() 这类,是写在 private 区的,外部调不动——这就是 public/private 分区的实感差别。 下方代码演示了最简载入:引入 OOP_CLibArray_1.mqh,然后声明 CLibArray 的实例 ar。复制到 MT5 包含目录下编译即可验证对象生成。
class="macro">#include <OOP_CLibArray_1.mqh>
CLibArray ar;用重载把 int 和 long 数组塞进同一个类
早期我们只给 CLibArray 写了吃 int 数组的 Find、AddToEnd 等函数。实际盯盘里 tick 成交量、挂单号经常超过 int 上限,long/ulong 类型的数组也得复用同一套逻辑,挨个改函数名既丑又难维护。 MQL5 允许同名函数按参数类型自动分派,这叫函数重载。下面这个类里 Find 和 AddToEnd 各写了 int 与 long 两个版本,编译器按你传的数组类型挑对应的那个,调用端完全无感。 私有段的两个 Find 都跑 ArraySize 做线性扫描,命中返回索引、没命中返回 -1;公有段 AddToEnd 用 ArrayResize 把数组加 1 再塞值,AddToEndIfNotExistss 先调 Find 判重再决定加不加。注意示例里函数名拼成了 AddToEndIfNotExistss(多一个 s),复制时最好顺手改回。 完整写法在随附的 OOP_CLibArray_1.mqh 里,开 MT5 建个 include 引一下,用 int 和 long 数组各调一次 AddToEndIfNotExistss,就能验证重载是否按类型走了。外汇与贵金属杠杆高,这类基础库虽不直接下单,但统计逻辑错了会放大实盘风险。
class CLibArray { class="kw">private: class=class="str">"cmt">// 用于int(整型)。检查带有所需值的元素是否存在于数组 class="type">int Find(class="type">int &aArray[],class="type">int aValue) { for(class="type">int i=class="num">0; i<ArraySize(aArray); i++) { if(aArray[i]==aValue) { class="kw">return(i); class=class="str">"cmt">// 元素存在, 返回元素索引 } } class="kw">return(-class="num">1); class=class="str">"cmt">// 无此元素, 返回 -class="num">1 } class=class="str">"cmt">// 用于long(长整型)。检查元素是否存在于数组 class="type">int Find(class="type">long &aArray[],class="type">long aValue) { for(class="type">int i=class="num">0; i<ArraySize(aArray); i++) { if(aArray[i]==aValue) { class="kw">return(i); class=class="str">"cmt">// 元素存在, 返回元素索引 } } class="kw">return(-class="num">1); class=class="str">"cmt">// 无此元素, 返回 -class="num">1 } class="kw">public: class=class="str">"cmt">// 用于int(整型)。加入到数组末端 class="type">void AddToEnd(class="type">int &aArray[],class="type">int aValue) { class="type">int m_size=ArraySize(aArray); ArrayResize(aArray,m_size+class="num">1); aArray[m_size]=aValue; } class=class="str">"cmt">// 用于 class="type">long(长整型)。加入到数组末端 class="type">void AddToEnd(class="type">long &aArray[],class="type">long aValue) { class="type">int m_size=ArraySize(aArray); ArrayResize(aArray,m_size+class="num">1); aArray[m_size]=aValue; } class=class="str">"cmt">// 用于 class="type">int(整型)。如果数组中无此值,则加入到数组末端 class="type">void AddToEndIfNotExistss(class="type">int &aArray[],class="type">int aValue) { if(Find(aArray,aValue)==-class="num">1) { AddToEnd(aArray,aValue); } } class=class="str">"cmt">// 用于 class="type">long(长整型)。如果数组中无此值,则加入到数组末端 class="type">void AddToEndIfNotExistss(class="type">long &aArray[],class="type">long aValue) { if(Find(aArray,aValue)==-class="num">1) { AddToEnd(aArray,aValue); } } };
「把函数挪到类外写更清爽」
当类里塞进几十个函数、每个还带一堆局部数据时,把所有实现都堆在类定义里会很难一眼看清结构。折中的做法是:类内部只留带参数的函数声明,具体实现挪到类外,用「类名::函数名」的写法绑定归属。
这样类定义本身变成一张接口清单,谁负责什么一目了然;真要抠某段逻辑,再去类外找对应实现即可。随附的 OOP_CLibArray_2.mqh 就是这种写法,CLibArray 只声明了 Find、AddToEnd 等几个重载,真正的循环和数组操作全在下面。
下面这段是类外实现的节选,注意两个冒号的作用:
class CLibArray 内部仅写 int Find(int &aArray[],int aValue); 这类声明;类外写 int CLibArray::Find(int &aArray[],int aValue) 才真正定义。
Find 函数用 for 从 i=0 跑到 ArraySize(aArray)-1,命中返回下标,跑完没命中返回 -1;AddToEnd 先取原大小 m_size,ArrayResize 扩 1,再把新值塞到末尾。
AddToEndIfNotExistss 这个名字在原文里多拼了一个 s,属于示例笔误,调用时 Find 返回 -1 才追加,避免重复元素进数组。外汇与贵金属 EA 里维护持仓 ticket 列表时,这种去重追加逻辑能降低误判概率,但行情跳空时仍可能漏判,属高风险场景。
class CLibArray { class="kw">private: class="type">int Find(class="type">int &aArray[],class="type">int aValue); class="type">int Find(class="type">long &aArray[],class="type">long aValue); class="kw">public: class="type">void AddToEnd(class="type">int &aArray[],class="type">int aValue); class="type">void AddToEnd(class="type">long &aArray[],class="type">long aValue); class="type">void AddToEndIfNotExistss(class="type">int &aArray[],class="type">int aValue); class="type">void AddToEndIfNotExistss(class="type">long &aArray[],class="type">long aValue); }; class=class="str">"cmt">//--- class="type">int CLibArray::Find(class="type">int &aArray[],class="type">int aValue) { for(class="type">int i=class="num">0; i<ArraySize(aArray); i++) { if(aArray[i]==aValue) { class="kw">return(i); } } class="kw">return(-class="num">1); } class=class="str">"cmt">//--- class="type">int CLibArray::Find(class="type">long &aArray[],class="type">long aValue) { for(class="type">int i=class="num">0; i<ArraySize(aArray); i++) { if(aArray[i]==aValue) { class="kw">return(i); } } class="kw">return(-class="num">1); } class=class="str">"cmt">//--- class="type">void CLibArray::AddToEnd(class="type">int &aArray[],class="type">int aValue) { class="type">int m_size=ArraySize(aArray); ArrayResize(aArray,m_size+class="num">1); aArray[m_size]=aValue; } class=class="str">"cmt">//--- class="type">void CLibArray::AddToEnd(class="type">long &aArray[],class="type">long aValue) { class="type">int m_size=ArraySize(aArray); ArrayResize(aArray,m_size+class="num">1); aArray[m_size]=aValue; } class=class="str">"cmt">//--- class="type">void CLibArray::AddToEndIfNotExistss(class="type">int &aArray[],class="type">int aValue) { if(Find(aArray,aValue)==-class="num">1) { AddToEnd(aArray,aValue); } } class=class="str">"cmt">//--- class="type">void CLibArray::AddToEndIfNotExistss(class="type">long &aArray[],class="type">long aValue) { if(Find(aArray,aValue)==-class="num">1) { AddToEnd(aArray,aValue); } }
◍ 类里声明变量要先走 Init
在 MQL5 文件顶层写代码时,变量可以在声明的同时直接给初值,例如下面这行就能编译通过并立刻拿到 123。 但一旦把变量搬进 class 内部,这种“声明即赋值”的写法就不被允许——类在实例化阶段不允许执行运行期赋值。 折中办法是写一个专门的初始化函数,把外部参数传进去再赋值。我们习惯把它命名为 Init(),在创建对象后手动调用一次。
class="type">int Var = class="num">123;
int Var; 声明,再在 Init() 里写 Var = 123;。开 MT5 新建一个空 EA 类模板就能复现这个编译差异。
class="type">int Var = class="num">123;
把删单脚本塞进一个类里
原先有个删挂单的脚本,用外部参数勾选要删的订单类型(BuyStop / SellStop / BuyLimit / SellLimit / BuyStopLimit / SellStopLimit)和是否跨品种(AllSymbol)。把它重写成 COrderDelete 类时,这些参数不再暴露为 input,而是挪到 private 区,变量名前统一加 m_ 前缀,比如 m_BuyStop。 加 m_ 不是语法强制,但能直接区分「类成员」和「全局/函数局部变量」。若三类作用域里写了同名变量,编译器会发隐藏警告,代码可读性也差。类里提供一个 Init() 方法,调用方先把原脚本的外部参数传进去,之后逻辑不变,只是把对外参的直读改成读 m_ 变量。 重构后脚本本身只剩几行:声明 input、include 那个 mqh、OnStart 里调 Init() 和 Delete()。Delete() 的返回值决定脚本怎么报结果。类独立成文件,EA 也能直接引用,不必再靠脚本硬跑。 下面这段是原始脚本循环删单的核心,逐行看: #include <Trade/Trade.mqh> // 引入 CTrade 类,封装下单删单接口 input bool AllSymbol=false; // 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 Trade; // 实例化交易对象 void OnStart() // 脚本入口 { bool Ret=true; // 结果标记,默认成功 for(int i=0; i<OrdersTotal(); i++) // 遍历终端所有挂单 { ulong Ticket=OrderGetTicket(i); // 按索引取订单 ticket 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)) // 调 CTrade 删单 { Ret=false; // 删失败则标记错误 } } else // 取 ticket 失败,结果未知 { Ret=false; // 同样判为出错
class="macro">#include <Trade/Trade.mqh> input class="type">bool AllSymbol=class="kw">false; input class="type">bool BuyStop=class="kw">false; input class="type">bool SellStop=class="kw">false; input class="type">bool BuyLimit=class="kw">false; input class="type">bool SellLimit=class="kw">false; input class="type">bool BuyStopLimit=class="kw">false; input class="type">bool SellStopLimit=class="kw">false; CTrade Trade; class="type">void OnStart() { class="type">bool Ret=true; for(class="type">int i=class="num">0; i<OrdersTotal(); i++) { class="type">class="kw">ulong Ticket=OrderGetTicket(i); if(Ticket>class="num">0) { class="type">long Type=OrderGetInteger(ORDER_TYPE); if(Type == ORDER_TYPE_BUY_STOP && !BuyStop) class="kw">continue; if(Type == ORDER_TYPE_SELL_STOP && !SellStop) class="kw">continue; if(Type == ORDER_TYPE_BUY_LIMIT && !BuyLimit) class="kw">continue; if(Type == ORDER_TYPE_SELL_LIMIT && !SellLimit) class="kw">continue; if(Type == ORDER_TYPE_BUY_STOP_LIMIT && !BuyStopLimit) class="kw">continue; if(Type == ORDER_TYPE_SELL_STOP_LIMIT && !SellStopLimit) class="kw">continue; if(!AllSymbol && Symbol()!=OrderGetString(ORDER_SYMBOL)) class="kw">continue; if(!Trade.OrderDelete(Ticket)) { Ret=class="kw">false; } } else { Ret=class="kw">false;