MQL5秘笈之:采用关联数组或字典实现快速数据访问·进阶篇
📘

MQL5秘笈之:采用关联数组或字典实现快速数据访问·进阶篇

第 2/3 篇

「数组里塞对象为何编译报错」

在 MT5 里直接声明 CObjectCustom array[3]; 然后 array[0]=weather; 会触发编译错误:'=' - structure have objects and cannot be copied(Test.mq5 第 18 行第 13 列)。根因是自定义类内含对象成员,按值复制被禁止,只能走指针。 把数组改成指针形态 CObjectCustom* array[3];,再用 new 在堆上构造实例赋值,就能绕开拷贝限制。下面这段可原样贴进脚本验证: 别把栈对象当值传 含有对象成员的类,永远用指针或引用传递,按值写进数组必卡编译。 后续可扩展成 CObjectCustom* arrayObj[8]; 批量 new 出 CHuman / CWeather / CExpert 等不同派生类,实现多态容器。若做向下转型如 CHuman* human = obj;,需确认 obj 实际指向 CHuman,否则运行时行为可能偏离预期。

MQL5 / C++
class="macro">#include "ObjectsCustom.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 脚本程序start函数                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//---
   CObjectCustom* array[class="num">3];
   CWeather* weather = new CWeather();
   array[class="num">0] = weather;
  }

◍ 用类型枚举给对象打标签

在 MT5 自定义类库里,最省事的对象识别办法不是靠类名字符串比对,而是给每个派生类分配一个整型常量。下面这套写法用 #define 把 0 到 8 映射到不同业务类,基类 CObjectCustom 只持有一个 m_type 私有成员。 基类构造函数被设为 protected,且接收 int type 参数写进 m_type;默认无参构造则把类型锁死为 TYPE_OBJECT(0)。派生类如 CHuman、CWeather 在初始化列表里调用 CObjectCustom(TYPE_HUMAN) 或 TYPE_WEATHER,把自身类型在构造瞬间固化。 调用侧只要拿到 CObjectCustom 指针,跑一句 Type() 就能知道背后是人、天气还是 K 线,不用 dynamic_cast 那套重活。外汇与贵金属 EA 里对象种类多、回收频繁,这种轻量标签能明显降低运行时开销,但高频切换对象仍伴随 MT5 堆内存碎片风险,实盘前建议在策略测试器跑够样本。 关键代码逐行拆一下:#define TYPE_HUMAN 1 是给人类类发的‘身份证号’;class CHuman : public CObjectCustom 表示继承标签基类;CHuman() : CObjectCustom(TYPE_HUMAN){;} 在构造时把 1 传给基类 m_type;Type() 返回该值供外部判断。

MQL5 / C++
class="macro">#class="kw">property copyright "Copyright class="num">2015, Vasiliy Sokolov."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#define TYPE_OBJECT    class="num">0     class=class="str">"cmt">// 基类 CObjectCustom
class="macro">#define TYPE_HUMAN     class="num">1     class=class="str">"cmt">// 类 CHuman
class="macro">#define TYPE_WEATHER   class="num">2     class=class="str">"cmt">// 类 CWeather
class="macro">#define TYPE_EXPERT    class="num">3     class=class="str">"cmt">// 类 CExpert
class="macro">#define TYPE_CAR       class="num">4     class=class="str">"cmt">// 类 CCar
class="macro">#define TYPE_NUMBER    class="num">5     class=class="str">"cmt">// 类 CNumber
class="macro">#define TYPE_BAR       class="num">6     class=class="str">"cmt">// 类 CBar
class="macro">#define TYPE_MQ        class="num">7     class=class="str">"cmt">// 类 CMetaQuotes
class="macro">#define TYPE_SHIP      class="num">8     class=class="str">"cmt">// 类 CShip
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 基类 CObjectCustom                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CObjectCustom
  {
class="kw">private:
   class="type">int               m_type;
class="kw">protected:
                     CObjectCustom(class="type">int type){m_type=type;}
class="kw">public:
                     CObjectCustom(){m_type=TYPE_OBJECT;}
   class="type">int Type(){class="kw">return m_type;}
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 描述人类的类。                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CHuman : class="kw">public CObjectCustom
  {
class="kw">public:
                     CHuman() : CObjectCustom(TYPE_HUMAN){;}
   class="type">void Run(class="type">void){printf("Human run...");}
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 描述天气的类                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CWeather : class="kw">public CObjectCustom
  {
class="kw">public:
                     CWeather() : CObjectCustom(TYPE_WEATHER){;}
   class="type">class="kw">double Temp(class="type">void){class="kw">return class="num">32.0;}
   };

基类指针数组里的类型分支处理

把不同派生类对象塞进同一个基类指针数组,是 MT5 自定义类库里很常见的做法。下面这段直接建了 3 个槽位的 CObjectCustom* 数组,分别挂上 CHuman、CWeather、CBar 实例,随后用循环统一走一遍。 循环里先取基类指针,再调 obj.Type() 做 switch 分流:遇到 TYPE_HUMAN 就裸转 CHuman* 后跑 Run(),遇到 TYPE_WEATHER 就转 CWeather* 把 Temp() 保留 1 位小数打印出来,其余类型落 default 打 unknown type.。这种写法在 EA 里管理多品种监控对象时,能省掉一堆重复判断。 顺带看另一段:CList 链表里混入了 CCar、CExpert、CWealth、CShip 四个互不相干的类(都只继承 CObject 没加任何成员),OnStart 里正序 EnumerateAll 完再 ReverseEnumerateAll 逆序走一遍,说明标准库链表对异构元素的前后向遍历是原生支持的。

MQL5 / C++
CObjectCustom* arrayObj[class="num">3];
arrayObj[class="num">0] = new CHuman();
arrayObj[class="num">1] = new CWeather();
arrayObj[class="num">2] = new CBar();
for(class="type">int i = class="num">0; i < ArraySize(arrayObj); i++)
{
   CObjectCustom* obj = arrayObj[i];
   class="kw">switch(obj.Type())
   {
    case TYPE_HUMAN:
    {
      CHuman* human = obj;
      human.Run();
      break;
    }
    case TYPE_WEATHER:
    {
      CWeather* weather = obj;
      printf(DoubleToString(weather.Temp(), class="num">1));
      break;
    }
    class="kw">default:
      printf("unknown type.");
   }
}

class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                     TestList.mq5 |
class=class="str">"cmt">//|  Copyright class="num">2015, Vasiliy Sokolov. |
class=class="str">"cmt">//|                       [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2014, Vasiliy Sokolov."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#include <Object.mqh>
class="macro">#include <Arrays\List.mqh>
class CCar : class="kw">public CObject{};
class CExpert : class="kw">public CObject{};
class CWealth : class="kw">public CObject{};
class CShip : class="kw">public CObject{};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 脚本程序start函数                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//---
   CList list;
   list.Add(new CCar());
   list.Add(new CExpert());
   list.Add(new CWealth());
   list.Add(new CShip());
   printf(">>> enumerate from begin to end >>>");
   EnumerateAll(list);
   printf("<<< enumerate from end to begin <<<");
   ReverseEnumerateAll(list);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 从头至尾遍历列表在终端中                                          |
class=class="str">"cmt">//| 显示每一个元素。                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void EnumerateAll(CList& list)
{

「双向遍历链表节点的写法」

在 MT5 的 CList 容器里,正向和反向枚举都用 CObject* 指针顺着 Next()/Prev() 走,而不是靠下标硬取。正向从 GetFirstNode() 起步,i 从 0 累加;反向从 GetLastNode() 起步,i 初始化为 list.Total()-1 再递减,这样打印出的索引才和插入顺序对得上。 下面这段直接扔进 EA 的辅助函数就能跑,注意两个循环的中止条件都是 node != NULL,漏掉就会越界崩脚本。外汇和贵金属品种上做历史样本缓存时,这种双向遍历在回看尾部行情事件时概率上更省事,但高频调用仍要留意 List 总规模带来的开销风险。

MQL5 / C++
CObject* node = list.GetFirstNode();
for(class="type">int i = class="num">0; node != NULL; i++, node = node.Next())
   printf("Element at " + (class="type">class="kw">string)i);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 从尾至头遍历列表在终端中                                         |
class=class="str">"cmt">//| 显示每一个元素。                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void ReverseEnumerateAll(CList& list)
{
  CObject* node = list.GetLastNode();
  for(class="type">int i = list.Total()-class="num">1; node != NULL; i--, node = node.Prev())
     printf("Element at " + (class="type">class="kw">string)i);
}

◍ 把任意 MQL5 基础类型压成统一 Key

做通用字典的第一个坎,是让 int、enum、double、string 都能当 key。MQL5 里任何基础类型本质上都能被 ulong 承载:short/ushort 是 ulong 的短版本,datetime、enum、color 基于 32 位 uint,bool 只有 0/1,都能安全转型。系统函数如 AccountInfoInteger() 返回 long,也暗合这个特性。 浮点数是例外。直接把 3.0 和 3.14159 转 ulong 都会被截断成 3,两个不同值撞成同一个 key,字典就废了。C 语言里不常用的「结构转换」能救场:两个同尺寸、同变量顺序的结构体可以逐字节互转。

MQL5 / C++
class="kw">struct ULongValue { class="type">class="kw">ulong value; };
class="kw">struct DoubleValue { class="type">class="kw">double value; };
ULongValue lValue;
DoubleValue dValue;
dValue.value = class="num">3.14159;
lValue = (ULongValue)dValue;
printf("%I64u", lValue.value);
上面这段把 double 按字节拷进 ulong。实测 3.14159 打出 4614256673094690983,改成 3.14160 变成 4614256650576692846——差一位小数,key 完全脱开。float 先扩成 double 再走同一条转换路即可。 字符串走另一条路:用 Adler32() 哈希成 uint。脚本实测 "MetaTrader" → 352191466,"Hello world" → 413860925,多一个感叹号的 "Hello world!" → 487130206。输入差一个字符,哈希值天差地别,且恒落在 0~2^32 区间,可直接当整型 key 存。 至此所有基础类型都对齐到整型 key,但光有 key 还不够——下一节要解决的,是成千上万个 key 怎么落进内存而不爆数组。

MQL5 / C++
class="kw">struct ULongValue { class="type">class="kw">ulong value; };
class="kw">struct DoubleValue { class="type">class="kw">double value; };
ULongValue lValue;
DoubleValue dValue;
dValue.value = class="num">3.14159;
lValue = (ULongValue)dValue;
printf("%I64u", lValue.value);

用翻倍扩容压住数组重分配开销

动态字典这类结构在 MT5 里最忌频繁重分配内存。一个务实做法是让数组每次扩容都翻一倍:从 2 个元素起,第一次调到 4(2^2),第二次 8(2^3),第 16 次就是 65 536(2^16)。当新增数量正好踩中当前尺寸的 2 次幂边界才触发 resize,实际占用内存通常不会超过存下所有元素所需的两倍。 反过来,从链表删元素时顺手缩数组,能把闲置内存吐回去,避免长周期跑实盘时悄悄吃掉终端资源。 下面这段把时间框架枚举转字符串的逻辑,就是典型的『一次建表、后续 O(1) 查』思路,比每次 switch 更省事。PeriodToString 用分支硬转,InitTimeframes 则把 PERIOD_MN1+1 长度的数组一次性填好,下标直接映射字符串。 外汇与贵金属杠杆高、滑点随机,任何内存优化都只是降低卡顿概率,不改动信号本身胜率。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  根据传入的时间框架值                                        &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;|
class=class="str">"cmt">//|  返回时间框架的字符串表示  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  &nbsp;  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string PeriodToString(ENUM_TIMEFRAMES tf)
{
&nbsp;&nbsp; class="kw">switch(tf)
&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;case PERIOD_M1:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; class="kw">return "M1";
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;case PERIOD_M5:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; class="kw">return "M5";
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;case PERIOD_M15:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; class="kw">return "M15";
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;case PERIOD_M30:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; class="kw">return "M30";
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;case PERIOD_H1:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; class="kw">return "H1";
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;case PERIOD_H4:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; class="kw">return "H4";
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;case PERIOD_D1:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; class="kw">return "D1";
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;case PERIOD_W1:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; class="kw">return "W1";
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;case PERIOD_MN1:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; class="kw">return "MN1";
&nbsp;&nbsp; }
&nbsp;&nbsp; class="kw">return "unknown";
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 时间框架对应的                       |
class=class="str">"cmt">//| 字符串值。                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string tf_values[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 为数组添加关联值。                                                  |&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;                                                 &nbsp;&nbsp;&nbsp; | 
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void InitTimeframes()
{
&nbsp;&nbsp; ArrayResize(tf_values, PERIOD_MN1+class="num">1);
&nbsp;&nbsp; tf_values[PERIOD_M1] = "M1";
&nbsp;&nbsp; tf_values[PERIOD_M5] = "M5";
&nbsp;&nbsp; tf_values[PERIOD_M15] = "M15";
&nbsp;&nbsp; tf_values[PERIOD_M30] = "M30";
&nbsp;&nbsp; tf_values[PERIOD_H1] = "H1";
&nbsp;&nbsp; tf_values[PERIOD_H4] = "H4";
&nbsp;&nbsp; tf_values[PERIOD_D1] = "D1";
&nbsp;&nbsp; tf_values[PERIOD_W1] = "W1";
&nbsp;&nbsp; tf_values[PERIOD_MN1] = "MN1";
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  根据传入的时间框架值                                        &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |
class=class="str">"cmt">//| 返回对应的字符串。       &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;   &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void PeriodToStringArray(ENUM_TIMEFRAMES tf)
{

常见问题

数组要求元素类型一致,结构体未定义默认赋值运算符或含不可复制成员时会拒编;先用memcpy或显式重载=运算符再入数组。
在基类留虚函数Type()返回枚举,遍历时switch分支向下转型调用;别用字符串比对,枚举分支更快且编译期可查。
可以,把代码贴给小布,它会标出next/prev不对称的位置并给出补写示例,省去人工逐行排查。
用序列化拼接+哈希可降撞率,但double转字符串需固定精度;高频场景建议分开类型桶,别强行混一Key。
维护容量与计数,满时ArrayResize扩为2倍并拷贝旧值;预估规模先留底容,避免每次push都重分配。