MQL5链表实战:从节点到标准库
🔗

MQL5链表实战:从节点到标准库

手把手构建单向、双向与循环链表并落地交易场景

MQL5 编程 难度 · 进阶 2026-04-19 8 分钟阅读
#MQL5#链表#面向对象#标准库#数据结构

一、为什么MQL5开发者需要链表

在MQL5中,数组是最常用的数据集合,但数组在内存中必须连续存储,插入和删除中间元素时往往要调用ArrayResize()并移动大量数据。链表(Linked List)作为一种动态数据结构,由若干节点组成,每个节点包含数据字段与指向下一个和/或上一个节点的引用(指针)。它的核心优势是结构灵活:节点在内存中不需要相邻,在任意位置插入或删除节点的成本低,且不需要预先确定容量。对于EA开发中需要动态收集、统计、管理对象的场景(如图形对象框架、虚拟交易仓位),链表比数组更合适。

MQL5支持面向对象编程,标准库也提供了CList等现成容器。但理解链表的底层原理,能让你在标准库不满足需求时自行扩展,或避免误用导致的性能与内存问题。本文从理论到自定义实现,再到标准库对比,最后给出两个真实交易相关案例。

✦ 学习前提
阅读本文需要了解MQL5类、继承、指针与new/delete基础。若不熟悉数组,建议先读官方文章《MQL5编程基础:数组》。

二、链表与节点的基础理论

计算机科学中,链表是由节点组成的线性或非线性集合。节点是一种复合数据类型,至少包含两部分:数据域和链接域。根据链接方向,常见有单向链表、双向链表和循环双向链表。

添加操作包括头插、尾插、指定位置插和空表插入;删除则是对应镜像操作,析构函数须正确释放所有节点。检查操作提供按值搜索、判空、取第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释放每个节点,防止内存泄漏。

⚠ 常见坑
MQL5中若基类指针指向派生类对象(如CiSingleNode* p = new CDoubleNode()),只能调用基类接口。若试图将派生类特有指针反向赋值给基类对象(如CDoubleNode* d = new CiSingleNode()),编译虽过但运行会类型转换出错,务必理清继承层次。

四、自定义列表类与继承体系

节点之上,我们构建列表类。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万时随机访问耗时显著上升。

✦ 性能对比
若需频繁按索引访问,用数组;若需频繁插入删除且顺序遍历,用CList。CList的Save/Load虚方法需派生类实现节点IO,参考CIntList与CNodeInt示例可持久化到文件。

六、实战案例:图表图形对象统计

设想用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 时请注意使用更正版。

⚠ 内存泄漏预防
凡new出来的节点,若列表FreeMode(false)或脱离列表,必须手动delete;用CheckPointer确认有效再访问,避免野指针。循环链表析构要确保LinkHeadTail不会重复释放。

常见问题

数组内存连续、随机访问O(1)但插入删除需移动或Resize;链表节点离散、插入删除O(1)已知位置、随机访问O(N),更适合动态不定长且频繁增删的集合,如图形对象统计。
默认FreeMode(true)时delete列表会级联删节点;若调用FreeMode(false)仅脱链,须手动delete各节点。可用CheckPointer检测,日志出现'undeleted objects left'即泄漏。
CList::Sort内部调用虚方法CObject::Compare,基类未实现。需自定义节点类(如CNodeInt)继承CObject并重写Compare,再使用派生列表类才能正确排序。
先写CiSingleNode含m_val和m_next,再写CDoubleNode:public CiSingleNode增加m_prev,重写SetPrevNode/GetPrevNode虚函数,用new创建并SetNextNode/SetPrevNode链接,结束delete。
在插入或删除后调用LinkHeadTail:m_head.SetPrevNode(m_tail); m_tail.SetNextNode(m_head); 使头前驱为尾、尾后继为头,形成环,注意析构时防重复释放。
可以,如CiUnrollDoubleNode将m_val改为int m_arr_val[],节点封装整个数组,适合松散双向链表批量数据处理,但需重写列表类的增删打印方法。