从基础到中级:数组(四)·综合运用
📘

从基础到中级:数组(四)·综合运用

第 3/3 篇

把多类型变量压进一个字节流

做跨模块通信时,经常要把 uint、ushort、uchar 不同宽度的变量塞进同一段 uchar 数组。下面这段脚本演示了如何带长度前缀地打包:先写该类型字节数,再按高位到低位拆字节。 常量初始化里 ui=0xCADA0169 占 4 字节,us=0x43BC 占 2 字节,uc=B'01011101' 是二进制字面量 0x5D。PrintFormat 用 ValueToString(…, FORMAT_HEX/FORMAT_BINARY) 把三种格式打印出来,方便对账。 打包逻辑是重复的:每次算 number=sizeof(var)+1,从 Infos.Size() 拿到起始位,ArrayResize 扩数组,第一字节存长度,后面依次右移取高字节。最终 Infos 共 5+3+2=10 字节,ArrayPrint 会原样吐出这 10 个 uchar 值,可直接拿去给另一段 Procedure 解析。 外汇与贵金属 EA 里这么干要留意:MT5 数组越界会直接崩脚本,Resize 前务必确认 counter 没跑偏;这类底层拼包在高杠杆品种上只是工具,不预示任何方向。

MQL5 / C++
class="macro">#include "Tutorial\File class="num">01.mqh"
class="type">void OnStart(class="type">void)
{
   const class="type">uint   ui = 0xCADA0169;
   class="type">class="kw">ushort       us = 0x43BC;
   class="type">uchar        uc = B&class="macro">#x27;class="num">01011101&class="macro">#x27;;

   class="type">uchar        Infos[],
               counter = class="num">0;
   class="type">uint         start,
               number;

   PrintFormat("Translation personal.\n" +
               "FUNCTION: [%s]\n" +
               "ui => 0x%s\n" +
               "us => 0x%s\n" +
               "uc => B&class="macro">#x27;%s&class="macro">#x27;\n",
               __FUNCTION__,
               ValueToString(ui, FORMAT_HEX),
               ValueToString(us, FORMAT_HEX),
               ValueToString(uc, FORMAT_BINARY)
               );

   number = class="kw">sizeof(ui) + class="num">1;
   start = Infos.Size();
   ArrayResize(Infos, start + number);
   Infos[counter++] = class="kw">sizeof(ui);
   Infos[counter++] = (class="type">uchar)(ui >> class="num">24);
   Infos[counter++] = (class="type">uchar)(ui >> class="num">16);
   Infos[counter++] = (class="type">uchar)(ui >> class="num">8);
   Infos[counter++] = (class="type">uchar)(ui & 0xFF);

   number = class="kw">sizeof(us) + class="num">1;
   start = Infos.Size();
   ArrayResize(Infos, start + number);
   Infos[counter++] = class="kw">sizeof(us);
   Infos[counter++] = (class="type">uchar)(us >> class="num">8);
   Infos[counter++] = (class="type">uchar)(us & 0xFF);

   number = class="kw">sizeof(uc) + class="num">1;
   start = Infos.Size();
   ArrayResize(Infos, start + number);
   Infos[counter++] = class="kw">sizeof(uc);
   Infos[counter++] = (uc);

   Print("******************");
   PrintFormat("The Infos block contains %d bytes.", Infos.Size());
   ArrayPrint(Infos);
   Print("******************");

   Procedure(Infos);
}

◍ 字节流里按标记拆出多字节数值

这段 Procedure 函数演示了如何从一个 uchar 数组里按首字节标记还原不同长度的整数。arg[0] 是操作码:4 对应 uint(4 字节)、2 对应 ushort(2 字节)、1 对应 uchar(1 字节),后续字节按大端序拼回原值。 循环写法上,for(uchar c=0; c<arg.Size();) switch(arg[c++]) 先取标记再自增,内层再用 c++,i++ 双计数器搬字节。以 case 4 为例,value = (value << 8) | arg[c] 每轮左移 8 位再或上当前字节,循环条件卡死 i < sizeof(value),uint 下正好吃 4 个字节。 输出时分别用 ValueToString 配 FORMAT_HEX / FORMAT_BINARY,uint 和 ushort 打十六进制、uchar 打二进制串。外汇与贵金属 EA 里做私有协议解析时,这种手搓反序列化比 Struct 映射更可控,但字节序写错会静默读出脏数据,MT5 上跑前建议先喂一段已知数组验证。

MQL5 / C++
class="type">void Procedure(const class="type">uchar &arg[])
{
  Print("Translation personal.\n" +
        "FUNCTION: ", __FUNCTION__);

  for (class="type">uchar c = class="num">0; c < arg.Size(); ) class="kw">switch(arg[c++])
  {
    case class="num">4:
      {
        class="type">uint value = class="num">0;

        for (class="type">uchar i = class="num">0; (c < arg.Size()) && (i < (class="kw">sizeof(value))); c++, i++)
          value = (value << class="num">8) | arg[c];
        Print("0x", ValueToString(value, FORMAT_HEX));
      }
      break;
    case class="num">2:
      {
        class="type">class="kw">ushort value = class="num">0;

        for (class="type">uchar i = class="num">0; (c < arg.Size()) && (i < class="kw">sizeof(value)); c++, i++)
          value = (value << class="num">8) | arg[c];
        Print("0x", ValueToString(value, FORMAT_HEX));
      }
      break;
    case class="num">1:
      {
        class="type">uchar value = class="num">0;

        for (class="type">uchar i = class="num">0; (c < arg.Size()) && (i < class="kw">sizeof(value)); c++, i++)
          value = (value << class="num">8) | arg[c];
        Print("B&class="macro">#x27;", ValueToString(value, FORMAT_BINARY), "&class="macro">#x27;");
      }
      break;
  }
}

「把多类型变量压进一个字节流」

在 MT5 脚本里做私有协议或自定义缓存时,常要把 uint、ushort、uchar 等不同宽度数据塞进同一个 uchar 数组。下面这段脚本演示了如何手工拆字节并顺序存放,最终 Infos 数组长度会是 10 字节(4+3+3)。 开头先定义常量 ui=0xCADA5169、us=0x43BC、uc=B'01011101',后者用二进制字面量直接写位模式,适合做标志位调试。PrintFormat 里用 ValueToString(..., FORMAT_HEX/FORMAT_BINARY) 能把变量按十六进制或二进制打印,开 MT5 跑一遍就能在专家日志看到原始值。 打包逻辑是:每写一种类型,先存 sizeof 占 1 字节,再按高位到低位右移强转 uchar 填入。uint 占 4 字节故写 5 个元素,ushort 写 3 个,uchar 写 2 个。ArrayResize 按当前 Size 追加,counter 自增保证不覆盖。 别把正态当圣经 这种手搓序列化在跨 EA 传参或写文件头时很实用,但字节序全靠自己移位是隐性坑;换平台跑之前先在脚本里 ArrayPrint 核对一遍,外汇和贵金属杠杆高,误读缓存可能引发错单。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "Tutorial\File class="num">01.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart(class="type">void)
{
   const class="type">uint   ui = 0xCADA5169;
   class="type">class="kw">ushort       us = 0x43BC;
   class="type">uchar        uc = B&class="macro">#x27;class="num">01011101&class="macro">#x27;;

   class="type">uchar        Infos[],
               counter = class="num">0;
   class="type">uint         start,
               number;

   PrintFormat("Translation personal.\n" +
               "FUNCTION: [%s]\n" +
               "ui => 0x%s\n" +
               "us => 0x%s\n" +
               "uc => B&class="macro">#x27;%s&class="macro">#x27;\n",
               __FUNCTION__,
               ValueToString(ui, FORMAT_HEX),
               ValueToString(us, FORMAT_HEX),
               ValueToString(uc, FORMAT_BINARY)
               );
   
   number = class="kw">sizeof(ui) + class="num">1;
   start = Infos.Size();
   ArrayResize(Infos, start + number);
   Infos[counter++] = class="kw">sizeof(ui);
   Infos[counter++] = (class="type">uchar)(ui >> class="num">24);
   Infos[counter++] = (class="type">uchar)(ui >> class="num">16);
   Infos[counter++] = (class="type">uchar)(ui >> class="num">8);
   Infos[counter++] = (class="type">uchar)(ui & 0xFF);

   number = class="kw">sizeof(us) + class="num">1;
   start = Infos.Size();
   ArrayResize(Infos, start + number);
   Infos[counter++] = class="kw">sizeof(us);
   Infos[counter++] = (class="type">uchar)(us >> class="num">8);
   Infos[counter++] = (class="type">uchar)(us & 0xFF);

   number = class="kw">sizeof(uc) + class="num">1;
   start = Infos.Size();
   ArrayResize(Infos, start + number);
   Infos[counter++] = class="kw">sizeof(uc);
   Infos[counter++] = (uc);

   Print("******************");
   PrintFormat("The Infos block contains %d bytes.", Infos.Size());
   ArrayPrint(Infos);

把字节流还原成可读数值的打印例程

上面这段 MQL5 代码展示了一个把压缩字节数组按「长度前缀 + 数据」格式解包并打印的例程,核心在 Procedure() 函数。它接收一个 uchar 类型引用数组 arg[],先输出当前函数名,再逐段读取。 外层 for 循环用 c 作游标,每次先取 arg[c++] 作为本段字节数 j,内层再把随后 j 个字节拼成 ulong。注意第 70 行 (value << 8) | arg[c] 是大端拼接:每读一个字节左移 8 位再或上新风数据,所以首字节在最高位。 第 72 行用 ValueToString 分别以十六进制和二进制打印,输出形如 0x1A3F B'0001101000111111'。如果你在 MT5 里跑这段,把 Infos 填成 {2,0xAB,0xCD,1,0x05},控制台会先打 AB 与 CD 拼成的 0xABCD,再打单字节 0x05,能直接验证拼接方向。 外汇与贵金属行情数据若用类似结构做本地缓存压缩,解包逻辑写错一端就可能把报价高低位颠倒,实盘前务必用已知字节序列在策略测试器里跑一遍。

MQL5 / C++
class="num">53.   Print("******************");
class="num">54.
class="num">55.   Procedure(Infos);
class="num">56.
class="num">57.   ArrayFree(Infos);
class="num">58. }
class="num">59. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">60. class="type">void Procedure(const class="type">uchar &arg[])
class="num">61. {
class="num">62.   Print("Translation personal.\n" +
class="num">63.         "FUNCTION: ", __FUNCTION__);
class="num">64.
class="num">65.   class="type">ulong value;
class="num">66.
class="num">67.   for (class="type">uchar c = class="num">0; c < arg.Size(); )
class="num">68.   {
class="num">69.     value = class="num">0;
class="num">70.     for (class="type">uchar j = arg[c++], i = class="num">0; (c < arg.Size()) && (i < j); i++, c++)
class="num">71.       value = (value << class="num">8) | arg[c];
class="num">72.     Print("0x", ValueToString(value, FORMAT_HEX), " B&class="macro">#x27;", ValueToString(value, FORMAT_BINARY), "&class="macro">#x27;");
class="num">73.   }
class="num">74. }

◍ 把冗余代码压成清晰逻辑

这一节本来是承接前文、预告下一篇的过渡段,但技术落点很明确:前面示例里第 28 行到第 48 行那段实现,存在明显的冗余,目标是把它精简掉而不丢功能。 MQL5 其实已经内置了能替代这块手写逻辑的机制,作者留到后续文章展开。当下你能做的,是把附件 Anexo.zip(5.5 KB)里的示例真正跑一遍,定位那二十来行重复代码,思考如何用语言原生特性收拢。 外汇与贵金属波动剧烈、杠杆风险极高,这类代码重构只解决可读与维护成本,不暗示任何收益。先把当前示例练熟,等下一篇揭晓简化写法时,你才有对照的基准。

常见问题

用位运算把各类型按约定偏移压进一个 uchar 数组,例如低4位放标志、接着放压缩后的数值字节,写前先规划好每段的位宽。
先读头部标志字节判断类型,再按预先定义的字节长度和位移用位或和左移拼回原值,拆错偏移会直接乱码。
可以,把代码贴给小布,它能指出位宽冲突、偏移算错和冗余判断,并给出更清晰的压包写法。
拆出每个字段后直接用 Print 按『字段名=值』格式输出,循环处理数组比手写多行打印更不容易漏。
用查表法存类型和字节长度,配合统一拆解函数,把重复分支收进循环,代码行数能砍掉一半以上。