面向对象编程基础·综合运用
「用类封装挂单批量删除逻辑」
在 MT5 脚本里想一次性清掉某类挂单,最忌讳把判断逻辑写死在主函数里。把参数和动作收进一个 COrderDelete 类,后面换品种或换订单类型只用改 Init 传入的布尔值,不用动核心循环。 这个类头一行就 #include <Trade/Trade.mqh>,靠 CTrade 对象发删除指令;私有成员用 7 个 bool 分别管跨品种开关与六种挂单类型(BuyStop / SellStop / BuyLimit / SellLimit / BuyStopLimit / SellStopLimit)。 Delete() 里先拿 OrdersTotal() 做上限跑 for 循环,用 OrderGetTicket(i) 取第 i 张单号,若返回大于 0 才算选中成功。选中后读 ORDER_TYPE,按类型逐个比对开关,没开权限的就 continue 跳过——比如 m_BuyStop 为 false 时,所有 ORDER_TYPE_BUY_STOP 单子原样留着。 外汇和贵金属挂单受点差与报价断流影响,脚本跑之前先确认账户允许对应类型操作,批量删单失败概率会随挂单数量上升而走高。
class="macro">#include <Trade/Trade.mqh> class COrderDelete { class="kw">private: class=class="str">"cmt">// 参数变量 class="type">bool m_AllSymbol; class="type">bool m_BuyStop; class="type">bool m_SellStop; class="type">bool m_BuyLimit; class="type">bool m_SellLimit; class="type">bool m_BuyStopLimit; class="type">bool m_SellStopLimit; class=class="str">"cmt">// 加载 CTrade 类 CTrade m_Trade; class="kw">public: class=class="str">"cmt">// 设置参数函数 class="type">void Init(class="type">bool aAllSymbol,class="type">bool aBuyStop,class="type">bool aSellStop,class="type">bool aBuyLimit,class="type">bool aSellLimit,class="type">bool aBuyStopLimit,class="type">bool aSellStopLimit) { class=class="str">"cmt">// 设置参数 m_AllSymbol =aAllSymbol; m_BuyStop =aBuyStop; m_SellStop =aSellStop; m_BuyLimit =aBuyLimit; m_SellLimit =aSellLimit; m_BuyStopLimit =aBuyStopLimit; m_SellStopLimit=aSellStopLimit; } class=class="str">"cmt">// 删除订单主函数 class="type">bool Delete() { class=class="str">"cmt">// 检查函数结果变量 class="type">bool m_Ret=true; class=class="str">"cmt">// 所有订单在客户端循环 for(class="type">int i=class="num">0; i<OrdersTotal(); i++) { class=class="str">"cmt">// 选择订单并取单号 class="type">class="kw">ulong m_Ticket=OrderGetTicket(i); class=class="str">"cmt">// 选择成功 if(m_Ticket>class="num">0) { class="type">long m_Type=OrderGetInteger(ORDER_TYPE); class=class="str">"cmt">// 检查订单类型 if(m_Type == ORDER_TYPE_BUY_STOP && !m_BuyStop) class="kw">continue; if(m_Type == ORDER_TYPE_SELL_STOP && !m_SellStop) class="kw">continue; if(m_Type == ORDER_TYPE_BUY_LIMIT && !m_BuyLimit) class="kw">continue; if(m_Type == ORDER_TYPE_SELL_LIMIT && !m_SellLimit) class="kw">continue; if(m_Type == ORDER_TYPE_BUY_STOP_LIMIT && !m_BuyStopLimit) class="kw">continue; if(m_Type == ORDER_TYPE_SELL_STOP_LIMIT && !m_SellStopLimit) class="kw">continue;
◍ 挂单批量清理的类的调用入口
这段脚本把挂单删除逻辑封装成了 COrderDelete 类,外部只暴露几个 input 开关和 OnStart 入口,实盘里改参数比改核心循环安全得多。 AllSymbol 默认 false,意味着脚本只删当前图表品种的挂单;若设 true,则遍历账户内所有品种。六种挂单类型(BuyStop / SellStop / BuyLimit / SellLimit / BuyStopLimit / SellStopLimit)各自独立 bool 开关,默认全 false,不显式开启哪一种就不会删那一种。 OnStart 里先 od.Init(...) 把七个外部参数灌进类,再调 od.Delete() 拿返回 bool。删除成功弹「脚本结束成功」,否则弹「脚本结束错误, 参见日志详情」——日志里具体哪单没删掉,得回看类内 OrderDelete 失败时的 Print 输出。 外汇与贵金属挂单清理涉及真实仓位风险,脚本误删 Limit 单可能造成预期入场落空,跑之前务必在策略测试器用 demo 账户验一遍开关组合。
class=class="str">"cmt">// Check symbol if(!m_AllSymbol && Symbol()!=OrderGetString(ORDER_SYMBOL)) class="kw">continue; class=class="str">"cmt">// 删除 if(!m_Trade.OrderDelete(m_Ticket)) { m_Ret=class="kw">false; class=class="str">"cmt">// 删除失败 } } class=class="str">"cmt">// 选择订单失败, 未知结果, class=class="str">"cmt">// 函数出错结束 else { m_Ret=class="kw">false; Print("选择订单错误"); } } class=class="str">"cmt">// 返回函数结果 class="kw">return(m_Ret); } }; class=class="str">"cmt">// 外部参数 class=class="str">"cmt">// 选择交易品种。 true - 删除所有交易品种的订单, class=class="str">"cmt">// class="kw">false - 仅删除脚本运行所在图表对应交易品种的订单 input class="type">bool AllSymbol=class="kw">false; class=class="str">"cmt">// 选择删除的订单类型 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; class=class="str">"cmt">// 包含类文件 class="macro">#include <OOP_CDeleteOrder_1.mqh> class=class="str">"cmt">// 加载类 COrderDelete od; class=class="str">"cmt">//+------------------------------------------------------------------ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------ class="type">void OnStart() { class=class="str">"cmt">// 传递外部参数至类 od.Init(AllSymbol,BuyStop,SellStop,BuyLimit,SellLimit,BuyStopLimit,SellStopLimit); class=class="str">"cmt">//--- 删除文件 class="type">bool Ret=od.Delete(); class=class="str">"cmt">// 处理删除结果 if(Ret) { Alert("脚本结束成功"); } else { Alert("脚本结束错误, 参见日志详情"); } }
类的自动启停:构造与析构
MT5 里 EA 和指标靠 OnInit()、OnDeinit() 管启动和收尾,类也有自己的对应机制——构造函数和析构函数,分别在类实例创建和销毁时自动跑,你从代码里永远看不到显式调用。
构造函数的写法是起一个跟类名完全一样的函数;析构函数则在前面加波浪符 ~。拿下面这段验证:脚本只做一件事,让实例暂停 3 秒,但跑起来会先弹「构造函数」警报,3 秒后弹「析构器」警报。
这种自动性在写指标辅助类时很实用:比如构造阶段把外部参数读好、在图表画好辅助线,析构阶段统一删对象,避免 MT5 退出时残留图形。外汇和贵金属行情跳空频繁,这类清理逻辑能降低误绘概率,但市场高风险仍在,自动化不替你兜底。
class=class="str">"cmt">// Class class CName { class="kw">public: class=class="str">"cmt">// 类 CName() { Alert("构造函数"); } class=class="str">"cmt">// 析构函数 ~CName() { Alert("析构器"); } class="type">void Sleep() { Sleep(class="num">3000); } }; class=class="str">"cmt">// 加载类 CName cname; class=class="str">"cmt">//+------------------------------------------------------------------ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------ class="type">void OnStart() { class=class="str">"cmt">// 暂停 cname.Sleep(); }
「把参数塞进构造函数省掉手动 Init」
把脚本改成类的时候,初始化那一行其实可以省掉。做法是在类里加一个构造函数,让它直接在对象创建时把参数接进去,然后内部调一次 Init()。 构造函数的所有参数都带默认值,所以老写法不受影响:不传参创建对象,之后再手动调 Init() 也行。 但有了构造函数后,更顺手的用法是创建对象时就把筛选条件传进去,连 Init() 都不用显式调用。下面这段就是带参构造的定义和调用样例。 Init() 仍留在 public 区,好处是能随时重新初始化。比如跑 EA 时,这一波只想撤 Stop 单,下一波只想撤 Limit 单,用不同参数再调一次 Init() 就能让 Delete() 删不同的挂单组。 随文给出的 OOP_CDeleteOrder_2.mqh 和 OOP_sDeleteOrders_3.mq5 里能直接看到这套写法,开 MT5 加载后可改参数验证删单范围。
COrderDelete(<span class="keyword">class="type">bool</span> aAllSymbol = <span class="keyword">class="kw">false</span>, <span class="keyword">class="type">bool</span> aBuyStop = <span class="keyword">class="kw">false</span>, <span class="keyword">class="type">bool</span> aSellStop = <span class="keyword">class="kw">false</span>, <span class="keyword">class="type">bool</span> aBuyLimit = <span class="keyword">class="kw">false</span>, <span class="keyword">class="type">bool</span> aSellLimit = <span class="keyword">class="kw">false</span>, <span class="keyword">class="type">bool</span> aBuyStopLimit = <span class="keyword">class="kw">false</span>, <span class="keyword">class="type">bool</span> aSellStopLimit=<span class="keyword">class="kw">false</span>) { Init(aAllSymbol,aBuyStop,aSellStop,aBuyLimit,aSellLimit,aBuyStopLimit,aSellStopLimit); } COrderDelete od(AllSymbol,BuyStop,SellStop,BuyLimit,SellLimit,BuyStopLimit,SellStopLimit);
◍ 同一类挂不同参数跑两类删单
在 MT5 的 EA 设计里,一个类没必要每次换逻辑就重新初始化。只要清楚这个类负责什么,就可以在初始化阶段载入多个带不同参数的实例,各自管各自的事。 以删除挂单为例:实盘里经常碰到两种需求,一种只清 BuyStop 和 BuyLimit,另一种只清 SellStop 和 SellLimit。与其写两套类或在运行中去改成员,不如直接 new 两个实例,把布尔开关按需要铺开。 下面这段就是典型用法——DeleteBuy 管买入类挂单,DeleteSell 管卖出类挂单,参数顺序一一对应六类订单加一个总开关。外汇与贵金属杠杆高,挂单批量删除前务必确认魔术码与品种过滤,误删可能直接吃滑点。 代码逐行看:第一行 COrderDelete 构造时传入七個布尔,DeleteBuy 的第2、4位为 true,即 BuyStop、BuyLimit 开启删除;第二行 DeleteSell 的第3、5位为 true,对应 SellStop、SellLimit;后两行各自调 Delete() 执行。
COrderDelete DeleteBuy(class="kw">false,true,class="kw">false,true,class="kw">false,class="kw">false,class="kw">false); COrderDelete DeleteSell(class="kw">false,class="kw">false,true,class="kw">false,true,class="kw">false,class="kw">false); DeleteBuy.Delete(); DeleteSell.Delete();
动态指针数组里的构造与泄漏
在 MT5 脚本里,若运行前不确定要实例化多少个类对象,就得用对象数组。和普通变量数组不同,声明时带星号 "*" 意味着用的是动态指针,而非自动变量。 可以预先定死大小,比如 CName* cname[10];也能用动态数组 CName* cname[],随后在 OnStart() 里用 ArrayResize(cname,10) 扩容。两者本质都是指针槽,真正装对象要靠循环里 new CName(i) 逐个注入。 跑一段不写 delete 的脚本,日志会刷出 10 次「构造函数」弹窗,却看不到析构函数,终端 Experts 标签页还会报内存泄漏——动态指针持有的对象不会在程序退出时自动释放。 补上循环 delete(cname[i]) 后,10 个析构函数依次触发,报错消失。外汇与贵金属 EA 开发中这类泄漏在高频重建对象时概率更高,建议封装成 RAII 风格或统一清理函数。
class=class="str">"cmt">// Class class CName { class="kw">private: class="type">int m_arg; class=class="str">"cmt">// 类 class="kw">public: class=class="str">"cmt">// 构造函数 CName(class="type">int aArg) { m_arg=aArg; Alert("构造函数"+IntegerToString(m_arg)); } class=class="str">"cmt">// 析构函数 ~CName() { Alert("析构函数 "+IntegerToString(m_arg)); } class=class="str">"cmt">//--- class="type">void Sleep() { Sleep(class="num">3000); } }; CName* cname[class="num">10]; CName* cname[]; ArrayResize(cname,class="num">10); ArrayResize(cname,class="num">10); for(class="type">int i=class="num">0; i<class="num">10; i++) { cname[i]=new CName(i); } cname[class="num">0].Sleep(); for(class="type">int i=class="num">0; i<class="num">10; i++) { class="kw">delete(cname[i]); }
「用虚函数把比较逻辑交给子类」
多态是面向对象里最值得盯紧的一块:它让同一段调用代码,在不同子类上跑出不同行为。典型做法是一个基类挂虚函数,再派生多个子类各自实现,比较两值就能拆出大于、小于、大于等于、小于等于、等于五种形态。 基类里的虚函数本身不写实质逻辑,只做接口占位。声明时以 virtual 开头,例如 virtual bool CheckVariant(int Var1,int Var2) 默认返回 false,相当于一个可插拔的连接器。 下面这段是五种比较的完整类结构。基类 CCheckVariant 之后,CVariant1 到 CVariant5 分别覆盖成 >、<、>=、<=、== 的实现。 如果提前知道要用哪种比较,直接用对应子类类型声明指针;若运行时才定,就用基类指针配合 new 动态载入子类。无论哪种,调用 CheckVariant 的代码完全一致,结果却由子类决定。该示例在随附的 OOP_sVariant_1.mq5 中可直跑验证。 别把虚函数当语法糖 它在 EA 里真正的价值是少写 if-else 分支。比如切换策略的触发条件时,换子类比改判断块安全,也方便小布类工具做变体回测。外汇与贵金属波动剧烈,任何逻辑改动都先上 MT5 用历史数据跑一遍再上实盘。
<span class="keyword">class</span> CCheckVariant { <span class="keyword">class="kw">public</span>: <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> CheckVariant(<span class="keyword">class="type">int</span> Var1,<span class="keyword">class="type">int</span> Var2) { <span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>); } }; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------</span> <span class="comment">class=class="str">"cmt">//| > |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------</span> <span class="keyword">class</span> CVariant1: <span class="keyword">class="kw">public</span> CCheckVariant { <span class="keyword">class="type">bool</span> CheckVariant(<span class="keyword">class="type">int</span> Var1,<span class="keyword">class="type">int</span> Var2) { <span class="keyword">class="kw">return</span>(Var1>Var2); } }; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------</span> <span class="comment">class=class="str">"cmt">//| < |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------</span> <span class="keyword">class</span> CVariant2: <span class="keyword">class="kw">public</span> CCheckVariant { <span class="keyword">class="type">bool</span> CheckVariant(<span class="keyword">class="type">int</span> Var1,<span class="keyword">class="type">int</span> Var2) { <span class="keyword">class="kw">return</span>(Var1<Var2); } }; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------</span> <span class="comment">class=class="str">"cmt">//| >= |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------</span> <span class="keyword">class</span> CVariant3: <span class="keyword">class="kw">public</span> CCheckVariant { <span class="keyword">class="type">bool</span> CheckVariant(<span class="keyword">class="type">int</span> Var1,<span class="keyword">class="type">int</span> Var2) { <span class="keyword">class="kw">return</span>(Var1>=Var2); } }; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------</span> <span class="comment">class=class="str">"cmt">//| <= |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------</span> <span class="keyword">class</span> CVariant4: <span class="keyword">class="kw">public</span> CCheckVariant { <span class="keyword">class="type">bool</span> CheckVariant(<span class="keyword">class="type">int</span> Var1,<span class="keyword">class="type">int</span> Var2) { <span class="keyword">class="kw">return</span>(Var1<=Var2); } }; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------</span> <span class="comment">class=class="str">"cmt">//| == |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------</span> <span class="keyword">class</span> CVariant5: <span class="keyword">class="kw">public</span> CCheckVariant { <span class="keyword">class="type">bool</span> CheckVariant(<span class="keyword">class="type">int</span> Var1,<span class="keyword">class="type">int</span> Var2) { <span class="keyword">class="kw">return</span>(Var1==Var2); } };
◍ 用 switch 分流五类条件检查实例
在 MT5 的 EA 或指标工程里,若想按运行模式切换不同的条件判定逻辑,可先声明一个基类指针,再用整型变量控制具体子类的实例化。下面这段写法把 Variant 固定为 5,意味着编译后只有五分之一的分支会被实际走到,其余四个 new 在当次运行中不会触发。 CVariant1 var; 这一行只是头注释里说的加载类声明,真正干活的是 CCcheckVariant* var 这个多态指针;switch(Variant) 里 case 1 到 5 分别 new 出 CVariant1~CVariant5,每个分支后必须 break,否则会连续构造多个对象导致内存泄漏。 最后一行 bool rv = var.CheckVariant(1,2); 用实例化出的对象去比两个入参,返回布尔值供后续信号过滤。外汇与贵金属市场波动剧烈、杠杆风险高,这类条件分支仅用于策略逻辑分流,不预示任何方向。 开 MT5 把 Variant 改成 3 试一次,你能直接验证只有 CVariant3 被加载,其余子类根本不占运行时开销。
CVariant1 var; class=class="str">"cmt">// 加载类来检查">" 条件 CCheckVariant* var; class=class="str">"cmt">// 变量数量 class="type">int Variant=class="num">5; class=class="str">"cmt">// 依据变量数量,五分之一子类将会被使用 class="kw">switch(Variant) { case class="num">1: var = new CVariant1; break; case class="num">2: var = new CVariant2; break; case class="num">3: var = new CVariant3; break; case class="num">4: var = new CVariant4; break; case class="num">5: var = new CVariant5; break; } class="type">bool rv = var.CheckVariant(class="num">1,class="num">2);
private 与 protected 在继承链上的真实边界
写 EA 或指标类库时,public 区放的是给调用方用的接口,这一点没有歧义。但 protected 和 private 对外层用户(也就是用你封装好的类写策略的人)表现完全一致——两者里的函数都不会出现在 MT5 代码补全的下拉列表里,用户根本调不到。 真正分水岭在继承。基类 CBase 里一个函数放 protected、一个放 private,子类 Class 想直接调用:ProtectedFunc() 编译通过,PrivateFunc() 会直接报“不能调用私有成员函数”。也就是说,private 不只是挡用户,连子类都挡;protected 只挡用户,不挡子类。 随文给了两个验证文件:OOP_sProtPriv_1.mq5 看用户视角下 public 仅暴露一个函数,OOP_sProtPriv_2.mq5 看子类调用两种分区的差异。图 4 用蓝/灰箭头标了基类函数对子类的可见性,蓝可用、灰不可用。 别把正态当圣经 封装层级不是越 private 越安全。若你打算让类库后续被扩展(比如写一系列外汇品种过滤器子类),基类核心逻辑放 protected 才合理;纯内部状态机放 private。先想清楚要不要给人继承,再定分区。
class=class="str">"cmt">//+------------------------------------------------------------------ class=class="str">"cmt">//| 受保护的关键字的类 | class=class="str">"cmt">//+------------------------------------------------------------------ class CName1 { class="kw">protected: class="type">int ProtectedFunc(class="type">int aArg) { class="kw">return(aArg); } class="kw">public: class="type">int PublicFunction(class="type">int aArg) { class="kw">return(ProtectedFunc(aArg)); } }; class=class="str">"cmt">//+------------------------------------------------------------------ class=class="str">"cmt">//| 类的私有关键字 | class=class="str">"cmt">//+------------------------------------------------------------------ class CName2 { class="kw">private: class="type">int PrivateFunc(class="type">int aArg) { class="kw">return(aArg); } class="kw">public: class="type">int PublicFunction(class="type">int aArg) { class="kw">return(PrivateFunc(aArg)); } }; CName1 c1; class=class="str">"cmt">// 加载保护类 CName2 c2; class=class="str">"cmt">// 加载私有类 class=class="str">"cmt">//+------------------------------------------------------------------ class=class="str">"cmt">//| 基本类 | class=class="str">"cmt">//+------------------------------------------------------------------ class CBase { class="kw">protected: class="type">class="kw">string ProtectedFunc() { class="kw">return("CBase ProtectedFunc"); } class="kw">private: class="type">class="kw">string PrivateFunc() { class="kw">return("CBase PrivateFunc"); } class="kw">public: class="kw">virtual class="type">class="kw">string PublicFunction() { class="kw">return(""); } }; class=class="str">"cmt">//+------------------------------------------------------------------ class=class="str">"cmt">//| 子类 | class=class="str">"cmt">//+------------------------------------------------------------------ class Class: class="kw">public CBase { class="kw">public: class="type">class="kw">string PublicFunction() { class=class="str">"cmt">// 带此行,所有代码会编译成功 class="kw">return(ProtectedFunc()); class=class="str">"cmt">// 如果您未注释此行和注释前一个, 将会遇到编译错误,返回(PrivateFunc()); class=class="str">"cmt">// 返回(PrivateFunc()); } };
「虚函数不重写就回退基类」
在 MQL5 的继承链里,基类声明的 virtual 函数并不强制子类重写。子类若提供了同名函数,调用时走子类版本;若没写,就静默回退到基类的实现——这对写 EA 框架时减少重复代码很实用。 上面这段示例里,Class2 完全是空壳,只继承了 CBase。执行 OnStart 时,c1.Function() 返回 "Function of child ",而 c2.Function() 返回 "Function of base class",证明空子类也能通过基类虚函数跑通逻辑。 从类使用者角度看,public 区的函数全部可见,这就是继承性的直观体现。但注意:子类自己加的“额外”函数,只有在指针类型声明为子类时才调得到;用基类指针载子类,那些额外函数会被编译器隐藏。 随附的 OOP_sDefaultVirtual_1.mq5 可直接在 MT5 里编译运行,改一下 Class1 的返回串就能验证多态替换是否生效。外汇与贵金属杠杆高,策略代码逻辑错一处可能瞬间放大亏损,跑通前务必用模拟盘验。
class=class="str">"cmt">//+------------------------------------------------------------------ class=class="str">"cmt">//| 基本类 | class=class="str">"cmt">//+------------------------------------------------------------------ class CBase { class="kw">public: class="kw">virtual class="type">class="kw">string Function() { class="type">class="kw">string str=""; str="Function "; str=str+"of base "; str=str+"class"; class="kw">return(str); } }; class=class="str">"cmt">//+------------------------------------------------------------------ class=class="str">"cmt">//| 子类1 | class=class="str">"cmt">//+------------------------------------------------------------------ class Class1: class="kw">public CBase { class="kw">public: class="type">class="kw">string Function() { class="type">class="kw">string str=""; str="Function "; str=str+"of child "; class="kw">return(str); } }; class=class="str">"cmt">//+------------------------------------------------------------------ class=class="str">"cmt">//| 子类2 | class=class="str">"cmt">//+------------------------------------------------------------------ class Class2: class="kw">public CBase { }; Class1 c1; class=class="str">"cmt">// 加载类 class="num">1 Class2 c2; class=class="str">"cmt">// 加载类 class="num">2 class=class="str">"cmt">//+------------------------------------------------------------------ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------ class="type">void OnStart() { Alert("class="num">1: "+c1.Function()); class=class="str">"cmt">// 从 Class1 运行函数 Alert("class="num">2: "+c2.Function()); class=class="str">"cmt">// 从 CBase运行函数 } class="type">void OnStart() { Alert("class="num">1: " + c1.Function()); class=class="str">"cmt">// 从 Class1运行函数 Alert("class="num">2: " + c2.Function()); class=class="str">"cmt">// 从 CBase运行函数 }
◍ 基类指针怎么决定跑哪个类
在 MQL5 里写带虚函数的继承结构时,指针类型直接决定了实际跑的是基类还是子类代码。如果你明确知道要用 Class1 还是 Class2,直接声明对应子类的实例最省事,虚函数调用会精确落到子类实现里。 不确定用哪个子类时,可以声明一个 CBase* 动态指针,运行时用 new 指向具体子类来载入。这样同一段调用代码,在 new Class1 和 new Class2 时行为不同,是多态的典型用法。 如果写成 CBase c; 这种自动(栈上)指针,编译器原样按基类处理,虚函数会被降格成普通函数,不会跳到子类。也就是说,想靠虚函数做运行时分发,必须用堆分配的动态指针,栈对象吃不到多态。 下面这段演示了三种声明差异:定长子类对象、基类动态指针、基类自动对象。注意 c=new Class1 之后,c 的虚函数走的是 Class1 逻辑;而 CBase c; 那种写法虚函数已静态绑定。
Class1 c1; class=class="str">"cmt">// 加载类 class="num">1 Class2 c2; class=class="str">"cmt">// 加载类 class="num">2 CBase *c; class=class="str">"cmt">// 动态指针 class="type">void OnStart() { c=new Class1; class=class="str">"cmt">// 加载类 ... CBase c; class=class="str">"cmt">// 自动指针
用基类引用让一个函数吃下所有对象
在 MT5 的 MQL5 里,对象不能按值传参,指针必须加 & 以引用方式进函数。只要函数形参写成基类类型(例如 CBase &c),子类实例都能喂进去,函数内部再调虚函数,就能自动走到各自实现。 上面这段示例里,CBase 只声明了一个返回空串的虚 Function(),Class1 和 Class2 各自重写返回 "Class 1" / "Class 2"。OnStart 中连续调用 Function(c1) 和 Function(c2),弹窗会先后出现 Class 1、Class 2 两行——说明同一个函数处理了两种对象。 实盘写 EA 时,这种写法适合把不同信号源(均线类、波动率类)统一丢给一个研判函数。外汇和贵金属波动剧烈、杠杆高风险大,任何信号都只是概率倾向,别把基类多态当成胜率保证。 代码逐行看:class CBase 定义基类,virtual string Function() 留空实现;Class1: public CBase 重写返回具体标识;Class1 c1; Class2 c2; 是栈上实例化。void Function(CBase &c) 的 & 不能省,否则编译报错。里面 Alert(c.Function()) 利用虚表分发。OnStart 里两次 Function 调用验证了引用传参的多态效果。
class=class="str">"cmt">//+------------------------------------------------------------------ class=class="str">"cmt">//| 基本类 | class=class="str">"cmt">//+------------------------------------------------------------------ class CBase { class="kw">public: class="kw">virtual class="type">class="kw">string Function() { class="kw">return(""); } }; class=class="str">"cmt">//+------------------------------------------------------------------ class=class="str">"cmt">//| 子类1 | class=class="str">"cmt">//+------------------------------------------------------------------ class Class1: class="kw">public CBase { class="kw">public: class="type">class="kw">string Function() { class="kw">return("Class class="num">1"); } }; class=class="str">"cmt">//+------------------------------------------------------------------ class=class="str">"cmt">//| 子类2 | class=class="str">"cmt">//+------------------------------------------------------------------ class Class2: class="kw">public CBase { class="kw">public: class="type">class="kw">string Function() { class="kw">return("Class class="num">2"); } }; Class1 c1; class=class="str">"cmt">// 加载类 class="num">1 Class2 c2; class=class="str">"cmt">// 加载类 class="num">2 class=class="str">"cmt">//+------------------------------------------------------------------ class=class="str">"cmt">//| 处理对象的函数 | class=class="str">"cmt">//+------------------------------------------------------------------ class="type">void Function(CBase &c) { Alert(c.Function()); } class=class="str">"cmt">//+------------------------------------------------------------------ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------ class="type">void OnStart() { class=class="str">"cmt">// 处理对象使用一个函数. Function(c1); Function(c2); }
「类接口里的方法和属性到底怎么分」
写 MQL5 类的时候,圈内人嘴上说「方法」其实和「函数」是一回事,区别只在视角。你自己写类,public 区里声明的全是函数;但别人调你的类,敲个点弹出的下拉列表里那些能直接用的,就被叫成方法。 除了方法,public 区还能塞变量,包括数组。这些变量在下拉列表里和方法并排出现,术语上叫「属性」。属性和普通变量用法没两样,赋值、读取都直接走点号。 下面这段来自 OOP_sMethodsAndProperties.mq5 的示例,定义了两个 int 属性 Property1、Property2 和两个空方法 Function1、Function2,OnStart 里给属性赋值后弹窗读出。外汇与贵金属 EA 开发中滥用 public 属性会放大耦合风险,建议只暴露必要接口。 代码逐行看:class 开头定义类 CMethodsAndProperties;public: 之后 Property1、Property2 是整型属性并带注释;Function1、Function2 是返回 void 的空方法,内部仅 return;OnStart 中 c.Property1=1、c.Property2=2 完成属性写入,Alert 拼接 IntegerToString 输出两属性值。
class CMethodsAndProperties { class="kw">public: class="type">int Property1; class=class="str">"cmt">// 属性 class="num">1 class="type">int Property2; class=class="str">"cmt">// 属性 class="num">2 class="type">void Function1() { class=class="str">"cmt">//... class="kw">return; } class="type">void Function2() { class=class="str">"cmt">//... class="kw">return; } }; class="type">void OnStart() { c.Property1 = class="num">1; class=class="str">"cmt">// 设置属性 class="num">1 c.Property2 = class="num">2; class=class="str">"cmt">// 设置属性 class="num">2 class=class="str">"cmt">// 读属性 Alert("Property1 = " + IntegerToString(c.Property1) + ", Property2 = " + IntegerToString(c.Property2)); }
◍ struct 怎么在 MT5 里落地
MQL5 里的 struct 比 class 轻,它只能装变量和数组,没有 public/private 之分,所有成员默认公开。写法是 struct 打头,大括号里直接声明成员,例如随附的 OOP_Struct.mq5 里就用了 Str1 装 int、double 和对应数组。 要用结构得先按结构名声明变量,也能声明结构数组:Str1 s1 是单实例,Str1 sar1[] 是数组,后者在策略里批量缓存不同品种的状态很顺手。 结构还能嵌套结构。上面 Str2 里塞了一个 Str1 类型的 Str 成员,想取里层变量得用两个点连写:s2.Str.IntVar=1 就是把 Str2 实例 s2 内嵌 Str1 的 IntVar 设为 1。外汇和贵金属波动快,用嵌套 struct 组织多周期数据比散变量更不容易写错,但杠杆品种高风险,参数错了可能瞬间回撤。 下面这段是原文结构声明的可直接拷进 MT5 验证:
class="kw">struct Str1 { class="type">int IntVar; class="type">int IntArr[]; class="type">class="kw">double DblVar[]; class="type">class="kw">double DblArr[]; }; Str1 s1; Str1 sar1[]; class="kw">struct Str2 { class="type">int IntVar; class="type">int IntArr[]; class="type">class="kw">double DblVar[]; class="type">class="kw">double DblArr[]; Str1 Str; }; s2.Str.IntVar=class="num">1;
MQL5 类机制的关键落点
把前面两篇拆开的面向对象写法收敛一下,真正在 MT5 里写类时容易卡住的只有几个硬规则。class 关键字之后跟类名,大括号内必须切出 private、protected、public 三个访问区,变量和函数落错区就会导致编译期或运行期越权调用。 private 区的成员只在类内部可见,protected 允许子类复用,public 则全局可访问;这是权限边界,不是装饰。类外定义函数必须写成 ClassName::FunctionName() 的形式,否则链接器不认归属。 动态指针创建对象要用 new,程序退出前必须 delete,漏掉就会在 EA 重载时留下内存残影。子类声明写成 class Class : public CBase,虚函数靠 virtual 标记,子类同名即覆盖、否则回退基类实现。 一个容易忽略的点:类内声明变量不能就地赋值,初始化要交给构造函数或后续调用。结构本身也能嵌套数组和其他结构,这在组织行情快照时比裸数组省事。下面这段是最小骨架,直接贴进 MT5 编辑器能编译通过。
class CName { class="kw">private: class="kw">protected: class="kw">public: }; class="type">void ClassName::FunctionName() { ... } class Class : class="kw">public CBase { ... }
「把工具请下神坛」
这套 OOP 示例里,OOP_CDeleteOrder_2.mqh 与 OOP_sDeleteOrders_3.mq5 是仅有的两个建议留下反复用的文件,其余 14 个演示脚本和包含文件在跑通逻辑后都能直接删,不占 Include 和 Scripts 目录。 真正写 EA 或脚本时,你大概率只会从构造函数传参删单那个类改起,而不是把 protected/private 的对照示例都留着。MT5 里打开 Scripts 夹拖一个 OOP_sConstDestr_1 进去编译,就能亲眼看到构造和析构的触发顺序,比看说明直观。 外汇和贵金属杠杆高、滑点凶,用这些类封装下单逻辑只是降低代码混乱度,不解决胜率问题;跑之前先在策略测试器用历史数据验证一遍参数传递有没有漏。