MQL5秘笈之:采用关联数组或字典实现快速数据访问·进阶篇
「数组里塞对象为何编译报错」
在 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,否则运行时行为可能偏离预期。
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() 返回该值供外部判断。
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 逆序走一遍,说明标准库链表对异构元素的前后向遍历是原生支持的。
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 总规模带来的开销风险。
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 语言里不常用的「结构转换」能救场:两个同尺寸、同变量顺序的结构体可以逐字节互转。
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);
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 长度的数组一次性填好,下标直接映射字符串。 外汇与贵金属杠杆高、滑点随机,任何内存优化都只是降低卡顿概率,不改动信号本身胜率。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据传入的时间框架值 | class=class="str">"cmt">//| 返回时间框架的字符串表示 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string PeriodToString(ENUM_TIMEFRAMES tf) { class="kw">switch(tf) { case PERIOD_M1: class="kw">return "M1"; case PERIOD_M5: class="kw">return "M5"; case PERIOD_M15: class="kw">return "M15"; case PERIOD_M30: class="kw">return "M30"; case PERIOD_H1: class="kw">return "H1"; case PERIOD_H4: class="kw">return "H4"; case PERIOD_D1: class="kw">return "D1"; case PERIOD_W1: class="kw">return "W1"; case PERIOD_MN1: class="kw">return "MN1"; } 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">//| 为数组添加关联值。 | | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void InitTimeframes() { ArrayResize(tf_values, PERIOD_MN1+class="num">1); tf_values[PERIOD_M1] = "M1"; tf_values[PERIOD_M5] = "M5"; tf_values[PERIOD_M15] = "M15"; tf_values[PERIOD_M30] = "M30"; tf_values[PERIOD_H1] = "H1"; tf_values[PERIOD_H4] = "H4"; tf_values[PERIOD_D1] = "D1"; tf_values[PERIOD_W1] = "W1"; tf_values[PERIOD_MN1] = "MN1"; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据传入的时间框架值 | class=class="str">"cmt">//| 返回对应的字符串。 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void PeriodToStringArray(ENUM_TIMEFRAMES tf) {