从基础到中级:继承·进阶篇
🧬

从基础到中级:继承·进阶篇

(2/3)· 以为继承只是面向对象专属?这篇把结构体里的复用机制拆给你看

进阶 第 2/3 篇
不少交易者写 MQL5 把动物和植物的共有字段在各自结构里重写一遍,改一处漏一处。以为继承非得用 class 才叫继承,结果代码越堆越难动。其实结构体层面就能抽基类,少写就是少错。

用基类把异构数据收进同一套容器

在 MT5 的 EA 或指标工程里,经常要同时管「单值注册项」和「双值生物特征项」两类结构。如果各自写一套存取逻辑,后续遍历和序列化会非常散。 这里用 st_Base 作为公共基类,只放一个 private 的 uint KeyValue,再暴露 SetKey / GetKey。派生出来的 st_Reg 持有一个 string Value,st_Bio 持有 string Value[2],两者通过各自重载的 SetValue 把外部传入的参数写进成员。 关键点在 st_Bio::SetValue:它接收 arg1、arg2、arg3 三个参数,但只把 arg2 写进 Value[0]、arg3 写进 Value[1],arg1 在当前片段里未直接赋值给成员(可能留给基类 Key 用)。这种「同样的接口名、不同的参数列」就是编译期多态的入口。 开 MT5 新建一个 include 头文件,把下面代码贴进去编译,能验证结构体继承与数组成员初始化是否如预期。外汇与贵金属品种波动剧烈,这类底层封装虽不直接下单,但错误的数据结构会在回测中放大滑点偏差。

MQL5 / C++
class="kw">struct st_Base
{
  class="kw">private:
    class="type">uint KeyValue;
  class="kw">public:
    class="type">void SetKey(const class="type">uint arg) { KeyValue = arg; }
    class="type">uint GetKey(class="type">void) { class="kw">return KeyValue; }
};
class="kw">struct st_Reg : class="kw">public st_Base
{
  class="kw">private:
    class="type">class="kw">string  Value;
  class="kw">public:
    class="type">void SetValue(const class="type">uint arg1, const class="type">class="kw">string arg2)
    {
      Value = arg2;
    }
    class="type">class="kw">string GetValue(class="type">void)  { class="kw">return Value; }
};
class="kw">struct st_Bio : class="kw">public st_Base
{
  class="kw">private:
    class="type">class="kw">string  Value[class="num">2];
  class="kw">public:
    class="type">void SetValue(const class="type">uint arg1, const class="type">class="kw">string arg2, const class="type">class="kw">string arg3)
    {
      Value[class="num">0] = arg2;
      Value[class="num">1] = arg3;
    }
};

◍ 用基类把注册与生物特征数据收拢

MQL5 里结构体也能玩继承。先定义一个 st_Base,把 KeyValue 藏进 private,只留 SetKey 和 GetKey 两个 public 接口(行 004–013),所有派生结构都自带这把 uint 钥匙。 st_Reg 继承 st_Base 后,private 里只放一个 string Value,SetValue 先调父类的 SetKey 写索引、再存字符串(行 021–025)。这是单值登记的写法,适合账号或标签类数据。 st_Bio 同样继承 st_Base,但 Value 是长度为 2 的 string 数组(行 033)。它的 SetValue 接收三个参数,前一个是 uint 钥匙,后两个分别落进 Value[0] 和 Value[1],比 st_Reg 多存一组关联信息。 注意另一段派生写法:GetValue 用两个 string 引用出参,把 Value[0]、Value[1] 一次性吐出去并返回 true(行 41–47)。在 MT5 里跑这段,你能直接验证数组长度 2 与双出参引用的行为,外汇贵金属品种做用户数据映射时别忘这类结构的高风险误用可能。

MQL5 / C++
class="type">bool GetValue(class="type">class="kw">string &arg1, class="type">class="kw">string &arg2)
{
   arg1 = Value[class="num">0];
   arg2 = Value[class="num">1];

   class="kw">return true;
}
class=class="str">"cmt">//+----------------+
};
class=class="str">"cmt">//+------------------------------------------------------------------+

class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct st_Base
{
   class="kw">private:
      class="type">uint KeyValue;
   class="kw">public:
   class=class="str">"cmt">//+----------------+
      class="type">void SetKey(const class="type">uint arg) { KeyValue = arg; }
   class=class="str">"cmt">//+----------------+
      class="type">uint GetKey(class="type">void) { class="kw">return KeyValue; }
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct st_Reg : class="kw">public st_Base
{
   class="kw">private:
      class="type">class="kw">string  Value;
   class="kw">public:
   class=class="str">"cmt">//+----------------+
      class="type">void SetValue(const class="type">uint arg1, const class="type">class="kw">string arg2)
      {
         SetKey(arg1);
         Value = arg2;
      }
   class=class="str">"cmt">//+----------------+
      class="type">class="kw">string GetValue(class="type">void)  { class="kw">return Value; }
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct st_Bio : class="kw">public st_Base
{
   class="kw">private:
      class="type">class="kw">string  Value[class="num">2];
   class="kw">public:
   class=class="str">"cmt">//+----------------+
      class="type">void SetValue(const class="type">uint arg1, const class="type">class="kw">string arg2, const class="type">class="kw">string arg3)
      {

「用模板结构存键值对与动态数组」

上面这段 MT5 代码展示了两类自定义结构:一个固定存两个字符串值的 st_Bio,以及一个泛型 st_Data<T> 动态容器。st_Bio 里 SetKey 把传入参数写成键,Value[0]、Value[1] 分别落地两个字段,GetValue 再按引用原样吐回,调用零拷贝。 st_Data 的 Set 方法用 ArrayResize 做增量扩容:当数组为空时一次开 2 个槽,否则每次加 1 个,返回 INVALID_HANDLE 时判定失败。实测在 MT5 编译环境下,首次插入后 Values.Size() 为 2,后续每次 +1,这个步进差异直接影响内存申请次数。 Get 方法按 index 遍历比对 GetKey(),命中即返回,未命中退回 Values[0]——这意味着查不到键时不会报错,而是静默给首个元素,写回测脚本时容易掩盖键值错配。 宏 PrintX 把变量名和值拼成 'X => [值]' 打到日志,CheckBio 则预置 string sz[2] 准备承接 Bio 的双字段,方便在策略初始化阶段肉眼核对结构体里到底写了什么。

MQL5 / C++
SetKey(arg1);
Value[class="num">0] = arg2;
Value[class="num">1] = arg3;
}
class=class="str">"cmt">//+----------------+
class="type">bool GetValue(class="type">class="kw">string &arg1, class="type">class="kw">string &arg2)
{
 arg1 = Value[class="num">0];
 arg2 = Value[class="num">1];

 class="kw">return true;
}
class=class="str">"cmt">//+----------------+
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">template <class="kw">typename T>
class="kw">struct st_Data
{
 class="kw">private:
 T Values[];
 class="kw">public:
 class=class="str">"cmt">//+----------------+
 class="type">bool Set(const T &arg)
 {
 if (ArrayResize(Values, Values.Size() + (Values.Size() == class="num">0 ? class="num">2 : class="num">1)) == INVALID_HANDLE)
 class="kw">return false;

 Values[Values.Size() - class="num">1] = arg;

 class="kw">return true;
 }
 class=class="str">"cmt">//+----------------+
 T Get(const class="type">uint index)
 {
 for (class="type">uint c = class="num">0; c < Values.Size(); c++)
 if (Values[c].GetKey() == index)
 class="kw">return Values[c];

 class="kw">return Values[class="num">0];
 }
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define PrintX(X) Print(class="macro">#X, " => [", X, "]")
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CheckBio(st_Data <st_Bio> &arg)
{
 class="type">class="kw">string sz[class="num">2];

 Print("Checking data in the structure...");

模板容器在 OnStart 里的实际装配

上面这段是脚本入口 OnStart 里把字符串数组和键值数组塞进两个模板实例的过程。T 被按空格切成 8 个子串,M 与 K 各 8 个元素,循环 c 从 0 到 7 依次调用 reg.SetValue 与 bio.SetValue,再把对象写进 Info_1(st_Reg 模板)和 Info_2(st_Bio 模板)。 注意 K 数组顺序是 {2,1,4,0,7,5,3,6},并不是连续下标,所以 Info_1.Get(3) 实际取到的是 K[3]=0 对应的 reg 记录,而 Info_1.Get(13) 越界,会走 PrintX 里的失败分支。 CheckBio(Info_2) 里那段 for 循环只跑 i=7 和 i=10 两次(步长 3,上限 11),分别打印索引 7、10 的二维数组 sz。若 GetValue 成功就 ArrayPrint,失败打 Failed。你在 MT5 里跑这段代码,终端大概率是先出两条 Info_1 的取值日志,再看到两屏 sz 数组或 Failed 提示。 外汇与贵金属品种做这类数据结构验证时,行情跳动和内存占用虽不直接相关,但 EA 里滥用模板容器会拖慢 tick 处理,实盘前务必在策略测试器用历史数据跑一遍确认无越界。

MQL5 / C++
class="type">void OnStart(class="type">void)
{
   const class="type">class="kw">string T = "possible loss of data due to type conversion";
   const class="type">class="kw">string M[] = {"class="num">2", "cool", "class="num">4", "zero", "mad", "five", "what", "xoxo"};
   const class="type">uint   K[] = {class="num">2, class="num">1, class="num">4, class="num">0, class="num">7, class="num">5, class="num">3, class="num">6};

   st_Data <st_Reg> Info_1;
   st_Data <st_Bio> Info_2;

   class="type">class="kw">string  H[];

   StringSplit(T, &class="macro">#x27; &class="macro">#x27;, H);
   for (class="type">uint c = class="num">0; c < H.Size(); c++)
   {
      st_Reg  reg;
      st_Bio  bio;

      reg.SetValue(K[c], H[c]);
      bio.SetValue(K[c], M[c], H[c]);

      Info_1.Set(reg);
      Info_2.Set(bio);
   }

   PrintX(Info_1.Get(class="num">3).GetValue());
   PrintX(Info_1.Get(class="num">13).GetValue());
   CheckBio(Info_2);
}

◍ 把结构体、模板和继承揉进同一个列表

MQL5 没有 C++ 全套语法,但把结构化、模板、继承三者叠在一起写并不算越界。很多人觉得这类代码神秘,其实只是少有人把已学过的概念串起来用——同样效果,别人写一大坨,你用编译器代劳就行。 下面这段是起点:一个带私有键值的基结构,Setter/Getter 包住 uint 类型的 KeyValue。它后面会被继承,也会被模板化,但此刻先单独看。

MQL5 / C++
class="num">01. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">02. class="macro">#class="kw">property copyright "Daniel Jose"
class="num">03. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">04. class="kw">struct st_Base
class="num">05. {
class="num">06.    class="kw">private :
class="num">07.       class="type">uint KeyValue;
class="num">08.    class="kw">public  :
class="num">09.    class=class="str">"cmt">//+----------------+
class="num">10.       class="type">void SetKey(const class="type">uint arg)
class="num">11.       {
class="num">12.          KeyValue = arg;
class="num">13.       }
class="num">14.    class=class="str">"cmt">//+----------------+
class="num">15.       class="type">uint GetKey(class="type">void)
class="num">16.       {
class="num">17.          class="kw">return KeyValue;
class="num">18.       }
class="num">19.    class=class="str">"cmt">//+----------------+
class="num">20. };
class="num">21. class=class="str">"cmt">//+------------------------------------------------------------------+
逐行拆解:04 定义结构 st_Base;06–07 私有成员 KeyValue 存键;10–13 SetKey 写键;15–18 GetKey 读键;20 结构收尾。 真正卡人的地方在后面:当 st_Dev01 继承一个模板化的 st_Base 时,st_Dev01 自己也必须写成模板结构。更绕的是,派生结构里定义的类型参数,得反向塞回基类声明里——作者原话是花了很久才明白要把类似「代码09第22行」的声明改成「代码11第24行」那样,否则直接编译不过。 头文件里把 st_List 也改成模板后,主文件几乎不动,只换两处声明:列表变量类型、以及存进列表的结构类型。跑出来的结果和早期非模板版完全一致(参考原图05现象)。 别把继承模板当语法糖 继承模板基类时,派生结构不写 template 参数就会编译失败;这不是风格问题,是 MQL5 编译器硬要求。开 MT5 建个空 EA 把上面基结构贴进去,先试非模板继承,再加 template 看报错差异。 再进一步,把键和值的类型都模板化(代码13只动第23行),主文件稍作调整就能用任意类型做键值对列表。原图06展示的就是这套近乎完美的结构列表——作者留了个口子:不引入类(OOP)有个小坑填不上,后面再揭。

MQL5 / C++
class="num">01. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">02. class="macro">#class="kw">property copyright "Daniel Jose"
class="num">03. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">04. class="kw">struct st_Base
class="num">05. {
class="num">06.    class="kw">private :
class="num">07.       class="type">uint KeyValue;
class="num">08.    class="kw">public  :
class="num">09.    class=class="str">"cmt">//+----------------+
class="num">10.       class="type">void SetKey(const class="type">uint arg)
class="num">11.       {
class="num">12.          KeyValue = arg;
class="num">13.       }
class="num">14.    class=class="str">"cmt">//+----------------+
class="num">15.       class="type">uint GetKey(class="type">void)
class="num">16.       {
class="num">17.          class="kw">return KeyValue;
class="num">18.       }
class="num">19.    class=class="str">"cmt">//+----------------+
class="num">20. };
class="num">21. class=class="str">"cmt">//+------------------------------------------------------------------+

「用继承把键值对封装进结构体」

在 MT5 的 EA 或指标里,如果你要把多个对象的标识和附加信息一起管起来,基类加派生类的写法能少写很多重复代码。下面这段把 uint 类型的 KeyValue 藏在 private 里,只暴露 SetKey 和 GetKey 两个公共接口。

MQL5 / C++
class="kw">struct st_Base
{
class="kw">private :
  class="type">uint KeyValue;
class="kw">public :
  class="type">void SetKey(const class="type">uint arg) { KeyValue = arg; }
  class="type">uint GetKey(class="type">void) { class="kw">return KeyValue; }
};
逐行拆解:第 4 行用 struct 定义基类 st_Base;第 6 行 private 区放 uint KeyValue,外部不能直接改;第 10 行 SetKey 接收 const uint 参数写进 KeyValue;第 15 行 GetKey 原样返回存好的值。这样每个实例至少占 4 字节内存(uint 长度),但访问可控。 派生类 st_Dev01 继承 st_Base 后,多带一个 string ValueInfo。SetKeyInfo 一次写键和描述,GetInfo 把描述吐出来。
MQL5 / C++
class="kw">struct st_Dev01 : class="kw">public st_Base
{
class="kw">private :
  class="type">class="kw">string ValueInfo;
class="kw">public :
  class="type">void SetKeyInfo(class="type">uint arg1, class="type">class="kw">string arg2) { SetKey(arg1); ValueInfo = arg2; }
  class="type">class="kw">string GetInfo(class="type">void) { class="kw">return ValueInfo; }
};
你在 MT5 编辑器里建两个 st_Dev01 对象,分别 SetKeyInfo(1,"EURUSD") 和 SetKeyInfo(2,"XAUUSD"),遍历时靠 GetKey 区分、GetInfo 取品种名,做贵金属或外汇多品种扫描时结构会清晰很多。外汇与贵金属杠杆高,实盘前先在策略测试器用历史数据验证结构体读写逻辑。

MQL5 / C++
class="kw">struct st_Base
{
class="kw">private :
  class="type">uint KeyValue;
class="kw">public :
  class="type">void SetKey(const class="type">uint arg) { KeyValue = arg; }
  class="type">uint GetKey(class="type">void) { class="kw">return KeyValue; }
};
class="kw">struct st_Dev01 : class="kw">public st_Base
{
class="kw">private :
  class="type">class="kw">string ValueInfo;
class="kw">public :
  class="type">void SetKeyInfo(class="type">uint arg1, class="type">class="kw">string arg2) { SetKey(arg1); ValueInfo = arg2; }
  class="type">class="kw">string GetInfo(class="type">void) { class="kw">return ValueInfo; }
};
交给小布盯盘看盘口
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到结构重复带来的维护负担提示,你专注决策就好。

常见问题

结构体偏数据聚合、默认公开,类支持封装与访问控制;但两者都能做基类抽取共有字段,减少重复定义。
不一定,传统过程式也能出 EA;但理解继承机制后,后续改指标或扩展逻辑会更顺,概率上降低维护成本。
可以,比如把不同品种共有的 OHLC 与时间点抽成基础结构,再派生贵金属或外汇专属字段。
目前小布内置的是盘口与品种诊断,代码静态检查暂未开放;但重复结构导致的维护风险思路可借鉴定向优化。
可能跟不上派生与模板组合节奏;建议先落地前篇练习再进本篇,外汇贵金属编程高风险,基础松了容易出怪 bug。