MQL5链表实战:从节点到标准库
手把手构建单向、双向与循环链表并落地交易场景
一、为什么MQL5开发者需要链表
在MQL5中,数组是最常用的数据集合,但数组在内存中必须连续存储,插入和删除中间元素时往往要调用ArrayResize()并移动大量数据。链表(Linked List)作为一种动态数据结构,由若干节点组成,每个节点包含数据字段与指向下一个和/或上一个节点的引用(指针)。它的核心优势是结构灵活:节点在内存中不需要相邻,在任意位置插入或删除节点的成本低,且不需要预先确定容量。对于EA开发中需要动态收集、统计、管理对象的场景(如图形对象框架、虚拟交易仓位),链表比数组更合适。
MQL5支持面向对象编程,标准库也提供了CList等现成容器。但理解链表的底层原理,能让你在标准库不满足需求时自行扩展,或避免误用导致的性能与内存问题。本文从理论到自定义实现,再到标准库对比,最后给出两个真实交易相关案例。
二、链表与节点的基础理论
计算机科学中,链表是由节点组成的线性或非线性集合。节点是一种复合数据类型,至少包含两部分:数据域和链接域。根据链接方向,常见有单向链表、双向链表和循环双向链表。
- 单向链表节点:只有指向下一节点的指针,尾节点指向NULL。
- 双向链表节点:增加指向前一节点的指针,头节点前驱为NULL。
- 循环双向链表节点:头尾互连,形成环,无NULL终结。
- 主要操作分三组:添加、删除、检查;外加服务组如排序、复制。
添加操作包括头插、尾插、指定位置插和空表插入;删除则是对应镜像操作,析构函数须正确释放所有节点。检查操作提供按值搜索、判空、取第i个节点值和指针、取大小等。这些操作的时间复杂度:插入删除为O(1)(已知位置),随机访问为O(N),这是与数组最显著的差异。
三、自定义节点类的编程实现
我们先从最基础的单向节点CiSingleNode开始。它包含两个保护成员:m_val(数据)和m_next(下一节点指针)。为了未来多态扩展,特意将SetPrevNode和GetPrevNode写成虚设的虚函数。以下为类声明核心:
class CiSingleNode
{
protected:
int m_val;
CiSingleNode *m_next;
public:
void CiSingleNode(void);
void CiSingleNode(int _node_val);
void ~CiSingleNode(void);
void SetVal(int _node_val);
void SetNextNode(CiSingleNode *_ptr_next);
virtual void SetPrevNode(CiSingleNode *_ptr_prev){};
virtual CiSingleNode *GetPrevNode(void) const {return NULL;};
CiSingleNode *GetNextNode(void) const;
int GetVal(void){return m_val;}
};
通过继承,双向节点CDoubleNode在父类基础上增加m_prev指针,并重写虚函数。松散双向节点CiUnrollDoubleNode进一步继承,将数据域从单一int改为整型数组,可封装整个数组。以下为双向节点类模型代码:
class CDoubleNode : public CiSingleNode
{
protected:
CiSingleNode *m_prev;
public:
void CDoubleNode(void);
void CDoubleNode(int node_val);
void ~CDoubleNode(void){};
virtual void SetPrevNode(CiSingleNode *_ptr_prev);
virtual CiSingleNode *GetPrevNode(void) const;
};
创建节点并链接的示例:用指针数组保存节点,rand()填充数据,再循环调用SetNextNode和SetPrevNode建立关联。日志会输出各节点值及前后链接值,验证结构正确。注意用delete释放每个节点,防止内存泄漏。
四、自定义列表类与继承体系
节点之上,我们构建列表类。CiSingleList以CiSingleNode为元素,提供AddFront、AddRear、RemoveFront、Size、PrintList等方法,并保留虚函数以便派生类重写。它是所有自定义列表的基类。双向列表CDoubleList公有继承CiSingleList,仅增加处理前驱的虚方法;松散双向列表CiUnrollDoubleList继承CDoubleList,重写以数组为数据的操作;循环双向列表CiCircleDoubleList则重写几乎所有方法,并额外提供LinkHeadTail()将头尾互链。
class CiSingleList
{
protected:
CiSingleNode *m_head;
CiSingleNode *m_tail;
uint m_size;
public:
void CiSingleList();
void CiSingleList(int _node_val);
void ~CiSingleList();
void AddFront(int _node_val);
void AddRear(int _node_val);
int RemoveFront(void);
bool IsEmpty(void) const;
virtual uint Size(void) const;
virtual void PrintList(string _caption=NULL);
};
循环链表中,addFront方法先调用父类插入,再调用LinkHeadTail保持环:
void CiCircleDoubleList::addFront(int _node_val)
{
CDoubleList::addFront(_node_val);
this.LinkHeadTail();
}
void CiCircleDoubleList::LinkHeadTail(void)
{
this.m_head.SetPrevNode(this.m_tail);
this.m_tail.SetNextNode(this.m_head);
}
脚本测试显示,自定义列表在填充百万节点时速度与标准库接近,但获取大小因需遍历而慢(359ms vs 0ms),这引出了标准库设计的优势。
五、MQL5标准库CList详解
标准库CList位于Objects集合中,继承自CObject。它拥有8个数据成员,包括当前节点索引和指针,支持内存管理标记FreeMode(删除列表时是否连带删除节点)、排序标记与模式。方法分为属性、创建、添加、删除、导航、排序、比较、搜索、输入输出等组。
关键差异:CList用m_data_total维护节点数,Total()即时返回,无需遍历;而自定义Size()若未缓存则需O(N)。内存管理上,默认FreeMode(true),delete列表会级联删除节点;设为false则仅脱链,需手动删节点否则泄漏(日志报'undeleted objects left')。
CList *myList=new CList;
// myList.FreeMode(false);
CNodeInt *p_new_nodes_int[10];
for(int i=0;i<10;i++){
p_new_nodes_int[i]=new CNodeInt(rand());
myList.Add(p_new_nodes_int[i]);
}
Print(myList.Total());
delete myList;
排序时CList::Sort()调用虚方法CObject::Compare(),基类未实现,必须自定义节点类重写Compare才能正确排序。随机访问元素用GetNodeAtIndex,内部二分查找,复杂度log2(N),但仍慢于数组直接下标。示例测试:列表节点达60万时随机访问耗时显著上升。
六、实战案例:图表图形对象统计
设想用EMA21斜率识别价格区间,在向下运动起点和向上运动起点画垂直线,两线构成一个“框架”。我们用CVertLineNode(继承CObject)封装两条垂直线属性、持续时间和形成标志,并将不同持续时间的框架放入多个CList中统计。脚本Stat_collector.mq5在过去2000根K线找出65个短框架、15个中等框架等,直接在图表画出并输出计数。链表在此展现了灵活组合统计的优势:动态增删框架节点,无需预设数组大小。
class CVertLineNode : public CObject
{
private:
SVertLineProperties m_vert_lines[2];
uint m_duration;
bool m_IsFrameFormed;
public:
void CVertLineNode(void);
void ~CVertLineNode(void){};
void SetLine(const SVertLineProperties &_vert_line,bool IsFirst=true);
uint GetDuration(void) const;
bool DrawLine(bool IsFirst=true) const;
};
七、实战案例:虚拟交易数据库
当需要在同一品种测试多种独立策略时,真实账户只允许一个仓位,其他策略可用“虚拟交易”模拟。我们定义CVirtualPositionNode等节点,用列表作为客户端内存数据库:一个主列表包含虚拟仓位子列表、虚拟订单子列表等。所有虚拟对象信息存于RAM,无需缓存文件,列表容器让策略组合与遍历异常方便。节点类初步定义如下:
class CVirtualPositionNode : public CObject
{
protected:
CVirtualPositionNode *m_virt_position;
public:
void CVirtualPositionNode(void);
void ~CVirtualPositionNode(void);
};
该思路可扩展至EA回测与多策略监控,体现了链表作为通用容器的价值。但注意虚拟交易不涉及真实服务器,仅客户端模拟,不可用于实盘信号。
八、文件组织与常见编译错误
建议将所有自定义类文件(.mqh)和测试脚本放在%MQL5%\Projects\UserLists下,用引号包含(#include "CiSingleList.mqh")。若放默认目录需用尖括号。社区反馈曾有用户编译test_MQL5_List报'm_head不能修改'等,原因是某些方法误标const,作者已修复——自行下载 ZIP 时请注意使用更正版。