面向对象编程基础·进阶篇
🧩

面向对象编程基础·进阶篇

(2/3)· 多数人在多态、封装字眼前就止步,其实先学会用类装函数就能跑通 OOP

新手友好 第 2/3 篇
把常用函数堆在一个 mqh 里,程序一长就记不住谁是谁。用类按用途分组,调用时一眼定位,比翻文件快得多。脚本直接塞进 EA 容易变量重名,改成类就能独立运行互不干扰。

「用类封装数组的去重追加」

在 MT5 里手写 EA 或指标时,往整型数组塞数据最常见两个动作:无脑接在尾部,以及只在该值不存在时才接尾部。把这两件事包进一个类,比每次都重写循环要干净得多。 下面这个类把查重逻辑藏进 private,对外只暴露 AddToEnd 和 AddToEndIfNotExists。Find 函数线性扫一遍数组,命中就返回索引,没命中返回 -1,调用方据此决定是否扩容。 注意原代码里 AddToEndIfNotExistss 函数名多拼了一个 s,编译虽能过但易和声明混淆,实战建议改名对齐。外汇与贵金属脚本涉及数组高频读写,封装后出错概率会低一些,但高频调用仍要留意 ArrayResize 的内存开销。

MQL5 / C++
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 包含目录下编译即可验证对象生成。

MQL5 / C++
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,就能验证重载是否按类型走了。外汇与贵金属杠杆高,这类基础库虽不直接下单,但统计逻辑错了会放大实盘风险。

MQL5 / C++
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 列表时,这种去重追加逻辑能降低误判概率,但行情跳空时仍可能漏判,属高风险场景。

MQL5 / C++
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(),在创建对象后手动调用一次。

MQL5 / C++
class="type">int Var = class="num">123;
上面这段代码若原样塞进类体就会报错;正确路径是先 int Var; 声明,再在 Init() 里写 Var = 123;。开 MT5 新建一个空 EA 类模板就能复现这个编译差异。

MQL5 / C++
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; // 同样判为出错

MQL5 / C++
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;
交给小布盯盘整理类结构
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到您 EA 里类的调用关系,把重复劳动交给小布,你专注决策。

常见问题

mqh 只是函数集合,类则把函数与变量按数据类型或用途封装,拥有独立作用域,避免命名冲突。
将脚本内的函数和局部变量移入 class 声明,外部通过实例化对象调用,原 EA 无需改动变量名。
可以,小布盯盘的品种页会解析 EA 结构,提示同名函数参数不匹配等常见重载错误,省去手动排查。
它允许同一类内用相同函数名处理不同数据类型,调用时按参数自动匹配,减少函数命名数量。
原文给出了替代标记法,核心是用更轻量的声明区分作用域,具体写法见正文示例节。