从基础到中级:结构(二)·进阶篇
📘

从基础到中级:结构(二)·进阶篇

第 2/2 篇

◍ 用结构体收口批量记录的写法

在 MT5 脚本里处理多组同类数据,最忌散着开六个平行数组。原文第一段用了 Name[10]、Surname[10]、Address[10]、Position[10]、Remuneration[10]、Birth[10]、Hiring[10] 七个独立数组,记录数由 #define def_Number_of_Records 10 统一约束,但字段之间没有任何语义绑定,后期写入和遍历都容易错位。 更稳的做法是把这些字段收进一个 struct,再用该结构体开数组。下面第二段代码用 st_Register 把姓名、住址、薪资、出生与入职时间打包,然后声明 Record[def_Number_of_Records],十条人员记录就成了一个连续对象数组,而不是七条互不相干的序列。 开 MT5 按 F4 新建脚本,把第二段代码整段贴入 OnStart 内,编译后跑一遍,能在调试窗口看到 Record[0] 到 Record[9] 各自带着完整字段。外汇与贵金属相关的数据管理同样高风险,参数写错可能让后续统计全部偏移,建议先用固定 10 条样本验证结构再扩量。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart(class="type">void)
{
class="macro">#define def_Number_of_Records  class="num">10

   class="kw">struct st_Register
   {
      class="type">class="kw">string      Name,
                  Surname,
                  Address,
                  Position;
      class="type">class="kw">double      Remuneration;
      class="type">class="kw">datetime    Birth,
                  Hiring;
   };

   st_Register Record[def_Number_of_Records];

用结构体把多条记录塞进数组

在 MT5 里处理批量数据,先定义一个 struct 把字段捆在一起比散着写变量省事得多。下面这段把姓名、地址、岗位、薪资和日期收进 st_Register,再声明一个长度为 10 的数组,相当于预开了 10 个工位。 录入靠一个 NewRecord 函数,成功返回 true 才让 Counter 加 1,因此 Counter 最终就是实际写入条数。示例里连写 4 条 Brasil 记录,Print 输出会是 Number of records: 4。 ViewRecord(Record[3]) 直接按索引取第 4 条(Yara Alves,岗位 Accounting)。这种写法在外汇/贵金属策略里做参数批次回测时很有用,但注意数组越界会直接报错,MT5 高风险环境里建议先判 Counter 再取索引。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct st_Register
{
   class="type">class="kw">string      Name,
               Surname,
               Address,
               Position;
   class="type">class="kw">double      Remuneration;
   class="type">class="kw">datetime    Birth,
               Hiring;
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart(class="type">void)
{  
   st_Register Record[class="num">10];
   class="type">uchar       Counter = class="num">0,
               Index;

   Index = Counter;
   Counter += (NewRecord(Record[Counter], "Daniel", "Jose", "Brasil", "class="num">1971/class="num">03/class="num">30") ? class="num">1 : class="num">0);
   Record[Index] = UpdatePosition(Record[Index], "Chief Programmer");
   Index = Counter;
   Counter += (NewRecord(Record[Counter], "Edimarcos", "Alcantra", "Brasil", "class="num">1974/class="num">12/class="num">07") ? class="num">1 : class="num">0);
   Record[Index] = UpdatePosition(Record[Index], "Programmer");
   Index = Counter;
   Counter += (NewRecord(Record[Counter], "Carlos", "Almeida", "Brasil", "class="num">1985/class="num">11/class="num">15") ? class="num">1 : class="num">0);
   Record[Index] = UpdatePosition(Record[Index], "Junior Programmer");
   Index = Counter;
   Counter += (NewRecord(Record[Counter], "Yara", "Alves", "Brasil", "class="num">1978/class="num">07/class="num">25") ? class="num">1 : class="num">0);
   Record[Index] = UpdatePosition(Record[Index], "Accounting");

   Print("Number of records: ", Counter);
   ViewRecord(Record[class="num">3]);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool NewRecord(st_Register &arg, class="kw">const class="type">class="kw">string Name, class="kw">const class="type">class="kw">string Surname, class="kw">const class="type">class="kw">string Address, class="kw">const class="type">class="kw">string Birth)
{
   arg.Name = Name;
   arg.Surname = Surname;
   arg.Address = Address;

「用结构体数组管多条协作记录」

这段 MT5 代码示范了如何用 struct 把多个交易协作者的静态资料收进一个固定长度数组,方便后续在 EA 或脚本里统一读取与改写。gl_Record[10] 意味着最多缓存 10 条记录,超出会越界报错,实盘前应按需调大或改动态数组。 NewCollaborator 内部把字符串生日通过 StringToTime 转成 datetime 赋给 arg.Birth,Hiring 则取 TimeLocal() 的本地计算机时间,返回 true 表示写入成功。注意 TimeLocal 不走经纪商服务器时区,回测和实盘可能差几个小时,跨时区团队要统一用 TimeTradeServer 才稳。 UpdatePosition 接收一个 st_Register 副本,改完 Position 字段后返回新结构体,原记录不被破坏——这种值传递在 MT5 里能避免误改全局状态。ViewRecord 用 PrintFormat 把姓氏、名、岗位和生日日期打到日志,方便你按 F4 编译后直接在 Experts 标签里核对。 OnStart 里先 ZeroMemory 清掉 gl_Record 残留,再连续塞了 3 条 Brasil 程序员资料,GetNumberRecord 取当前条数赋给 Index。你可以直接把这段代码贴进 MT5 脚本跑一遍,看日志输出是否和下面一致:3 条记录、Chief/Programmer/Junior 三个岗位。外汇与贵金属脚本涉及本地时间处理,时区错配可能引发信号误触发,属高风险操作,验证时先用模拟账户。

MQL5 / C++
class="num">43.    arg.Birth = StringToTime(Birth);
class="num">44.    arg.Hiring = TimeLocal();
class="num">45.
class="num">46.    class="kw">return true;
class="num">47. }
class="num">48. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">49. st_Register UpdatePosition(class="kw">const st_Register &arg, class="kw">const class="type">class="kw">string NewPosition)
class="num">50. {
class="num">51.    st_Register info = arg;
class="num">52.
class="num">53.    info.Position = NewPosition;
class="num">54.
class="num">55.    class="kw">return info;
class="num">56. }
class="num">57. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">58. class="type">void ViewRecord(class="kw">const st_Register &arg)
class="num">59. {
class="num">60.    PrintFormat("Collaborator: %s, %s\nPosition: %s\nBirth in: %s",
class="num">61.                  arg.Surname,
class="num">62.                  arg.Name,
class="num">63.                  arg.Position,
class="num">64.                  TimeToString(arg.Birth, TIME_DATE));
class="num">65. }
class="num">66. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">001. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">002. class="macro">#class="kw">property copyright "Daniel Jose"
class="num">003. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">004. class="kw">struct st_Register
class="num">005. {
class="num">006.    class="type">uchar      ID;
class="num">007.    class="type">class="kw">string     Name,
class="num">008.               Surname,
class="num">009.               Address,
class="num">010.               Position;
class="num">011.    class="type">class="kw">double     Remuneration;
class="num">012.    class="type">class="kw">datetime   Birth,
class="num">013.               Hiring;
class="num">014. }gl_Record[class="num">10];
class="num">015. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">016. class="type">void OnStart(class="type">void)
class="num">017. {
class="num">018.    class="type">uchar Index;
class="num">019.
class="num">020.    ZeroMemory(gl_Record);
class="num">021.
class="num">022.    NewCollaborator("Daniel", "Jose", "Brasil", "class="num">1971/class="num">03/class="num">30", "Chief Programmer");
class="num">023.    NewCollaborator("Edimarcos", "Alcantra", "Brasil", "class="num">1974/class="num">12/class="num">07", "Programmer");
class="num">024.    NewCollaborator("Carlos", "Almeida", "Brasil", "class="num">1985/class="num">11/class="num">15", "Junior Programmer");
class="num">025.
class="num">026.    Index = GetNumberRecord();
class="num">027.

◍ 用结构体和函数封装一条记录的写入

这段 MT5 脚本演示了如何把「新增人员」动作拆成可复用的底层函数,而不是在主线里堆重复代码。主线先调 NewRecord 填好姓名、国籍、生日,再把返回的结构体交给 UpdatePosition 补上岗位,最后写回全局数组 gl_Record 并顺手赋 ID。 NewCollaborator 是更上层的封装:它内部建一个临时 st_Register,调 NewRecord 初始化,再靠 while 循环从 Index=0 开始找第一个 ID 为 0 的空位(第 51–55 行),找到后把 info.ID 设成 Index+1 并存入。注意第 53 行有个边界保护——若 Index 超出 gl_Record.Size() 直接返回 false,避免越界崩脚本。 NewRecord 里第 68 行用 StringToTime 把 "1978/07/25" 这类字符串转成时间戳,第 69 行用 TimeLocal 记入职时间,这两个调用在回测和实盘里取值不同,实盘跑会拿到你电脑本地时钟。 UpdatePosition 非常轻:拷贝一份传入结构体,改 Position 字段,返回副本(第 76–80 行)。这种「传值返回」写法在 MQL5 里没有性能焦虑,结构体不大时比传引用改原值更不容易留副作用。 验证方式:把上面代码贴进 MT5 的 EA 或脚本,Print 出来的 Number of records 会随你调用 NewCollaborator 的次数递增;外汇与贵金属交易相关的自动化工具均属高风险,任何封装逻辑都先在策略测试器跑通再上真仓。

MQL5 / C++
if (NewRecord(gl_Record[Index], "Yara", "Alves", "Brasil", "class="num">1978/class="num">07/class="num">25"))
{
   gl_Record[Index].ID = Index + class="num">1;
   gl_Record[Index] = UpdatePosition(gl_Record[Index], "Accounting");
}

Print("Number of records: ", GetNumberRecord());
Print("--------------------");

for(class="type">uchar c = class="num">1; ViewRecord(c); c++)
   Print("********************");
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool NewCollaborator(class="kw">const class="type">class="kw">string Name, class="kw">const class="type">class="kw">string Surname, class="kw">const class="type">class="kw">string Address, class="kw">const class="type">class="kw">string Birth, class="kw">const class="type">class="kw">string Position)
{
   st_Register info;
   class="type">uchar      Index = class="num">0;

   if (!NewRecord(info, Name, Surname, Address, Birth))
      class="kw">return class="kw">false;

   info = UpdatePosition(info, Position);

   class="kw">while (gl_Record[Index].ID)
   {
      if (Index >= gl_Record.Size()) class="kw">return class="kw">false;
      Index++;
   }

   info.ID = Index + class="num">1;
   gl_Record[Index] = info;

   class="kw">return true;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool NewRecord(st_Register &arg, class="kw">const class="type">class="kw">string Name, class="kw">const class="type">class="kw">string Surname, class="kw">const class="type">class="kw">string Address, class="kw">const class="type">class="kw">string Birth)
{
   arg.Name = Name;
   arg.Surname = Surname;
   arg.Address = Address;
   arg.Birth = StringToTime(Birth);
   arg.Hiring = TimeLocal();

   class="kw">return true;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
st_Register UpdatePosition(class="kw">const st_Register &arg, class="kw">const class="type">class="kw">string NewPosition)
{
   st_Register info = arg;

   info.Position = NewPosition;

   class="kw">return info;
}
class=class="str">"cmt">//+------------------------------------------------------------------+

用计数器与查表函数榨干记录数组

这段逻辑站在全局容器 gl_Record 上做两件事:数有效条目、按 ID 抽单条。外汇与贵金属 EA 里这类簿记函数跑在 OnTick 之外才安全,实时行情推送中误用可能拖慢执行并放大滑点风险。 第一段是个极简计数器。声明 uchar counter 从 0 起,for 循环里 c 从 0 走到 gl_Record.Size(),每遇到 gl_Record[c].ID 非零就把 counter 加 1,最后返回 counter。用 uchar 意味着记录数超过 255 会溢出,实盘前先确认你的注册表规模。 ViewRecord 则负责按 ID 查人。ZeroMemory 先把 st_Register info 清空,循环里只要没匹配到 info.ID 就继续扫,命中即赋值。PrintFormat 把姓氏、名字、岗位和出生日期(TIME_DATE 格式)打到日志;查不到就报“Record ID [x] not found.”,返回值是 info.ID 的布尔形态。 别把正态当圣经:这段代码假设 ID 唯一且非零代表有效。若你的写入逻辑会出现脏 ID,计数器会虚高,查表也可能首命中即停。开 MT5 把这段贴进脚本,用 Print 验证 Size 与 counter 的差值,就能立刻看出容器里有多少空槽。

MQL5 / C++
{
   class="type">uchar counter = class="num">0;

   for (class="type">uchar c = class="num">0; c < gl_Record.Size(); counter += (gl_Record[c].ID ? class="num">1 : class="num">0), c++);

   class="kw">return counter;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool ViewRecord(class="kw">const class="type">uchar ID)
{
   st_Register info;

   ZeroMemory(info);

   for (class="type">uchar c = class="num">0; (c < gl_Record.Size()) && (!info.ID); c++)
      info = (gl_Record[c].ID == ID ? gl_Record[c] : info);

   if (info.ID)
      PrintFormat("Collaborator: %s, %s\nPosition: %s\nBirth in: %s",
                  info.Surname,
                  info.Name,
                  info.Position,
                  TimeToString(info.Birth, TIME_DATE));
   else
      Print("Record ID [", ID ,"] not found.");

   class="kw">return (class="type">bool) info.ID;
}
class=class="str">"cmt">//+------------------------------------------------------------------+

「记住这一条就够了」

结构在 MQL5 里本质上就是一种特殊类型的变量,本文用不到 3 KB 的附件代码搭出了一个类数据库的小系统,教学目的已经达到。真要上生产环境,得补大量防错测试,但概念跑通比堆功能更重要。 下一篇会把这个结构用法往实用方向推,编码量可能明显降下来。你现在最该做的,是把附件 Anexo.zip 里的代码在 MT5 里跑一遍,改两个字段看看输出怎么变。外汇和贵金属波动剧烈,任何自写结构逻辑都先上模拟盘验证再谈实盘。

常见问题

先定义包含字段的结构体,再声明该结构体数组,每次写入只需给数组元素赋值,避免零散变量混乱。
会。结构体数组让同类记录聚成一堆,用下标遍历即可统一处理,改字段只动结构体定义。
可以。小布能按你给的结构体字段做AIGC诊断,直接标出偏离正常的记录,省去手搓计数器与查表函数。
把写入逻辑包进带结构体参数的函数,调用时传结构体实例,由函数内部完成数组追加与计数。
维护一个自增计数器作长度,查表函数按关键字段遍历已用区间,命中即返回下标,避免全量盲扫。