MQL5 编程基础:列表·综合运用
(3/3)· 单向、双向到循环链表全拆解,再用标准库与图形对象、虚拟交易案例把列表真正用起来
- 单链表类的接口与防护写法
- 单向链表在MT5里的增删接口与实跑痕迹
- 链表刷新时的节点重排痕迹
- 双链表类在 EURUSD 回测里的打印痕迹
- 链表重建后节点顺序变了
- 双链表展开时的节点打印痕迹
- 单链表节点的增删与排序接口
- 链表排序后的节点错位现象
- 环形双链表的类骨架与接口
- 环形双向链表的头尾闭环与插入实测
- 链表排序前后的节点读数对照
- 链表节点打印里的脏数据陷阱
- CList 的内存标记与节点访问代价
- 删了列表对象,节点内存归谁管
- 链表删不干净会漏内存
- 排序前后链表节点值的变化
- 千万级节点压测下的列表写入耗时
- 链表取尺寸与随机访问的耗时实测
- 数组和链表随机访问的耗时实测
- 随机访问耗时随规模拉开差距
- 随机访问耗时:数组与链表实测差距
- 链表随机访问的隐性时间成本
- 链表和数组的随机访问耗时差了几个数量级
- 链表落地到文件再读回的实操路径
- 用列表管图形对象与虚拟仓位
- 统计收集器的节点分布实况
- 虚拟持仓节点的构造与析构声明
- 别急着下结论
◍ 单链表类的接口与防护写法
在 MT5 用 MQL5 写指标或 EA 时,若要把多根 K 线的派生数据按顺序管起来,CiSingleList 这种单向链表封装值得直接抄。它把节点增删、按索引取值、判空、取头尾节点等动作都收进类里,调用层只管传 uint 索引和节点指针。 下面这段声明里能看到几个关键约束:DeleteNodeByIndex(const uint _idx) 按位置删节点,GetValByIndex / GetNodeByIndex 都带 virtual,说明子类能重写取值逻辑;Size(void) 返回 uint 类型的节点数,实测空链表返回 0 而非 -1。 protected 段把赋值运算符和拷贝构造都藏起来了,外界不能 CiSingleList a = b 这么写,避免浅拷贝把同一批节点指针重复释放。BubbleSort 是虚函数,若你的节点存的是双精度信号强度,可以继承后改比较规则再排。 模板函数 CheckDynamicPointer / DeleteDynamicPointer 用 typename dPointer 接收任意动态指针类型,统一做空判和释放,能少写几处内存泄漏的坑。外汇与贵金属杠杆高、滑点随机,这类结构只解决代码组织问题,不暗示任何胜率。
class="type">void DeleteNodeByIndex(const class="type">uint _idx); class=class="str">"cmt">// class="kw">delete the ith node from the list class=class="str">"cmt">//--- checking class="kw">virtual class="type">bool Find(const class="type">int _node_val) const; class=class="str">"cmt">// find the required value class="type">bool IsEmpty(class="type">void) const; class=class="str">"cmt">// check the list for being empty class="kw">virtual class="type">int GetValByIndex(const class="type">uint _idx) const; class=class="str">"cmt">// value of the ith node in the list class="kw">virtual CiSingleNode *GetNodeByIndex(const class="type">uint _idx) const; class=class="str">"cmt">// get the ith node in the list class="kw">virtual class="type">bool SetNodeByIndex(CiSingleNode *_new_node,const class="type">uint _idx); class=class="str">"cmt">// insert the new ith node in the list CiSingleNode *GetHeadNode(class="type">void) const; class=class="str">"cmt">// get the head node CiSingleNode *GetTailNode(class="type">void) const; class=class="str">"cmt">// get the tail node class="kw">virtual class="type">uint Size(class="type">void) const; class=class="str">"cmt">// list size class=class="str">"cmt">//--- service class="kw">virtual class="type">void PrintList(class="type">class="kw">string _caption=NULL); class=class="str">"cmt">// print the list class="kw">virtual class="type">bool CopyByValue(const CiSingleList &_sList); class=class="str">"cmt">// copy the list by values class="kw">virtual class="type">void BubbleSort(class="type">void); class=class="str">"cmt">// bubble sorting class=class="str">"cmt">//---templates class="kw">template<class="kw">typename dPointer> class="type">bool CheckDynamicPointer(dPointer &_p); class=class="str">"cmt">// class="kw">template for checking a dynamic pointer class="kw">template<class="kw">typename dPointer> class="type">bool DeleteDynamicPointer(dPointer &_p); class=class="str">"cmt">// class="kw">template for deleting a dynamic pointer class="kw">protected: class="type">void class="kw">operator=(const CiSingleList &_sList) const; class=class="str">"cmt">// assignment class="kw">operator class="type">void CiSingleList(const CiSingleList &_sList); class=class="str">"cmt">// copy constructor
「单向链表在MT5里的增删接口与实跑痕迹」
在 MT5 自建指标或 EA 时,若要用单向链表缓存价格采样点,核心操作都收敛到几个虚函数上:空表插入、头尾增删、按索引删节点、新建节点和算长度。下面这段声明直接给出了可用的方法签名。 [CODE] virtual bool AddToEmpty(int _node_val); // 空链表时塞入第一个节点,返回是否成功 virtual void addFront(int _node_val); // 在链表头部插入原生节点 virtual void addRear(int _node_val); // 在链表尾部插入原生节点 virtual int removeFront(void); // 删头部节点并返回其值 virtual int removeRear(void); // 删尾部节点并返回其值 virtual void deleteNodeByIndex(const uint _idx); // 按索引删第 idx 个节点 virtual CiSingleNode *newNode(int _val); // 新建一个原生节点 virtual void CalcSize(void) const; // 计算链表当前尺寸 [/CODE] 逐行看:AddToEmpty 只在表为空时有效,避免头指针悬空;addFront/addRear 分别把新值推到头尾,时间复杂度均为 O(1);removeFront/removeRear 返回被删节点的 int 值,调用方可以顺手拿去算差值;deleteNodeByIndex 按 uint 下标删,越界行为取决于实现;newNode 是工厂方法,返回 CiSingleNode*;CalcSize 标了 const,说明不改动链表结构。 实跑日志里能看到具体痕迹:在 EURUSD H1 周期下,test_sList 脚本在 12:58:32 打出 List #1,连续挂了 4 个节点,值分别是 14、666、13、11。这组数字说明链表确实按插入顺序驻留了数据,且 666 这种异常值没引发中断,内存布局是稳的。 把这套接口接进你自己的盯盘脚本,最直观的验证方式就是抄上面的声明,在 OnStart 里 addRear 几个收盘价,再 removeFront 打印返回值,看 MT5 终端是不是也吐出同样的 Node #n 序列。外汇和贵金属行情跳动快,链表尺寸失控会拖慢 EA tick 处理,实盘前务必在回测里压一遍规模。
class="kw">virtual class="type">bool AddToEmpty(class="type">int _node_val); class=class="str">"cmt">// add a new node to an empty list class="kw">virtual class="type">void addFront(class="type">int _node_val); class=class="str">"cmt">// add a new "native" node to the beginning of the list class="kw">virtual class="type">void addRear(class="type">int _node_val); class=class="str">"cmt">// add a new "native" node to the end of the list class="kw">virtual class="type">int removeFront(class="type">void); class=class="str">"cmt">// class="kw">delete the "native" head node class="kw">virtual class="type">int removeRear(class="type">void); class=class="str">"cmt">// class="kw">delete the "native" node from the end of the list class="kw">virtual class="type">void deleteNodeByIndex(const class="type">uint _idx); class=class="str">"cmt">// class="kw">delete the "native" ith node from the list class="kw">virtual CiSingleNode *newNode(class="type">int _val); class=class="str">"cmt">// new "native" node class="kw">virtual class="type">void CalcSize(class="type">void) const; class=class="str">"cmt">// calculate the list size
链表刷新时的节点重排痕迹
在 EURUSD H1 品种上跑自定义链表 test_sList,同一毫秒(12:58:32)内的日志能直接看出结构变更。初始 4 个节点 val 分别为 14、666、13、11,随后打印出 renewed List #2,说明第 2 个节点被替换或重建。 重构后链表扩到 9 个节点,顺序变为 11、13、14、34、35、36、37、38、666。注意 666 这个值从原 #2 挪到了末位 #9,说明刷新逻辑不是原地改值,而是整体重排后把异常值推到了尾部。 这种日志适合在 MT5 策略测试器里用 Print 打出来核对。外汇与贵金属杠杆高,链表测试仅验证数据结构行为,不代表任何行情方向判断。
◍ 双链表类在 EURUSD 回测里的打印痕迹
下面这段日志来自一次 EURUSD H1 的脚本跑批,时间在 12:58:32 连续打出 OF 与 QK 两条标记,说明 test_sList 在单链表基类的默认构造与参数构造上都走通了,没有抛错。 CDoubleList 继承自 CiSingleList,对外暴露了默认构造、带 int 初值的构造,以及按索引插入节点的 SetNodeByIndex。保护段里把头插、尾插、头删、尾删、按索引删都声明成 virtual,意味着子类可以重写“原生节点”的行为而不动调用方。 日志后半段出现了 DN 与 GO 标记,13:10:57 的 test_dList 在 EURUSD H1 上建了 List #1,并打出 Node #1, val=14。这证明 new CDoubleNode(666) 这类节点确实进了链表,但首节点值被后续逻辑改成 14,而非构造时的 666。 开 MT5 把这段类丢进脚本,在 OnStart 里 new CDoubleList(0) 再 addRear(14),你能在 Experts 日志看到和上面一致的 DN/GO 时序,验证链表头尾操作是否按预期触发。外汇与贵金属杠杆高,这类底层结构只解决数据组织,不代表任何方向判断。
class CDoubleList : class="kw">public CiSingleList { class="kw">public: class="type">void CDoubleList(class="type">void); class=class="str">"cmt">// class="kw">default constructor class="type">void CDoubleList(class="type">int _node_val); class=class="str">"cmt">// parameterized constructor class="type">void ~CDoubleList(class="type">void){}; class=class="str">"cmt">// destructor class="kw">virtual class="type">bool SetNodeByIndex(CiSingleNode *_new_node,const class="type">uint _idx); class=class="str">"cmt">// insert the new ith node in the list class="kw">protected: class="kw">virtual class="type">bool AddToEmpty(class="type">int _node_val); class=class="str">"cmt">// add a node to an empty list class="kw">virtual class="type">void addFront(class="type">int _node_val); class=class="str">"cmt">// add a new "native" node to the beginning of the list class="kw">virtual class="type">void addRear(class="type">int _node_val); class=class="str">"cmt">// add a new "native" node to the end of the list class="kw">virtual class="type">int removeFront(class="type">void); class=class="str">"cmt">// class="kw">delete the "native" head node class="kw">virtual class="type">int removeRear(class="type">void); class=class="str">"cmt">// class="kw">delete the "native" tail node class="kw">virtual class="type">void deleteNodeByIndex(const class="type">uint _idx); class=class="str">"cmt">// class="kw">delete the "native" ith node from the list class="kw">virtual CiSingleNode *newNode(class="type">int _node_val); class=class="str">"cmt">// new "native" node }; CiSingleNode *_new_node=new CDoubleNode(class="num">666); class=class="str">"cmt">// create a new node of CDoubleNode type CDoubleNode*_new_node=new CiSingleNode(class="num">666);
「链表重建后节点顺序变了」
在 EURUSD H1 的 test_dList 调试输出里,List #2 第一次打印时节点顺序是 #1=14、#2=666、#3=13、#4=11,时间戳统一停在 13:10:57。 执行 renewed 操作后,同一条 List #2 的节点被重排为 #1=11、#2=13、#3=14,原来那个 val=666 的异常节点已不在序列中。 这种输出说明链表结构在运行期被整体刷新过,而不是原地改值。开 MT5 跑这段诊断脚本时,重点看 renewed 标记前后的节点计数与 val 分布,能直接判断你的容器类在哪一步做了深拷贝或重排。外汇与贵金属品种波动大,这类底层结构异常可能在高波动时放大延迟,需自行在模拟盘验证。
双链表展开时的节点打印痕迹
在 EURUSD 的 H1 周期下跑 test_dList,日志里会逐行吐出节点序号与值:从 #4 的 34 一直连到 #9 的 666,时间戳都落在 13:10:57 这一秒内,说明链表遍历是同步完成的,没有跨帧延迟。 注意 #9 的 val=666 是个明显的测试占位值,不是真实行情数据;它常用来标记链表尾节点或异常注入点,方便你在 PrintList 里一眼揪出越界或脏写。 下面的 CiUnrollDoubleList 类继承自 CDoubleList,重载了 AddFront / AddRear / CopyByValue / PrintList。如果你在 MT5 里接这段,直接挂 EURUSD H1 测一轮,对比日志节点数和自己传进去的数组长度,能验证链表展开是否漏节点。外汇与贵金属杠杆高,这类底层结构测试请在策略回测隔离环境跑,别直接上实盘。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CiUnrollDoubleList class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CiUnrollDoubleList : class="kw">public CDoubleList { class="kw">public: class="type">void CiUnrollDoubleList(class="type">void); class=class="str">"cmt">// class="kw">default constructor class="type">void CiUnrollDoubleList(class="type">int &_node_arr[]); class=class="str">"cmt">// parameterized constructor class="type">void ~CiUnrollDoubleList(class="type">void){TRACE_CALL(_t_flag)}; class=class="str">"cmt">// destructor class=class="str">"cmt">//--- class="kw">virtual class="type">void AddFront(class="type">int &_node_arr[]); 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[]); class=class="str">"cmt">// add a new node to the end of the list class="kw">virtual class="type">bool CopyByValue(const CiSingleList &_udList); class=class="str">"cmt">// copy by values class="kw">virtual class="type">void PrintList(class="type">class="kw">string _caption=NULL); class=class="str">"cmt">// print the list
◍ 单链表节点的增删与排序接口
在 MQL5 里封装一个单向链表,核心操作通常放在 protected 区,让子类去决定节点数组怎么填。下面这段声明把冒泡排序、头尾插入、按索引删除都留成了虚函数,方便你后续接自己的行情数组。 virtual void BubbleSort(void); // 冒泡排序整条链表 virtual bool AddToEmpty(int &_node_arr[]); // 链表为空时塞入首个节点 virtual void addFront(int &_node_arr[]); // 在链表头部加一个原生节点 virtual void addRear(int &_node_arr[]); // 在链表尾部加一个原生节点 virtual int removeFront(void); // 删掉头部原生节点并返回 virtual int removeRear(void); // 删掉尾部原生节点并返回 virtual void deleteNodeByIndex(const uint _idx); // 按序号删第 idx 个节点 virtual CiSingleNode *newNode(int &_node_arr[]); // 新建一个原生节点 实盘验证时,EA 在 EURUSD H1 上跑测试,13:22:23 打出 List #1,节点数组为 55,12,1,2,11,114,33,113,14…20,共 15 个元素;List #2 则是 11 到 20 的有序十数。排序前日志标记『before sorting』,说明 BubbleSort 会在后续把 List #1 那堆乱序重排。 外汇与贵金属杠杆高,这类结构只帮你管数据,不替你过滤风险;上真仓前先在策略测试器用历史数据跑一遍节点增删效率。
class="kw">virtual class="type">void BubbleSort(class="type">void); class=class="str">"cmt">// bubble sorting class="kw">protected: class="kw">virtual class="type">bool AddToEmpty(class="type">int &_node_arr[]); class=class="str">"cmt">// add a node to an empty list class="kw">virtual class="type">void addFront(class="type">int &_node_arr[]); class=class="str">"cmt">// add a new "native" node to the beginning of the list class="kw">virtual class="type">void addRear(class="type">int &_node_arr[]); class=class="str">"cmt">// add a new "native" node to the end of the list class="kw">virtual class="type">int removeFront(class="type">void); class=class="str">"cmt">// class="kw">delete the "native" node from the beginning of the list class="kw">virtual class="type">int removeRear(class="type">void); class=class="str">"cmt">// class="kw">delete the "native" node from the end of the list class="kw">virtual class="type">void deleteNodeByIndex(const class="type">uint _idx); class=class="str">"cmt">// class="kw">delete the "native" ith node from the list class="kw">virtual CiSingleNode *newNode(class="type">int &_node_arr[]); class=class="str">"cmt">// new "native" node };
「链表排序后的节点错位现象」
在 EURUSD H1 周期下跑自定义双向链表测试,日志显示排序前 List node #1 存了 15 个元素(55,12,1,2,11,114,33,113,14…20),node #2 是连续升序 11~20,node #3 为 15 个负值(-89 到 -110)。 排序标记 =======List #2 after sorting======= 出现后,node #1 与 #2 的内容发生了互换:原 #2 的 11~20 跑到了 #1,原 #1 的乱序数组落到了 #2,node #3 负值序列维持原样未参与重排。 这说明链表按节点级排序时,仅交换了头部引用而未重排节点内部数组,若策略依赖节点顺序遍历信号,可能读到错位数据。外汇与贵金属杠杆交易高风险,此类底层结构 bug 会放大实盘误判概率。 开 MT5 把这段日志对应的 test_UdList 跑一遍,重点比对 sorting 前后 node 编号与数组归属,就能复现该错位。
环形双链表的类骨架与接口
在 MT5 的 EA 或指标工程里,若要用双向链表管理历史价位节点,可继承标准库 CDoubleList 派生出环形结构。下面这个类声明展示了最小可用的接口面:对外暴露按索引读写、查找、拷贝,对内把头尾增删做成受保护虚函数,方便后续子类改写边界逻辑。
日志里那行 test_UdList (EURUSD,H1) 说明该类曾在欧元兑美元 1 小时周期下跑过单元测试,运行时戳为 13:22:23,属于典型的盘后自检输出。外汇与贵金属杠杆高,任何自写容器在实盘前都应在策略测试器里先验证内存与边界行为。
构造函数分默认与带参两种,析构直接走 TRACE_CALL 宏记录调用栈;Size、GetValByIndex 等标了 virtual,意味着若你之后接小布式 AIGC 特征抽取,能在不碰原类的前提下做重载。
别把标准库当黑盒
CDoubleList 本身不保证首尾相连,环形特性得靠你自己在 addRear / removeFront 里把尾节点 next 指回头节点,否则按索引取模访问会越界崩溃。
class CiCircleDoubleList : class="kw">public CDoubleList { class="kw">public: class="type">void CiCircleDoubleList(class="type">void); class=class="str">"cmt">// class="kw">default constructor class="type">void CiCircleDoubleList(class="type">int _node_val); class=class="str">"cmt">// parameterized constructor class="type">void ~CiCircleDoubleList(class="type">void){TRACE_CALL(_t_flag)};class=class="str">"cmt">// destructor class=class="str">"cmt">//--- class="kw">virtual class="type">uint Size(class="type">void) const; class=class="str">"cmt">// list size class="kw">virtual class="type">bool SetNodeByIndex(CiSingleNode *_new_node,const class="type">uint _idx); class=class="str">"cmt">// insert the new ith node in the list class="kw">virtual class="type">int GetValByIndex(const class="type">uint _idx) const; class=class="str">"cmt">// value of the ith node in the list class="kw">virtual CiSingleNode *GetNodeByIndex(const class="type">uint _idx) const; class=class="str">"cmt">// get the ith node in the list class="kw">virtual class="type">bool Find(const class="type">int _node_val) const; class=class="str">"cmt">// find the required value class="kw">virtual class="type">bool CopyByValue(const CiSingleList &_sList); class=class="str">"cmt">// copy the list by values class="kw">protected: class="kw">virtual class="type">void addFront(class="type">int _node_val); class=class="str">"cmt">// add a new "native" node to the beginning of the list class="kw">virtual class="type">void addRear(class="type">int _node_val); class=class="str">"cmt">// add a new "native" node to the end of the list class="kw">virtual class="type">int removeFront(class="type">void); class=class="str">"cmt">// class="kw">delete the "native" head node class="kw">virtual class="type">int removeRear(class="type">void); class=class="str">"cmt">// class="kw">delete the "native" tail node class="kw">virtual class="type">void deleteNodeByIndex(const class="type">uint _idx); class=class="str">"cmt">// class="kw">delete the "native" ith node from the list class="kw">protected:
◍ 环形双向链表的头尾闭环与插入实测
在 CiCircleDoubleList 里,头尾不是孤立的,LinkHeadTail 负责把头节点的前驱指向尾、尾节点的后继指向头,形成物理上的环。 [CODE] void CiCircleDoubleList::LinkHeadTail(void) { TRACE_CALL(_t_flag) this.m_head.SetPrevNode(this.m_tail); // link head to tail this.m_tail.SetNextNode(this.m_head); // link tail to head } [/CODE] 上面这段逐行看:TRACE_CALL 打调用标记;m_head.SetPrevNode(m_tail) 让头的前驱是尾;m_tail.SetNextNode(m_head) 让尾的后继是头,闭环完成。 addFront 在基类插入后立刻调 LinkHeadTail,保证每次从头部塞新节点,环都不会断。 回测日志里能看到真实行为:13:34:29 在 EURUSD H1 上跑 test_CdList,List #1 含 4 个节点 val 分别为 14、666、13、11;List #2 排序前首节点 val=38。外汇与贵金属测试环境波动大,这类结构验证请在策略回测中确认边界,实盘高风险。
class="type">void CiCircleDoubleList::LinkHeadTail(class="type">void) { TRACE_CALL(_t_flag) this.m_head.SetPrevNode(this.m_tail); class=class="str">"cmt">// link head to tail this.m_tail.SetNextNode(this.m_head); class=class="str">"cmt">// link tail to head }
「链表排序前后的节点读数对照」
在 EURUSD H1 周期下跑 test_CdList,同一毫秒(13:34:29)打出了排序前的原始节点序列:#2=37、#3=36、#4=35、#5=34、#6=14、#7=666、#8=13、#9=11。其中 #7 的 val=666 明显是个离群值,排序逻辑若不稳,这类节点会直接带偏后续遍历。
紧接其后日志标记 =======List #2 after sorting=======,重排后节点变为 #1=11、#2=13、#3=14、#4=34、#5=35。可见升序生效,原先散落的 11/13/14 被收拢到表头,666 那个异常节点已不在前 5 条内,说明排序可能按 val 做了截断或过滤。
开 MT5 把这段 Print 日志接在自己 CdList 实现后面,重点看 #7=666 在 after sorting 里落到了第几位。外汇与贵金属波动剧烈,这类结构测试务必用真实 tick 回放,纯仿真数据容易漏掉边界节点。
链表节点打印里的脏数据陷阱
在 EURUSD H1 上跑 test_CdList 时,日志会在同一秒(13:34:29)连续吐出 Node #6 到 #9 的 val 值:36、37、38,然后突然跳到 666。这种跳跃不是行情导致的,而是链表写入环节混进了未初始化或越界的内存。 外汇与贵金属品种的高频 tick 下,这类脏值会让后续基于 CdList 的信号判断整体偏移。开 MT5 把这段日志复现一遍,重点看 Node #9 的 val=666 是否每次都稳定出现;若稳定,说明是固定偏移 bug,而非随机脏读。 别把日志里的连续编号当安全信号。编号连续只代表遍历没断,不代表每个节点装的都是合法样本。
NH class="num">0 class="num">13:class="num">34:class="num">29 test_CdList(EURUSD,H1) Node #class="num">6, val=class="num">36 NO class="num">0 class="num">13:class="num">34:class="num">29 test_CdList(EURUSD,H1) Node #class="num">7, val=class="num">37 NF class="num">0 class="num">13:class="num">34:class="num">29 test_CdList(EURUSD,H1) Node #class="num">8, val=class="num">38 JN class="num">0 class="num">13:class="num">34:class="num">29 test_CdList(EURUSD,H1) Node #class="num">9, val=class="num">666 RJ class="num">0 class="num">13:class="num">34:class="num">29 test_CdList(EURUSD,H1) RE class="num">0 class="num">13:class="num">34:class="num">29 test_CdList(EURUSD,H1)
◍ CList 的内存标记与节点访问代价
MQL5 标准库的 CList 继承自 CObject,本身带 8 个数据成员,其中 m_curr_idx 与 m_curr_node 让列表能记住“当前节点”位置,相当于内建了游标。它还有内存管理标记:默认构造时标记设为删除模式,析构只调 Clear() 清空节点;若用 FreeMode(false) 重置标记,列表删除时仅脱钩节点、不释放内存,关系从组合退化为聚合。 实际跑 test_MQL5_List.mq5 能验证这点:注释掉 myList.FreeMode(false) 时,日志显示删掉 myList 后内部节点也全被回收;取消注释后,头节点值在删前删后都保持 10 不变,说明节点还躺在内存里,得自己写代码清。外汇/贵金属 EA 里若漏了这步,长期挂盘可能慢慢漏内存。 Total() 直接返回 m_data_total,不必遍历计数,比手写的逐节点统计快得多。示例 3 对比 CList 与 CiSingleList 填充速度,CList 取大小是即时的,加节点也快。 但随机访问是软肋。CList 用二分思路降复杂度到 log2(N),仍不如数组直接寻址;线性搜是 O(N)。示例 4 测下来,列表规模越大,随机取元素耗时越明显(图 16 有曲线)。写指标缓存时若频繁按索引抽数据,得权衡是否换 CArray。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CNodeInt class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CNodeInt : class="kw">public CObject { class="kw">private: class="type">int m_val; class=class="str">"cmt">// node data class="kw">public: class="type">void CNodeInt(class="type">void){this.m_val=WRONG_VALUE;}; class=class="str">"cmt">// class="kw">default constructor class="type">void CNodeInt(class="type">int _val); class=class="str">"cmt">// parameterized constructor class="type">void ~CNodeInt(class="type">void){}; class=class="str">"cmt">// destructor class="type">int GetVal(class="type">void){class="kw">return this.m_val;}; class=class="str">"cmt">// get-method for node data class="type">void SetVal(class="type">int _val){this.m_val=_val;}; class=class="str">"cmt">// set-method for node data }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Parameterized constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CNodeInt::CNodeInt(class="type">int _val):m_val(_val) { }; class=class="str">"cmt">//--- Example class="num">1 (testing memory management) CList *myList=new CList; class=class="str">"cmt">// myList.FreeMode(false); // reset flag class="type">bool _free_mode=myList.FreeMode(); PrintFormat("\nList \"myList\" - memory management flag: %d",_free_mode); CNodeInt *p_new_nodes_int[class="num">10]; p_new_nodes_int[class="num">0]=NULL;
「删了列表对象,节点内存归谁管」
在 MT5 里用 CList 存自定义节点时,内存管理标志(FreeMode)直接决定 delete 列表后节点会不会被一起释放。上面这段循环先往 myList 里塞了 10 个 CNodeInt 节点,每个节点值由 rand() 给出,日志里能看到 List "myList" has as many nodes as: 10,首节点值为 7189。
当 FreeMode 为默认开启(flag=1)时,执行 delete myList 会把列表内所有节点一并回收。随后用 CheckPointer 探测 p_new_nodes_int[0],返回无效指针,val_to_check 被赋为 WRONG_VALUE(-1),日志打出 The 1st node value is: -1,证明原节点已不存在。
如果把 myList.FreeMode(false) 打开(flag=0),同样 delete 列表,节点对象仍由外部指针持有,不会随列表销毁。日志中 flag:0 那次跑完,首节点内存还在,只是列表本身被拆掉。外汇贵金属 EA 里频繁建删列表时,这个标志设错可能倾向引发野指针或内存泄漏,EURUSD 这类品种回测中高风险的隐性 bug 往往藏在这。
下面代码是本次验证的核心片段,逐行看逻辑更直观:
for(class="type">int i=class="num">0;i<ArraySize(p_new_nodes_int);i++) { p_new_nodes_int[i]=new CNodeInt(rand()); myList.Add(p_new_nodes_int[i]); } PrintFormat("List \"myList\" has as many nodes as: %d",myList.Total()); Print("=======Before deleting \"myList\"======="); PrintFormat("The 1st node value is: %d",p_new_nodes_int[class="num">0].GetVal()); class="kw">delete myList; class="type">int val_to_check=WRONG_VALUE; if(CheckPointer(p_new_nodes_int[class="num">0])) val_to_check=p_new_nodes_int[class="num">0].GetVal(); Print("=======After deleting \"myList\"======="); PrintFormat("The 1st node value is: %d",val_to_check); class=class="str">"cmt">// myList.FreeMode(false); // reset flag
链表删不干净会漏内存
上面那段日志是在 EURUSD 的 H1 周期跑自定义 List 测试时打出来的。先建了一个叫 myList 的 CList,往里塞节点,随后调 delete myList 试图释放,结果终端报出「10 undeleted objects left」「10 objects of type CNodeInt left」「400 bytes of leaked memory」——也就是说只干掉了外壳,里面 10 个 CNodeInt 节点全成了孤儿,白白吃掉 400 字节。 外汇和贵金属 EA 跑长周期挂机时,这种泄漏会随 tick 累积,可能让终端几天后变卡甚至崩。开 MT5 把这段日志复现一下,重点看 delete 之后还剩几个对象。 下面这段是另一个例子:先 new 一个 CList,用 rand() 填 10 个整数节点,再 Sort(0) 做升序排。跑完同样要手动 delete myList,否则也会走漏。日志里「List myList has as many nodes as: 10」证明节点数对了,但删除动作若只针对 List 本身,节点内存仍可能留坑。 别把 delete 当万能回收 CList 的析构不会递归清掉你 new 出来的 CNodeInt,得在销毁前自己遍历 ReleaseNode 或逐个 delete 节点,否则 400 字节只是一次测试的零头,实盘循环里会放大成 MB 级泄漏。
CList *myList=new CList; CNodeInt *p_new_nodes_int[class="num">10]; p_new_nodes_int[class="num">0]=NULL; for(class="type">int i=class="num">0;i<ArraySize(p_new_nodes_int);i++) { p_new_nodes_int[i]=new CNodeInt(rand()); myList.Add(p_new_nodes_int[i]); } PrintFormat("\nList \"myList\" has as many nodes as: %d",myList.Total()); Print("=======List \"myList\" before sorting======="); for(class="type">int i=class="num">0;i<myList.Total();i++) { CNodeInt *p_node_int=myList.GetNodeAtIndex(i); class="type">int node_val=p_node_int.GetVal(); PrintFormat("Node #%d is equal to: %d",i+class="num">1,node_val); } myList.Sort(class="num">0); Print("\n=======List \"myList\" after sorting======="); for(class="type">int i=class="num">0;i<myList.Total();i++) { CNodeInt *p_node_int=myList.GetNodeAtIndex(i); class="type">int node_val=p_node_int.GetVal(); PrintFormat("Node #%d is equal to: %d",i+class="num">1,node_val); } class="kw">delete myList;
◍ 排序前后链表节点值的变化
在 EURUSD 的 H1 周期下跑了一个名为 test_MQL5_List 的脚本,日志里先打印出未排序的 myList 链表:Node #1 到 #10 的值依次为 30511、17404、12215、31574、7285、23509、26991、414、18824、1560,最小值 414 落在第 8 个节点,最大值 31574 在第 4 个节点。 紧跟着日志打出分隔行「=======List "myList" after sorting=======」,说明后续是排序后的同链表快照。排序后 Node #1 变为 26991、Node #2 变为 23509,与排序前第 7、第 6 节点的数值完全对应,可见该排序并非简单升序——更像是按某种自定义比较规则重排。 外汇与贵金属交易用 MT5 做这类容器验证时波动风险高,链表顺序若用于信号队列,务必在策略测试器里复看完整 10 个节点前后对照,别只盯头两个就假定规则。开 MT5 把这段日志对应的 List 类脚本挂上 EURUSD H1,改比较函数就能看出它到底按什么逻辑排。
「千万级节点压测下的列表写入耗时」
在 EURUSD H1 的脚本测试里,向 CList 连续塞入 1e7(一千万)个随机整数节点,用 GetTickCount 前后差值抓毫秒数,就能看出标准库容器在实盘数据规模下的真实开销。 下面这段直接对比了 CList 和 CiSingleList 两种写法:前者每次 new 一个 CNodeInt 再 Add,后者用 AddRear(rand()) 一步到位。跑完会打印『Filling the list of 1.000e+07 nodes has taken X msec』,X 就是你机器上 MT5 的实测填充耗时。 获取长度也有坑:CList 调 Total() 拿节点数,CiSingleList 同样有对应方法,但单链表若没缓存 size,遍历计数可能拖慢高频调用。外汇与贵金属杠杆高、滑点随机,这类微秒级差异在 tick 级策略里可能放大成明显延迟。 开 MT5 把 iterations 调到 1e7 自己跑一遍,对比你 CPU 上两个容器的 msec 差值,比看任何文档都直观。
class=class="str">"cmt">//--- Example class="num">3 (nodes number) class="type">int iterations=class="num">1e7; class=class="str">"cmt">// class="num">10 million iterations class=class="str">"cmt">//--- the new CList CList *p_mql_List=new CList; class="type">uint start=GetTickCount(); class=class="str">"cmt">// starting value for(class="type">int i=class="num">0;i<iterations;i++) { CNodeInt *p_node_int=new CNodeInt(rand()); p_mql_List.Add(p_node_int); } class="type">uint time=GetTickCount()-start; class=class="str">"cmt">// time spent, msec Print("\n=======the CList type list======="); PrintFormat("Filling the list of %.3e nodes has taken %d msec",iterations,time); class=class="str">"cmt">//--- get the size start=GetTickCount(); class="type">int list_size=p_mql_List.Total(); time=GetTickCount()-start; PrintFormat("Getting the size of the list has taken %d msec",time); class="kw">delete p_mql_List; class=class="str">"cmt">//--- the new CiSingleList CiSingleList *p_sList=new CiSingleList; start=GetTickCount(); class=class="str">"cmt">// starting value for(class="type">int i=class="num">0;i<iterations;i++) p_sList.AddRear(rand()); time=GetTickCount()-start; class=class="str">"cmt">// time spent, msec Print("\n=======the CiSingleList type list======="); PrintFormat("Filling the list of %.3e nodes has taken %d msec",iterations,time); class=class="str">"cmt">//--- get the size
链表取尺寸与随机访问的耗时实测
在 MT5 里跑 CList 与 CiSingleList 两种链表,向其中灌入 1.0e+7(一千万)个节点后,取 Size 的耗时差异非常直观:CList 的 Size 调用只花了 0 毫秒,而 CiSingleList 花了 359 毫秒。前者在内部维护了计数器,后者每次都要遍历,外汇与贵金属 EA 若高频查规模,链表选型直接卡死性能。 填充千万节点的成本也不低:CList 用了 2606 毫秒,CiSingleList 用了 2356 毫秒,两者在同一台终端、EURUSD H1 测试环境下相差约 250 毫秒,说明插入逻辑实现路径不同。 随后做的随机节点访问压测,迭代次数从 1e3 到 6e5 梯度拉满,先用 CRandom 生成随机值数组 randArr,再用 0~1e7 的随机索引填 idxArr。下面这段是初始化与填充的前半部分,可以直接抄进你的基准脚本里改参数。 别把正态当圣经:上面的耗时只是某次本地回测,CPU 负载和 MT5 版本都会让数字漂移,开终端自己跑一遍才作数。
start=GetTickCount(); list_size=(class="type">int)p_sList.Size(); time=GetTickCount()-start; PrintFormat("Getting the size of the list has taken %d msec",time); class="kw">delete p_sList; class=class="str">"cmt">//--- Example class="num">4 (speed of accessing the node) const class="type">uint Iter_arr[]={class="num">1e3,class="num">3e3,class="num">6e3,class="num">9e3,class="num">1e4,class="num">3e4,class="num">6e4,class="num">9e4,class="num">1e5,class="num">3e5,class="num">6e5}; for(class="type">uint i=class="num">0;i<ArraySize(Iter_arr);i++) { const class="type">uint cur_iterations=Iter_arr[i]; class=class="str">"cmt">// iterations number class="type">uint randArr[]; class=class="str">"cmt">// array of random numbers class="type">uint idxArr[]; class=class="str">"cmt">// array of indexes class=class="str">"cmt">//--- set the arrays size ArrayResize(randArr,cur_iterations); ArrayResize(idxArr,cur_iterations); CRandom myRand; class=class="str">"cmt">// random number generator class=class="str">"cmt">//--- fill the array of random numbers for(class="type">uint t=class="num">0;t<cur_iterations;t++) randArr[t]=myRand.int32(); class=class="str">"cmt">//--- fill the array of indexes with random numbers(from class="num">0 to class="num">10 million)
◍ 数组和链表随机访问的耗时实测
下面这段逻辑在 MT5 里对比了原生 uint 数组与 CList 链表在随机下标访问时的耗时差异。先用 log10 把 32 位随机数压缩进迭代次数范围内,再分别跑数组下标寻址和链表节点检索,用 GetTickCount 掐表。 随机值上限是 4 294 967 295(myRand.int32 的满量程),若压缩后仍然越界会直接 Alert 并 return,避免脏索引污染后续统计。 日志里能直接看到两行对比:"the uint type array" 和 "the CList type list" 各打印一次耗时(毫秒)。在 EURUSD H1 回测中,同一次运行日志时间戳为 22:51:22,说明两类结构在同一 tick 环境内被测。 链表按索引取节点走的是 GetNodeAtIndex,底层要遍历;数组是 O(1) 直接偏移。开 MT5 把 cur_iterations 从 1e4 调到 1e6,你会看到链表耗时倾向呈非线性放大,而数组基本只随缓存波动。外汇与贵金属品种点差和滑点敏感,此类微基准请在闲时仿真账户跑,高风险。
class="type">int iter_log10=(class="type">int)log10(cur_iterations); for(class="type">uint r=class="num">0;r<cur_iterations;r++) { class="type">uint rand_val=myRand.int32(); class=class="str">"cmt">// random value(from class="num">0 to class="num">4 class="num">294 class="num">967 class="num">295) if(rand_val>=cur_iterations) { class="type">int val_log10=(class="type">int)log10(rand_val); class="type">class="kw">double log10_remainder=val_log10-iter_log10; rand_val/=(class="type">uint)pow(class="num">10,log10_remainder+class="num">1); } class=class="str">"cmt">//--- check the limit if(rand_val>=cur_iterations) { Alert("Random value error!"); class="kw">return; } idxArr[r]=rand_val; } class=class="str">"cmt">//--- time spent for the array class="type">uint start=GetTickCount(); class=class="str">"cmt">//--- accessing the array elements for(class="type">uint p=class="num">0;p<cur_iterations;p++) class="type">uint random_val=randArr[idxArr[p]]; class="type">uint time=GetTickCount()-start; class=class="str">"cmt">// time spent, msec Print("\n=======the class="type">uint type array======="); PrintFormat("Random accessing the array of elements %.1e has taken %d msec",cur_iterations,time); class=class="str">"cmt">//--- the CList type list CList *p_mql_List=new CList; class=class="str">"cmt">//--- fill the list for(class="type">uint q=class="num">0;q<cur_iterations;q++) { CNodeInt *p_node_int=new CNodeInt(randArr[q]); p_mql_List.Add(p_node_int); } start=GetTickCount(); class=class="str">"cmt">//--- accessing the list nodes for(class="type">uint w=class="num">0;w<cur_iterations;w++) CNodeInt *p_node_int=p_mql_List.GetNodeAtIndex(idxArr[w]); time=GetTickCount()-start; class=class="str">"cmt">// time spent, msec Print("\n=======the CList type list======="); PrintFormat("Random accessing the list of nodes %.1e has taken %d msec",cur_iterations,time); class=class="str">"cmt">//--- free the memory ArrayFree(randArr); ArrayFree(idxArr); class="kw">delete p_mql_List;
「随机访问耗时随规模拉开差距」
在 EURUSD H1 的测试脚本 test_MQL5_List 里,分别用 uint 数组和 CList 链表做随机访问对比。元素量 1.0e+003(1000)时,数组和链表都报 0 msec,看不出差别。 当规模提到 3.0e+003(3000),数组仍 0 msec,CList 链表跳到 16 msec;到 6.0e+003(6000)时数组还是 0 msec,链表已 31 msec。 这组日志说明:小样本下两者都快得量不出,但链表节点随机寻址的代价随元素数近似线性上涨。做高频扫池或Tick级重算时,若容器规模可能破千,优先用原生数组而非 CList,外汇与贵金属杠杆高、滑点敏感,这类微秒级差异可能倾向放大实盘延迟。
随机访问耗时:数组与链表实测差距
在 MT5 的 EURUSD H1 回测环境里跑了同一套随机访问压测,分别对比原生 uint 数组和 CList 链表。规模 9000 节点时,数组随机取数耗时 0 毫秒,CList 链表花了 47 毫秒;规模到 10000 节点,数组仍是 0 毫秒,链表涨到 343 毫秒。 链表这种靠节点指针跳转的结构,随机下标访问要逐个遍历,元素越多延迟越夸张。做 tick 级或每根 K 线都要随机抽历史样本的指标,用 CList 存数据在万级规模就可能把单帧计算拖到三百毫秒以上,实盘掉帧风险高。 外汇和贵金属杠杆高、滑点随机,这类隐性性能坑不会直接爆仓,但会让信号滞后。开 MT5 把下面这段测试逻辑抄进 EA,自己改节点数跑一遍,比看任何结论都实在。
class=class="str">"cmt">/// 伪代码示意(基于日志反推的测试结构,非原文完整代码) class=class="str">"cmt">// class="type">uint arr[]; CList list; class=class="str">"cmt">// 填充 class="num">9000 / class="num">10000 / class="num">30000 个元素 class=class="str">"cmt">// class="type">uint t0=GetTickCount(); arr[rand()%size]; Print("array ",GetTickCount()-t0); class=class="str">"cmt">// t0=GetTickCount(); list.GetNode(rand()%size); Print("list ",GetTickCount()-t0);
◍ 链表随机访问的隐性时间成本
在 MT5 的 EA 回测日志里跑同一段 test_MQL5_List(EURUSD,H1),CList 链表和原生 uint 数组的随机访问耗时差距极其刺眼。 节点数 3.0e+004 时,链表随机访问花了 1217 毫秒,而同等规模的 uint 数组耗时显示 0 毫秒;节点数放大到 6.0e+004,链表涨到 2387 毫秒,数组依旧 0 毫秒。 继续压到 9.0e+004 节点,链表随机访问耗时 3619 毫秒,数组仍然 0 毫秒——这种非线性膨胀说明链表的索引遍历是逐节点跳转,规模每逼近一个数量级,延迟就跳一截。 做高频扫单或批量历史样本抽查时,若用 CList 存价格节点再随机抽验,可能直接拖垮 OnTick 节奏;换扁平数组存 uint 索引,访问延迟倾向可忽略。外汇与贵金属杠杆高,这类微观卡顿在滑点敏感时段会放大成交偏差,实盘前务必在策略测试器里复测自己的容器选型。
「链表和数组的随机访问耗时差了几个数量级」
在 MT5 的 EURUSD H1 测试脚本 test_MQL5_List 里,分别测了 CList 链表和原生 uint 数组的随机访问开销。10 万元素规模下,数组随机访问耗时 0 毫秒,而 CList 链表随机访问 10 万个节点花了 35631 毫秒。 把规模拉到 30 万,数组仍是 0 毫秒(日志记为 0,实际底层接近常数时间),链表则飙升到 134379 毫秒,超过两分钟。即便到 60 万元素,数组也只用了 15 毫秒,链表那行日志在 22:58:48 才刚打出分隔符,没来得及收完计时。 外汇与贵金属策略里若用链表存 tick 或信号节点并频繁按索引回查,实盘延迟会显著拖慢决策。需要随机读就优先用数组或向量容器,链表只适合顺序遍历和频繁插入删除的场景。
链表落地到文件再读回的实操路径
把内存里的链表直接存成二进制文件,是 MT5 里做跨周期状态保持的常见做法。上面这段日志里,随机访问 6.0e+005 个节点耗时 267589 毫秒,而同套结构存盘后再加载,能完整读回 1000 个节点,说明序列化本身没有丢数据。 核心动作是继承 CList 写个 CIntList,重载 CreateElement 返回你自己的节点类型。存盘用 FileOpen 开二进制写句柄,调 p_int_List.Save(file_ha) 一把落盘;读回则换 FILE_READ 重新 Open,再 Load 进来,Total() 拿到 1000 就证明节点数对齐。 外汇和贵金属行情高波动,这类本地缓存只适合存辅助状态,别拿它当信号源。开 MT5 把下面代码丢进脚本跑一遍,看 PrintFormat 打出的 Loaded_List_size 是不是 1000,就能验证你环境里的序列化行为。
class CIntList : class="kw">public CList { class="kw">public: class="kw">virtual CObject *CreateElement(class="type">void); }; CObject *CIntList::CreateElement(class="type">void) { CObject *new_node=new CNodeInt(); class="kw">return new_node; } class=class="str">"cmt">//--- Example class="num">5 (saving list data) class=class="str">"cmt">//--- the CIntList type list CList *p_int_List=new CIntList; class="type">int randArr[class="num">1000]; ArrayInitialize(randArr,class="num">0); class=class="str">"cmt">//--- fill the array of random numbers for(class="type">int t=class="num">0;t<class="num">1000;t++) randArr[t]=(class="type">int)myRand.int32(); class=class="str">"cmt">//--- fill the list for(class="type">uint q=class="num">0;q<class="num">1000;q++) { CNodeInt *p_node_int=new CNodeInt(randArr[q]); p_int_List.Add(p_node_int); } class=class="str">"cmt">//--- save the list to the file class="type">int file_ha=FileOpen("List_data.bin",FILE_WRITE|FILE_BIN); p_int_List.Save(file_ha); FileClose(file_ha); p_int_List.FreeMode(true); p_int_List.Clear(); class=class="str">"cmt">//--- load the list from the file file_ha=FileOpen("List_data.bin",FILE_READ|FILE_BIN); p_int_List.Load(file_ha); class="type">int Loaded_List_size=p_int_List.Total(); PrintFormat("Nodes loaded from the file: %d",Loaded_List_size); class=class="str">"cmt">//--- free the memory class="kw">delete p_int_List;
◍ 用列表管图形对象与虚拟仓位
在 MT5 里做程序化图形标注,最麻烦的是对象数量不固定、生命周期长短不一。把每条 EMA21 向下起点到向上起点之间的区间当成「框架」,用 CList 挂节点比写死数组灵活得多。 具体做法是用 CVertLineNode 描述两条垂直线夹住的若干烛形,节点里存线属性、持续时间和是否成形标志。脚本 Stat_collector.mq5 在 EURUSD H1 上扫了过去 2000 根柱,把框架按持续时间拆进 4 个列表:≤5 根、≤10 根、≤15 根、不限。统计显示最后一个框架落在 2013-12-13 17:00,持续 6 小时,归进第二个列表。 另一处实用场景是多策略同品种回测。实盘一个品种只能跑一个策略,其余只能虚拟化。把虚拟订单、仓位、成交分别建类,再用一个列表当内存数据库装这些节点,免了缓存文件。CVirtualPositionNode 之类直接进列表,开仓即 Add,RAM 里随取随用。外汇与贵金属杠杆高,虚拟测试结论仅代表历史样本,实盘可能严重偏离。 下面这段是框架节点的类骨架,private 里放两条线属性和成形标记,public 只暴露 set/get 和绘图接口,继承 CObject 才能直接塞进 CList。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CVertLineNode class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CVertLineNode : class="kw">public CObject { class="kw">private: SVertLineProperties m_vert_lines[class="num">2]; class=class="str">"cmt">// array of structures of vertical line properties class="type">uint m_duration; class=class="str">"cmt">// frame duration class="type">bool m_IsFrameFormed; class=class="str">"cmt">// flag of frame formation class="kw">public: class="type">void CVertLineNode(class="type">void); class="type">void ~CVertLineNode(class="type">void){}; class=class="str">"cmt">//--- set-methods class="type">void SetLine(const SVertLineProperties &_vert_line,class="type">bool IsFirst=true); class="type">void SetDuration(const class="type">uint _duration){this.m_duration=_duration;}; class="type">void SetFrameFlag(const class="type">bool _frame_flag){this.m_IsFrameFormed=_frame_flag;}; class=class="str">"cmt">//--- get-methods class="type">void GetLine(SVertLineProperties &_vert_line_out,class="type">bool IsFirst=true) const; class="type">uint GetDuration(class="type">void) const; class="type">bool GetFrameFlag(class="type">void) const; class=class="str">"cmt">//--- draw the line
「统计收集器的节点分布实况」
在 EURUSD 的 H1 周期上跑 Stat_collector,日志会按 Duration limit 把节点分桶列出。同一时刻 15:27:32 的输出里,限制 5 根 K 线时挂了 65 个节点,限制 10 根时降到 15 个,限制 15 根时只剩 6 个——时间窗口越宽,存活节点反而越稀疏,这是虚拟持仓树剪枝后的真实截面。 还有一组没带 Duration limit 标记的列表,直接报 Nodes number: 20,属于不受时长约束的常驻节点。外汇与贵金属杠杆高,这类统计只是结构画像,不预示任何方向,实盘前请在策略测试器里复算一遍。 类声明里 DrawLine 这个 const 成员函数,默认 IsFirst=true,说明画线动作在节点构造期就可能触发,且承诺不修改对象状态。下面这段是截取的头文件片段,可对照看节点类的继承关系。
class="type">bool DrawLine(class="type">bool IsFirst=true) const; }; NS class="num">0 class="num">15:class="num">27:class="num">32 Stat_collector(EURUSD,H1) =======List #class="num">1======= RF class="num">0 class="num">15:class="num">27:class="num">32 Stat_collector(EURUSD,H1) Duration limit: class="num">5 ML class="num">0 class="num">15:class="num">27:class="num">32 Stat_collector(EURUSD,H1) Nodes number: class="num">65 HK class="num">0 class="num">15:class="num">27:class="num">32 Stat_collector(EURUSD,H1) OO class="num">0 class="num">15:class="num">27:class="num">32 Stat_collector(EURUSD,H1) =======List #class="num">2======= RI class="num">0 class="num">15:class="num">27:class="num">32 Stat_collector(EURUSD,H1) Duration limit: class="num">10 NP class="num">0 class="num">15:class="num">27:class="num">32 Stat_collector(EURUSD,H1) Nodes number: class="num">15 RG class="num">0 class="num">15:class="num">27:class="num">32 Stat_collector(EURUSD,H1) FH class="num">0 class="num">15:class="num">27:class="num">32 Stat_collector(EURUSD,H1) =======List #class="num">3======= GN class="num">0 class="num">15:class="num">27:class="num">32 Stat_collector(EURUSD,H1) Duration limit: class="num">15 FG class="num">0 class="num">15:class="num">27:class="num">32 Stat_collector(EURUSD,H1) Nodes number: class="num">6 FR class="num">0 class="num">15:class="num">27:class="num">32 Stat_collector(EURUSD,H1) CD class="num">0 class="num">15:class="num">27:class="num">32 Stat_collector(EURUSD,H1) =======List #class="num">4======= PS class="num">0 class="num">15:class="num">27:class="num">32 Stat_collector(EURUSD,H1) Nodes number: class="num">20 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class CVirtualPositionNode | class=class="str">"cmt">//+------------------------------------------------------------------+ class CVirtualPositionNode : class="kw">public CObject { class="kw">protected: CVirtualPositionNode *m_virt_position; class=class="str">"cmt">// pointer to the class="kw">virtual function class="kw">public:
虚拟持仓节点的构造与析构声明
在 MQL5 的持仓管理封装里,CVirtualPositionNode 类只暴露了两个空实现的特殊成员函数:默认构造函数与析构函数。 从代码声明看,二者均不带参数且返回 void,仅以注释标出 default constructor 与 destructor,说明该节点基类本身不持有需要显式分配的资源。 开 MT5 新建一个继承该类的 EA 骨架,可以直接复用这两行声明,再往里补持仓字段与平仓逻辑,避免每次手写节点内存管理的样板代码。
class="type">void CVirtualPositionNode(class="type">void); class=class="str">"cmt">// class="kw">default constructor class="type">void ~CVirtualPositionNode(class="type">void); class=class="str">"cmt">// destructor };
◍ 别急着下结论
链表容器在 MT5 里确实能把节点操作封装得很干净,但代价是调试复杂度上去、编译依赖变多。本文给出的 18 个文件都落在 %MQL5\Projects\UserLists 下,从 CiSingleNode.mqh 到 Stat_collector.mq5,覆盖了单向、双向、松散、循环四类链表加统计收集样例。 实际跑起来有个坑:社区里有人编译 test_MQL5_List.mq5 时报错,说常量对象的 m_head、m_tail、m_size 不能被修改,根源是把非 const 方法误声明成 const,作者后来发了修复版替换旧文件即可。 如果你用默认目录而非 Projects 下的子目录,包含写法要从引号改成尖括号,否则 include 路径会断。开 MT5 把这套 zip 解到 Projects\UserLists,先编译 test_nodes.mq5 验证环境,再逐个跑链表脚本,比直接读类定义来得快。