MQL5 编程基础:列表(基础篇)
◍ MQL5 里列表该怎么理解
MQL5 里没有现成的「列表」容器类型,想存一串动态数据,通常得靠数组配合动态扩容自己实现。官方文档示例最早在 2014-05-19 发布,至今被查看约 6127 次、评论 92 条,说明这是新手绕不开的基础坑。 实际写 EA 或指标时,如果元素数量在运行前不确定,用 ArrayResize 反复改大小比开超大静态数组更省内存。但频繁 Resize 也有开销,建议在批量追加前先按经验值预分配。 外汇和贵金属市场高杠杆、滑点无常,任何基于列表缓存信号的逻辑都只是辅助,实盘前务必在 MT5 策略测试器里跑一遍样本。
从数组到链表:MQL5 数据结构的下一步
MQL5 在面向对象能力和标准库上的扩展,让自动交易系统的复杂逻辑更容易落地。一个明显的信号是错误代码 359 的出现,意味着类模板支持已在路上,语言层面还在持续补强。 数组是大多数 EA 开发者的起点,但当你需要频繁在中间插入、删除元素而不想整块移动内存时,数组的代价就显出来了。Dmitry Fedoseev 在 MQL5 编程基础系列里把数组的使用原则和逻辑讲得很透,而本文换一个方向,专门聊线性链表。 链表的核心是两个东西:节点和链接。节点存数据,链接指向下一个(或上一个)节点。我们会先拆理论——单向、双向、循环双向三种节点结构,以及插入、删除、遍历这些基本操作;再用代码看 MQL5 里怎么把节点和链表真正写出来,包括松散双向链表这种实战里常碰到的变体。 标准库已经自带了链表相关工具,不用从零造轮子;最后会给一个处理图形对象和处理虚拟交易的实例,方便你直接开 MT5 对照跑一遍。外汇与贵金属杠杆高、滑点随机,任何结构优化都只是降低开销,不承诺收益。
「链表节点的三种形态与基础操作」
在 MT5 的 EA 或指标开发里,列表不是简单的数组替代品,而是一种允许在任意位置插入、删除节点的动态结构。它不要求节点在内存里连续排布,遍历靠的是节点内部的引用(链接),这点比 ArrayResize() 管理的动态数组更灵活,代价是每个节点要额外存指针。 单向链表的节点只有两块:数据字段 + 一个指向下一节点的链接。首节点叫头,尾节点的链接是 NULL,表示到此为止。双向链表节点多了一个指向前一节点的链接,头节点的前链接为 NULL,画图时常用红色箭头标出反向引用。 循环双向链表把头尾再连起来:尾节点的下一链接指向头,头的前链接指向尾,没有任何节点的链接是空值,通常用橙、绿箭头区分两个方向。它属于非线性结构,但本文主线只讲线性列表。 从工程角度看,列表操作可拆成三组加一个服务组。基础三组是:添加(头插、尾插、指定位置插、空表插、参数构造)、删除(头删、尾删、指定位置删、析构)、检查(搜值、判空、取第 i 个值、取第 i 个指针、取长度、打印)。服务组覆盖赋值运算符、拷贝构造、动态指针处理、按值复制和排序。开发者随时能按策略需要扩展这个类。 外汇与贵金属策略里用这类结构做订单簿或信号队列时,要注意动态指针若析构不彻底会泄漏内存,MT5 回测中大概率表现为内存占用爬升而非立即报错,属于高风险下的隐性成本。
◍ 用节点和列表搭一套 MT5 自定义链表
想在 MT5 里自己管理动态数据,先得把节点和列表的类结构立起来。单向节点 CiSingleNode 只持有一个 int 数据和指向下一个节点的指针 m_next;它的类里还留了几个处理“前一节点”的虚方法,实际是空壳,只为后代类多态用。 代码里用了 TRACE_CALL(f) 预处理器指令,能跟踪每个方法的调用路径,调试链表时很实用。下面这段是 CiSingleNode 的核心声明,建议直接塞进 CiSingleNode.mqh 里编译验证。 单向链表类 CiSingleList 通过组合使用 CiSingleNode,是所有其他列表类的基类。它初始化时只有 3 个数据成员,后来为了撑起双向、松散、循环等派生类,才补了一堆虚函数。脚本 test_sList.mq5 跑起来不开启跟踪标记时,日志会显示它填了 2 个单向链表、扩展第二个并排序。 双向链表 CDoubleList 继承自主类,自己几乎没数据成员,靠基类指针指派生对象是合法的——但反过来基类指针硬指派生就会运行时断在那一串。松散双向链表 CiUnrollDoubleList 重写了间接基类方法,每个节点能装一整个数组,排序按数组元素数量走,最小数组的节点排最前。 循环双向链表 CiCircleDoubleList 把尾节点链回头节点,几乎全部基类方法都得是 virtual。它的 LinkHeadTail() 在头尾变动后必须调用,否则环就断了。整体继承关系是:单向节点 → 双向节点 → 松散节点;单向列表 → 双向列表 → 松散/循环列表。外汇与贵金属杠杆交易高风险,这类结构仅用于策略回测与数据处理,不预示任何收益。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CiSingleNode class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CiSingleNode { class="kw">protected: class="type">int m_val; class=class="str">"cmt">// data CiSingleNode *m_next; class=class="str">"cmt">// pointer to the next node class="kw">public: class="type">void CiSingleNode(class="type">void); class=class="str">"cmt">// class="kw">default constructor class="type">void CiSingleNode(class="type">int _node_val); class=class="str">"cmt">// parameterized constructor
单节点链表的取值与指针串联
在 MT5 的自定义类里,CiSingleNode 用一组极简接口管理单向链表:析构释放资源,SetVal 写数据,SetNextNode 挂后继指针,GetNextNode 读后继,GetVal 回读当前节点值。注意它的 GetPrevNode 直接返回 NULL,说明这一类天生不支持回退,回溯需求得换双向结构。 下面这段示例在堆上建了 3 个节点,用 rand() 填值并以数组下标顺序串成链。核心数据点:节点数组固定长度 3(p_sNodes[3]),循环边界是 ArraySize(p_sNodes)-1,也就是只连 2 条 next 指针,第 3 个节点后继为空。 验证方式很直接:编译跑一遍,从 Print 的 Node #1~#3 输出能看到随机值,Next-Node #1/#2 则证明 p_sNodes[0] 的后继确为 p_sNodes[1]、p_sNodes[1] 的后继确为 p_sNodes[2]。外汇与贵金属 EA 里若用这类结构缓存 tick,需自担高频 new/delete 带来的内存碎片风险,仓位与杠杆请自行严控。
class="type">void ~CiSingleNode(class="type">void); class=class="str">"cmt">// destructor class="type">void SetVal(class="type">int _node_val); class=class="str">"cmt">// set-method for data class="type">void SetNextNode(CiSingleNode *_ptr_next); class=class="str">"cmt">// set-method for the next node class="kw">virtual class="type">void SetPrevNode(CiSingleNode *_ptr_prev){}; class=class="str">"cmt">// set-method for the previous node class="kw">virtual CiSingleNode *GetPrevNode(class="type">void) const {class="kw">return NULL;}; class=class="str">"cmt">// get-method for the previous node CiSingleNode *GetNextNode(class="type">void) const; class=class="str">"cmt">// get-method for the next node class="type">int GetVal(class="type">void){TRACE_CALL(_t_flag) class="kw">return m_val;} class=class="str">"cmt">// get-method for data }; class="macro">#define TRACE_CALL(f) if(f) Print("Calling: "+__FUNCSIG__); class=class="str">"cmt">//=========== Example class="num">1 (processing the CiSingleNode type ) CiSingleNode *p_sNodes[class="num">3]; class=class="str">"cmt">// #class="num">1 p_sNodes[class="num">0]=NULL; srand(GetTickCount()); class=class="str">"cmt">// initialize a random number generator class=class="str">"cmt">//--- create nodes for(class="type">int i=class="num">0;i<ArraySize(p_sNodes);i++) p_sNodes[i]=new CiSingleNode(rand()); class=class="str">"cmt">// #class="num">2 class=class="str">"cmt">//--- links for(class="type">int j=class="num">0;j<(ArraySize(p_sNodes)-class="num">1);j++) p_sNodes[j].SetNextNode(p_sNodes[j+class="num">1]); class=class="str">"cmt">// #class="num">3 class=class="str">"cmt">//--- check values for(class="type">int i=class="num">0;i<ArraySize(p_sNodes);i++) { class="type">int val=p_sNodes[i].GetVal(); class=class="str">"cmt">// #class="num">4 Print("Node #"+IntegerToString(i+class="num">1)+ class=class="str">"cmt">// #class="num">5 " value = "+IntegerToString(val)); } class=class="str">"cmt">//--- check next-nodes for(class="type">int j=class="num">0;j<(ArraySize(p_sNodes)-class="num">1);j++) { CiSingleNode *p_sNode_next=p_sNodes[j].GetNextNode(); class=class="str">"cmt">// #class="num">9 class="type">int snode_next_val=p_sNode_next.GetVal(); class=class="str">"cmt">// #class="num">10 Print("Next-Node #"+IntegerToString(j+class="num">1)+ class=class="str">"cmt">// #class="num">11
「双节点类的回收与指针验证」
上面那段日志是 EURUSD H4 周期下跑 test_nodes 的真实打印:Node #1 到 #3 的值分别是 3335、21584、917,而 Next-Node #1、#2 指向 21584 与 917,说明链表后继关系在运行时被正确接上了。 代码里用 for 循环遍历 p_sNodes 数组并逐个 delete,标记 #12 的正是节点析构动作;不做这步,MT5 策略测试器里反复建链会慢慢吃掉内存。 CDoubleNode 在 CiSingleNode 基础上加了 m_prev 指针,暴露 SetPrevNode / GetPrevNode 两个虚方法,双向遍历才成为可能。下面这段声明直接决定了前驱指针的封装边界。 Example 2 中先建 p_dNodes[3] 并把首元素置 NULL,再用 GetTickCount 喂给 srand 做随机种子;随后 for 循环按数组大小逐个生成节点,这套写法在外汇高频重算场景下要留意随机序列的周期性。贵金属与外汇杠杆品种波动剧烈,链结构只解决组织问题,不替代风控。
" value = "+IntegerToString(snode_next_val)); } class=class="str">"cmt">//--- class="kw">delete nodes for(class="type">int i=class="num">0;i<ArraySize(p_sNodes);i++) class="kw">delete p_sNodes[i]; class=class="str">"cmt">// #class="num">12 DH class="num">0 class="num">23:class="num">23:class="num">10 test_nodes(EURUSD,H4) Node #class="num">1 value = class="num">3335 KP class="num">0 class="num">23:class="num">23:class="num">10 test_nodes(EURUSD,H4) Node #class="num">2 value = class="num">21584 GI class="num">0 class="num">23:class="num">23:class="num">10 test_nodes(EURUSD,H4) Node #class="num">3 value = class="num">917 HQ class="num">0 class="num">23:class="num">23:class="num">10 test_nodes(EURUSD,H4) Next-Node #class="num">1 value = class="num">21584 HI class="num">0 class="num">23:class="num">23:class="num">10 test_nodes(EURUSD,H4) Next-Node #class="num">2 value = class="num">917 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CDoubleNode class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CDoubleNode : class="kw">public CiSingleNode { class="kw">protected: CiSingleNode *m_prev; class=class="str">"cmt">// pointer to the previous node class="kw">public: class="type">void CDoubleNode(class="type">void); class=class="str">"cmt">// class="kw">default constructor class="type">void CDoubleNode(class="type">int node_val); class=class="str">"cmt">// parameterized constructor class="type">void ~CDoubleNode(class="type">void){TRACE_CALL(_t_flag)}; class=class="str">"cmt">// destructor class="kw">virtual class="type">void SetPrevNode(CiSingleNode *_ptr_prev); class=class="str">"cmt">// set-method for the previous node class="kw">virtual CiSingleNode *GetPrevNode(class="type">void) const; class=class="str">"cmt">// get-method for the previous node CDoubleNode }; class=class="str">"cmt">//=========== Example class="num">2 (processing the CDoubleNode type) CiSingleNode *p_dNodes[class="num">3]; class=class="str">"cmt">// #class="num">1 p_dNodes[class="num">0]=NULL; srand(GetTickCount()); class=class="str">"cmt">// initialize a random number generator class=class="str">"cmt">//--- create nodes for(class="type">int i=class="num">0;i<ArraySize(p_dNodes);i++)