MQL5 编程基础:列表·进阶篇
(2/3)· 数组之后为什么还需要链表?这篇把线性链表的写法与标准库工具一次讲透
「双向链表节点的挂载与回读验证」
在 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 跑久了内存占用会悄悄爬升。外汇贵金属品种波动大、杠杆高,这类底层结构测试请在策略测试器里跑,别直接挂实盘。
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 做随机种子填数,节点构造时就把数组收编。外汇与贵金属行情的高波动下,这类结构适合把多周期指标缓存进节点做回看,但实盘信号仍可能失效,须以历史回测验证。
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 吞吐。
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。外汇与贵金属杠杆高,这类结构仅用于本地数据组织,不直接产生交易信号。
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 里确认释放干净。
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 };