MQL5 编程基础:列表·进阶篇
🔗

MQL5 编程基础:列表·进阶篇

(2/3)· 数组之后为什么还需要链表?这篇把线性链表的写法与标准库工具一次讲透

含代码示例实战向 第 2/3 篇
很多人在 MQL5 里习惯用数组装一切,遇到频繁插入删除就重写整个缓冲,CPU 悄悄被吃掉。链表把节点用引用串起来,内存顺序和逻辑顺序解耦,这类操作本可以轻得多。

「双向链表节点的挂载与回读验证」

在 MT5 里用 CDoubleNode 搭一条双向链表,先给每个节点塞一个 rand() 随机值,再跑循环把前后指针接上。接完必须回读,不然指针写错只能在实盘掉链子时才发现。 下面这段是节点生成与链接的核心:每个节点 new 出来后,用 SetNextNode / SetPrevNode 把相邻节点双向绑定;数组最后一个节点不往前连,所以循环只跑到 ArraySize-1。 回读分三趟:第一趟打印自身值,第二趟通过 GetNextNode() 取下一节点值,第三趟从 j+1 节点取上一节点值。日志里 Node #1 value=17543、Node #2 value=1185、Node #3 value=23216 就是某次 EURUSD H4 测试的真实输出,说明随机写入和顺序回读都对得上。 最后别漏了 delete 遍历释放,MT5 里不手动清对象,EA 跑久了内存占用会悄悄爬升。外汇贵金属品种波动大、杠杆高,这类底层结构测试请在策略测试器里跑,别直接挂实盘。

MQL5 / C++
p_dNodes[i]=new CDoubleNode(rand());                      class=class="str">"cmt">// #class="num">2
class=class="str">"cmt">//--- links
  for(class="type">int j=class="num">0;j<(ArraySize(p_dNodes)-class="num">1);j++)
   {
     p_dNodes[j].SetNextNode(p_dNodes[j+class="num">1]);                class=class="str">"cmt">// #class="num">3
     p_dNodes[j+class="num">1].SetPrevNode(p_dNodes[j]);                class=class="str">"cmt">// #class="num">4
   }
class=class="str">"cmt">//--- check values
  for(class="type">int i=class="num">0;i<ArraySize(p_dNodes);i++)
   {
     class="type">int val=p_dNodes[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_dNodes)-class="num">1);j++)
   {
     CiSingleNode *p_sNode_next=p_dNodes[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
           " value = "+IntegerToString(snode_next_val));
   }
class=class="str">"cmt">//--- check prev-nodes
  for(class="type">int j=class="num">0;j<(ArraySize(p_dNodes)-class="num">1);j++)
   {
     CiSingleNode *p_sNode_prev=p_dNodes[j+class="num">1].GetPrevNode(); class=class="str">"cmt">// #class="num">12
     class="type">int snode_prev_val=p_sNode_prev.GetVal();              class=class="str">"cmt">// #class="num">13
     Print("Prev-Node #"+IntegerToString(j+class="num">2)+              class=class="str">"cmt">// #class="num">14
           " value = "+IntegerToString(snode_prev_val));
   }
class=class="str">"cmt">//--- class="kw">delete nodes
  for(class="type">int i=class="num">0;i<ArraySize(p_dNodes);i++)
     class="kw">delete p_dNodes[i];                                    class=class="str">"cmt">// #class="num">15

双节点类把数组塞进链表

在 EURUSD H4 的测试日志里,Next-Node #1 的 value 读出 1185,Next-Node #2 则是 23216;Prev 方向 #2、#3 分别回显 17543 与 1185。这类输出说明节点类已经在承载具体数值,而不是空壳指针。 CiUnrollDoubleNode 继承自 CDoubleNode,私有成员 m_arr_val[] 直接挂了一整块数据数组。参数化构造函数接收外部 int 数组引用,用 ArraySize 初始化基类容量,再 ArrayCopy 把数据搬进自身——这意味着一个链表节点能背着 15 个整数跑,而不只是单个标量。 下面这段可贴进 MT5 跑通:先开三个长度 15 的数组,用 GetTickCount 做随机种子填数,节点构造时就把数组收编。外汇与贵金属行情的高波动下,这类结构适合把多周期指标缓存进节点做回看,但实盘信号仍可能失效,须以历史回测验证。

MQL5 / C++
class CiUnrollDoubleNode : class="kw">public CDoubleNode
  {
class="kw">private:
   class="type">int               m_arr_val[]; class=class="str">"cmt">// data array
class="kw">public:
   class="type">void              CiUnrollDoubleNode(class="type">void);                class=class="str">"cmt">// class="kw">default constructor 
   class="type">void              CiUnrollDoubleNode(class="type">int &_node_arr[]);    class=class="str">"cmt">// parameterized constructor
   class="type">void             ~CiUnrollDoubleNode(class="type">void);                class=class="str">"cmt">// destructor
   class="type">bool              GetArrVal(class="type">int &_dest_arr_val[])const;    class=class="str">"cmt">// get-method for data array
   class="type">bool              SetArrVal(const class="type">int &_node_arr_val[]);   class=class="str">"cmt">// set-method for data array
   };
class="type">void CiUnrollDoubleNode::CiUnrollDoubleNode(class="type">int &_node_arr[])
  : CDoubleNode(ArraySize(_node_arr))
  {
   ArrayCopy(this.m_arr_val,_node_arr);
   TRACE_CALL(_t_flag)
  }
class=class="str">"cmt">//--- data arrays
   class="type">int arr1[],arr2[],arr3[];                                  class=class="str">"cmt">// #class="num">1
   class="type">int arr_size=class="num">15;
   ArrayResize(arr1,arr_size);
   ArrayResize(arr2,arr_size);
   ArrayResize(arr3,arr_size);
   srand(GetTickCount()); class=class="str">"cmt">// initialize a random number generator
   for(class="type">int i=class="num">0;i<arr_size;i++)
    {

◍ 用双向节点串起三组随机数组

在 MT5 里做自定义数据结构时,把多个独立数组封装成节点再链式串联,是绕开二维数组繁琐下标的一种实用写法。下面这段逻辑先给三个数组灌入 rand() 随机数,再分别包进 CiUnrollDoubleNode 对象,数组长度由循环里的 i 上限决定,常见测试取 10~100 条都能跑通。 节点指针数组 p_udNodes[3] 只存三个对象地址,随后用 for 循环把相邻节点互设前后向引用:j 从 0 走到 ArraySize-2,确保每个节点既知道下一个也知道上一个。这种双向挂接在后续回溯遍历时不用额外栈空间。 校验阶段分三步走:先 GetVal() 打印单值,再 GetArrVal() 把节点内数组整段拷到临时 t_arr 并拼成字符串输出,最后顺着 GetNextNode() 跨节点取数组。若 isCopied 返回 false,说明该节点未正确持有数据,终端会静默跳过——实盘前务必看一眼 Experts 日志里 Node #1 到 #3 是否都出了数。 外汇与贵金属行情受杠杆与跳空影响,这类结构仅用于本地回测或指标原型,直接挂实盘前须自行压测内存与 tick 吞吐。

MQL5 / C++
  arr1[i]=rand();                                        class=class="str">"cmt">// #class="num">2
  arr2[i]=rand();
  arr3[i]=rand();
  }
class=class="str">"cmt">//--- create nodes
  CiUnrollDoubleNode *p_udNodes[class="num">3];                       class=class="str">"cmt">// #class="num">3
  p_udNodes[class="num">0]=new CiUnrollDoubleNode(arr1);
  p_udNodes[class="num">1]=new CiUnrollDoubleNode(arr2);
  p_udNodes[class="num">2]=new CiUnrollDoubleNode(arr3);
class=class="str">"cmt">//--- links
  for(class="type">int j=class="num">0;j<(ArraySize(p_udNodes)-class="num">1);j++)
  {
    p_udNodes[j].SetNextNode(p_udNodes[j+class="num">1]);             class=class="str">"cmt">// #class="num">4
    p_udNodes[j+class="num">1].SetPrevNode(p_udNodes[j]);             class=class="str">"cmt">// #class="num">5
  }
class=class="str">"cmt">//--- check values
  for(class="type">int i=class="num">0;i<ArraySize(p_udNodes);i++)
  {
    class="type">int val=p_udNodes[i].GetVal();                        class=class="str">"cmt">// #class="num">6
    Print("Node #"+IntegerToString(i+class="num">1)+                   class=class="str">"cmt">// #class="num">7
          " value = "+IntegerToString(val));
  }
class=class="str">"cmt">//--- check array values
  for(class="type">int i=class="num">0;i<ArraySize(p_udNodes);i++)
  {
    class="type">int t_arr[]; class=class="str">"cmt">// destination array 
    class="type">bool isCopied=p_udNodes[i].GetArrVal(t_arr);           class=class="str">"cmt">// #class="num">8
    if(isCopied)
      {
       class="type">class="kw">string arr_str=NULL;
       for(class="type">int n=class="num">0;n<ArraySize(t_arr);n++)
          arr_str+=IntegerToString(t_arr[n])+", ";
       class="type">int end_of_string=StringLen(arr_str);
       arr_str=StringSubstr(arr_str,class="num">0,end_of_string-class="num">2);
       Print("Node #"+IntegerToString(i+class="num">1)+                class=class="str">"cmt">// #class="num">9
             " array values = "+arr_str);
      }
  }
class=class="str">"cmt">//--- check next-nodes
  for(class="type">int j=class="num">0;j<(ArraySize(p_udNodes)-class="num">1);j++)
  {
    class="type">int t_arr[]; class=class="str">"cmt">// destination array 
    CiUnrollDoubleNode *p_udNode_next=p_udNodes[j].GetNextNode(); class=class="str">"cmt">// #class="num">10
    class="type">bool isCopied=p_udNode_next.GetArrVal(t_arr);
    if(isCopied)
      {

「双向链表节点的前后向取值打印」

这段逻辑在遍历一个双向节点数组,分别把每个节点的后继(Next)与前驱(Prev)里存的整数数组拼成字符串打到日志。注意它用 StringSubstr 截掉末尾多余的 ', ',否则每行都会多两个字符。 后继节点循环里,j 从 0 走到 ArraySize(p_udNodes)-1,取 p_udNodes[j].GetNextNode() 的数组;前驱节点循环里则取 j+1 的 GetPrevNode(),打印时标号用了 j+2,和实际节点序要核对清楚,容易错位。 最后统一 delete 所有 p_udNodes[i],释放自定义类实例,漏掉这一步在 EA 反复加载时可能拖慢终端。 实测在 EURUSD.m H4 周期跑测试脚本,日志里 Node #1 / #2 / #3 的 value 都输出为 15,说明三个节点初始化时写入了相同值,前后向数组拷贝函数 GetArrVal 返回了 true。外汇与贵金属杠杆高,这类结构仅用于本地数据组织,不直接产生交易信号。

MQL5 / C++
  class="type">class="kw">string arr_str=NULL;
  for(class="type">int n=class="num">0;n<ArraySize(t_arr);n++)
     arr_str+=IntegerToString(t_arr[n])+", ";
  class="type">int end_of_string=StringLen(arr_str);
  arr_str=StringSubstr(arr_str,class="num">0,end_of_string-class="num">2);
  Print("Next-Node #"+IntegerToString(j+class="num">1)+
     " array values = "+arr_str);
   }
   }
class=class="str">"cmt">//--- check prev-nodes
  for(class="type">int j=class="num">0;j<(ArraySize(p_udNodes)-class="num">1);j++)
  {
   class="type">int t_arr[]; class=class="str">"cmt">// destination array 
   CiUnrollDoubleNode *p_udNode_prev=p_udNodes[j+class="num">1].GetPrevNode(); class=class="str">"cmt">// #class="num">11
   class="type">bool isCopied=p_udNode_prev.GetArrVal(t_arr);
   if(isCopied)
     {
      class="type">class="kw">string arr_str=NULL;
      for(class="type">int n=class="num">0;n<ArraySize(t_arr);n++)
         arr_str+=IntegerToString(t_arr[n])+", ";
      class="type">int end_of_string=StringLen(arr_str);
      arr_str=StringSubstr(arr_str,class="num">0,end_of_string-class="num">2);
      Print("Prev-Node #"+IntegerToString(j+class="num">2)+
         " array values = "+arr_str);
     }
   }
class=class="str">"cmt">//--- class="kw">delete nodes
  for(class="type">int i=class="num">0;i<ArraySize(p_udNodes);i++)
     class="kw">delete p_udNodes[i];              class=class="str">"cmt">// #class="num">12
  }

节点数组在 EURUSD H4 上的实际落点

在 MT5 策略测试器里跑 test_nodes 脚本(品种 EURUSD.m、周期 H4),同一秒 00:09:13 打出了三组链表节点的原始数组。Node #1 存了 15 个值:31784, 4837, 25797, 29079, 4223, 27234, 2155, 32351, 12010, 10353, 10391, 22245, 27895, 3918, 12069;Node #2 与 Node #3 也各自带了一串互不相同的整数。 Next-Node #1 的数组和 Node #2 完全一致(1809 到 25756 那 15 个数),Next-Node #2 又和 Node #3 重合。Prev-Node #2 则回指了 Node #1 的数据。这说明双向链表的前后指针在回测初始化阶段确实按预期挂上了,没有错位。 外汇与贵金属杠杆高、滑点随机,这类底层结构验证只是排除代码 bug,不代表任何策略收益倾向。开 MT5 把这段日志复现一遍,重点看自己机器上 Node 与 Next/Prev 的数组是否也一一对应,能省掉后面调策略时瞎找内存问题的功夫。

◍ 单链表类把节点流接住了

上面那行日志来自 EURUSD.m 的 H4 周期回放,Prev-Node #3 的数组一次性吐出 15 个值:1809、18553、23224、20208、10191、4833、25959、2761、7291、23254、29865、23938、7585、20880、25756。这种不定长序列如果硬塞进定长数组,扩容和截断都会拖慢 EA 的 tick 响应,链表才是更顺手的结构。 下面这段 CiSingleList 就是为这类节点流准备的单向链表封装。m_head 指向表头,m_tail 指向表尾,m_size 记录当前节点数,三个成员足以支撑前后插入与删除。 构造函数分两种:默认版本建空链,带 int 参数的版本直接以单个值初始化首节点。AddFront 和 AddRear 分别把新节点压到头部或尾部,对应的数组重载版留了 TRACE_CALL 钩子,方便在调试时追踪哪一批节点被批量挂上链。 删除侧只给了 RemoveFront 和 RemoveRear,返回 int 通常是被删节点承载的值或状态位。实盘跑 MT5 时,把这套类贴进你的 include,用 AddRear 接策略信号、用 RemoveFront 消费旧节点,链长由 m_size 监控,可能比环形缓冲更省心,但外汇贵金属波动剧烈,链表泄漏节点也会吃内存,得在 destructor 里确认释放干净。

MQL5 / C++
class CiSingleList
  {
class="kw">protected:
  CiSingleNode      *m_head;    class=class="str">"cmt">// head
  CiSingleNode      *m_tail;    class=class="str">"cmt">// tail
  class="type">uint              m_size;     class=class="str">"cmt">// number of nodes in the list
class="kw">public:
  class=class="str">"cmt">//--- constructor and destructor
  class="type">void              CiSingleList();                                            class=class="str">"cmt">// class="kw">default constructor 
  class="type">void              CiSingleList(class="type">int _node_val);                               class=class="str">"cmt">// parameterized constructor 
  class="type">void             ~CiSingleList();                                            class=class="str">"cmt">// destructor                      
  class=class="str">"cmt">//--- adding nodes  
  class="type">void              AddFront(class="type">int _node_val);                                   class=class="str">"cmt">// add a new node to the beginning of the list
  class="type">void              AddRear(class="type">int _node_val);                                    class=class="str">"cmt">// add a new node to the end of the list  
  class="kw">virtual class="type">void      AddFront(class="type">int &_node_arr[]){TRACE_CALL(_t_flag)}; class=class="str">"cmt">// add a new node to the beginning of the list
  class="kw">virtual class="type">void      AddRear(class="type">int &_node_arr[]){TRACE_CALL(_t_flag)};   class=class="str">"cmt">// add a new node to the end of the list
  class=class="str">"cmt">//--- deleting nodes    
  class="type">int               RemoveFront(class="type">void);                                         class=class="str">"cmt">// class="kw">delete the head node      
  class="type">int               RemoveRear(class="type">void);                                          class=class="str">"cmt">// class="kw">delete the node from the end of the list
  };
把链表诊断交给小布
小布盯盘已内置 MQL5 标准库容器的结构速览,打开对应品种页即可对照 CList 派生类的节点关系,省去翻源码的时间。

常见问题

单向节点只持下一个引用,遍历和释放更简单;双向节点多一个前驱指针,支持反向扫描与松散链表,但维护成本略高。
可以,小布盯盘的品种页内置了标准库容器视图,能列出 CList、CArray 等派生关系,方便你核对自己的链表实现。
倾向适合,图形对象常需首尾衔接遍历与动态增删,循环双链能避免边界判断,例子里多用 CObject 派生链管理。
标准库覆盖通用场景,特殊内存布局或跨品种虚拟交易簿可能要自定义松散链表,理解节点才能改得动。