从基础到中级:联合(二)·进阶篇
🧩

从基础到中级:联合(二)·进阶篇

(2/3)· 继入门篇铺完联合概念后,本篇用 6 个小节拆穿“联合只能装原始类型”的错觉,并顺手扩写数组的非常规用法

实战向进阶 第 2/3 篇
不少写 EA 的人卡在“联合里塞数组会编译报错”的直觉上,其实只是没搞清声明位置和静态数组的边界。把联合当纯原始类型容器,等于主动放弃了一批紧凑内存布局的写法。

「用双指针翻转位容器里的字节序」

在自定义联合体 un_01 里,u8_bits 是一个按字节存储的容器。Swap 函数要做的事很直接:把容器首尾字节对调,实现字节序反转,常见于跨端读取行情裸数据前的预处理。 代码用了一对游标 i 和 j,i 从 0 自增、j 从 Size()-1 自减,循环条件 i<j 保证只扫前半段就完成全容器镜像。每次迭代用 tmp 做三次赋值完成一对字节交换,时间复杂度是 O(n/2),n 为字节数。 实际在 MT5 里验证时,可先给 u8_bits 塞入 4 字节(如 0x01,0x02,0x03,0x04),调用 Swap 后打印,应得到 0x04,0x03,0x02,0x01。外汇与贵金属市场数据封包字节序不统一,这类翻转在解析 Tick 结构时可能用得上,但涉及实盘解析须警惕字节对齐差异带来的高风险。

MQL5 / C++
class="num">48. un_01 Swap(class="kw">const un_01 &arg)
class="num">49. {
class="num">50.    un_01 info = arg;
class="num">51.
class="num">52.    for (class="type">uchar i = class="num">0, j = (class="type">uchar)(info.u8_bits.Size() - class="num">1), tmp; i < j; i++, j--)
class="num">53.    {
class="num">54.        tmp = info.u8_bits[i];
class="num">55.        info.u8_bits[i] = info.u8_bits[j];
class="num">56.        info.u8_bits[j] = tmp;
class="num">57.    }
class="num">58.
class="num">59.    class="kw">return info;
class="num">60. }
class="num">61. class=class="str">"cmt">//+------------------------------------------------------------------+

用联合体重构内存拷贝

把一段声明了 uint、ushort、uchar 并手动拼装数组的笨重代码,改成用 union 直接按字节搬内存,是 MQL5 里很实在的优化。核心思路是:只要 union 里最大离散类型是 uint(4 字节),就声明一个元素数等于该宽度的静态 uchar 数组,覆盖整块内存区,省掉大量逐位赋值。 原文里老代码(代码 04)跑出来的 Info 数组内容见图 08,而初次用数组拷贝(代码 05)得到的结果见图 09——数值在内存里是镜像/轮转的,和图 08 对不上。若你的场景只关心输入输出的对外值、不关心内存原样(图 10 红蓝区一致、粉区仍乱),可以容忍这种镜像。 但要做成和图 08 完全一致的字节布局,就得在写入前显式反转。代码 06 初版只有首个元素对,因为 start 值设成了 0,拷贝只动了反转后数组的第 1 个(原最后个)。把第 45、50 行的 start 按片段 05 修正后,输出才和图 08 严丝合缝(图 12)。 别把联合当普通结构体用 联合体内所有字段共享同一块内存,改一个变量其余全变。我们不在乎某个字段的具体值,只在乎那段 uchar 数组的内容,所以拿数组整体复制代替零散赋值,代码立刻瘦一圈。 最终版(代码 07)把重复的起止赋值收进循环,逻辑更干净。你在 MT5 里建个 EA 把这段贴进去,把 ui = 0xCADA5169、us = 0x43BC、uc = '01011101' 当作探针,跑完对比终端输出的十六进制与二进制串,就能验证联合体内存视图是否如预期镜像。外汇与贵金属编程测试请先在策略测试器用模拟环境,实盘高风险。

MQL5 / C++
class="num">01. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">02. class="macro">#class="kw">property copyright "Daniel Jose"
class="num">03. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">04. class="macro">#include "Tutorial\File class="num">01.mqh"
class="num">05. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">06. class="type">void OnStart(class="type">void)
class="num">07. {
class="num">08.     class="kw">const class="type">uint  ui = 0xCADA5169;
class="num">09.     class="type">class="kw">ushort      us = 0x43BC;
class="num">10.     class="type">uchar       uc = B&class="macro">#x27;class="num">01011101&class="macro">#x27;;
class="num">11.
class="num">12.     class="type">uchar       Infos[];
class="num">13.     class="type">uchar       counter = class="num">0;
class="num">14.     class="type">uint        start;
class="num">15.     class="type">uint        number;
class="num">16.
class="num">17.     PrintFormat("Translation personal.\n" +
class="num">18.                 "FUNCTION: [%s]\n" +
class="num">19.                 "ui => 0x%s\n" +
class="num">20.                 "us => 0x%s\n" +
class="num">21.                 "uc => B&class="macro">#x27;%s&class="macro">#x27;\n",
class="num">22.                 __FUNCTION__,
class="num">23.                 ValueToString(ui, FORMAT_HEX),
class="num">24.                 ValueToString(us, FORMAT_HEX),
class="num">25.                 ValueToString(uc, FORMAT_BINARY)
class="num">26.                 );

◍ 把联合体内存塞进字节数组再还原

这段 MQL5 代码演示了如何把 uint、ushort、uchar 三种宽度不同的变量,按「长度前缀 + 大端字节」的格式连续写进一个 uchar 数组 Infos,再交给 Procedure 反解。 写 uint 时先算 number = sizeof(ui) + 1,即 5 字节(1 字节存长度 4,后 4 字节存数据);通过右移 24/16/8 位并 &0xFF 取出每个字节,依次填入数组。ushort 占 3 字节(长度 2 + 数据 2),uchar 占 2 字节(长度 1 + 数据 1)。 打印环节用 Infos.Size() 输出总字节数,ArrayPrint 直接把数组铺开,方便在 MT5 专家日志里核对每一格的数值。外汇与贵金属 EA 做跨模块二进制传参时,这种打包方式比字符串省内存,但字节序写错就会解出来乱码,属高风险底层操作。 Procedure 里的还原循环很关键:外层 for 读长度字节 j,内层按 j 把后续字节左移 8 位累加回 value,最后用 ValueToString 以十六进制和二进制双格式打印。跑一遍你能直观看到 0x 开头和 B' 开头的对应关系。

MQL5 / C++
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);

ArrayFree(Infos);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Procedure(class="kw">const class="type">uchar &arg[])
{
Print("Translation personal.\n" +
      "FUNCTION: ", __FUNCTION__);

class="type">class="kw">ulong value;

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

「用 union 把同一块内存掰成三种宽度看」

在 MQL5 里,union 能让你用不同位宽的类型共享同一段内存,而不必做类型转换拷贝。上面定义的 un_Model 里,u32、u16、u8 以及一个 4 字节数组 array[sizeof(uint)] 全部叠在同一地址上——sizeof(uint) 在 MT5 里固定是 4,所以 array 正好能拆出 32 位的每一个字节。 OnStart 里先塞了三个常量:ui = 0xCADA5169、us = 0x43BC、uc = B'01011101'(即十进制 93)。随后用 enum 标出 U32_Bits / U16_Bits / U8_Bits 三种观察视角,循环里每次 new 一个 un_Model,把 Infos 数组拉长 1 格准备记录。 这种写法对做行情裸数据解析很有用:比如从自定义 bin 文件读 tick,可以直接用 union 把 4 字节浮点位模式按 uchar 数组拆开排查字节序,外汇与贵金属杠杆品种波动剧烈,这类底层操作务必先在策略测试器用小样本验证,避免误读导致下单异常。

MQL5 / C++
union un_Model
{
  class="type">uint    u32;
  class="type">class="kw">ushort  u16;
  class="type">uchar   u8,
           array[class="kw">sizeof(class="type">uint)];
};

class="type">void OnStart(class="type">void)
{
  class="kw">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;;

  enum eValues {
    U32_Bits,
    U16_Bits,
    U8_Bits
  };

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

  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)
              );

  for (eValues c = U32_Bits; c <= U8_Bits; c++)
  {
    un_Model data;

    ArrayResize(Infos, Infos.Size() + class="num">1);

联合体字节序翻转与块打印

这段逻辑把不同位宽的整型塞进同一个联合体 un_Model,再按字节整体翻转为小端序写入 Infos 数组。switch 分支里 U32_Bits、U16_Bits、U8_Bits 分别对应 ui、us、uc 三种成员,每写入一个值就先用 Infos[counter++] 记录该类型的 sizeof,再把数据赋给联合体的对应字段。 第 061 行是关键的拼装:ArrayCopy 从 Swap(data).array 拷贝时,偏移量用 sizeof(data) 减去刚写入的字节数 Infos[counter-1],保证只搬有效字节、不把联合体里残留的高位零带进去。Swap 函数(074–086 行)就是个经典双指针反转,i 从 0、j 从 Size()-1 向中间交换,把大端布局扭成 MT5 原生小端。 收尾部分(064–067 行)用 PrintFormat 打出 Infos.Size() 的总字节数,再 ArrayPrint 把整块内容铺出来。如果你在 EURUSD 的 MT5 脚本里跑这套,Infos 块长度会直接反映你塞进去的 ui/us/uc 实际占用——比如一个 u32 加一个 u8 就是 5 字节,终端日志里能看到明确数字,可立即核对字节序是否符合预期。 Procedure(088 行起)接收 const uchar& 数组,用 arg[c++] 读出每个字段的长度 j,再循环 j 次把后续字节累加成 ulong value。这种变长解析写法在外汇行情打包解包里很实用,但联合体别名在不同编译器下行为可能有差异,上真仓前务必在策略测试器里先验证一遍。

MQL5 / C++
class="kw">switch (c)
{
   case U32_Bits:
      Infos[counter++] = class="kw">sizeof(ui);
      data.u32 = ui;
      class="kw">break;
   case U16_Bits:
      Infos[counter++] = class="kw">sizeof(us);
      data.u16 = us;
      class="kw">break;
   case U8_Bits:
      Infos[counter++] = class="kw">sizeof(uc);
      data.u8 = uc;
      class="kw">break;
}

counter += (class="type">uchar)ArrayCopy(Infos, Swap(data).array, counter, class="kw">sizeof(data) - Infos[counter - class="num">1]);
}

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

Procedure(Infos);

ArrayFree(Infos);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
un_Model Swap(class="kw">const un_Model &arg)
{
   un_Model info = arg;

   for (class="type">uchar i = class="num">0, j = (class="type">uchar)(info.array.Size() - class="num">1), tmp; i < j; i++, j--)
   {
      tmp = info.array[i];
      info.array[i] = info.array[j];
      info.array[j] = tmp;
   }

   class="kw">return info;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Procedure(class="kw">const class="type">uchar &arg[])
{
   Print("Translation personal.\n" +
         "FUNCTION: ", __FUNCTION__);

   class="type">class="kw">ulong value;

   for (class="type">uchar c = class="num">0; c < arg.Size(); )
   {
      value = class="num">0;
      for (class="type">uchar j = arg[c++], i = class="num">0; (c < arg.Size()) && (i < j); i++, c++)

◍ 用联合体把字节流拼回整型

MQL5 里想按字节重组一个 uint,最直接的办法是定义一个 union:把 u32、u16、u8 和一个 uchar 数组叠在同一段内存上。上面例子里 union un_Model 用 array[sizeof(uint)] 也就是 4 字节,和 u32 共享存储,改数组就等于改整型。 Procedure 函数接收一个 uchar 数组,用 ArrayCopy 从偏移 c+1 处拷 arg[c] 个字节进 data.array,再 +1 跳过长度字节,循环把所有片段拼成 data.u32 并打印十六进制与二进制。注意 arg[c] 存的是『接下来几个字节有效』,所以 c 的步进是拷贝长度加 1。 OnStart 里给了具体观测值:ui=0xCADA5169、us=0x43BC、uc=B'01011101'。把这些打进 Infos 再喂给 Procedure,终端会逐段吐出类似『0xCADA5169 B'11001010110110100101000101101001'』,你可以直接开 MT5 把代码跑一遍核对字节序。外汇与贵金属杠杆高,这类底层结构只用于行情数据解析辅助,不预示任何价格方向。

MQL5 / C++
union un_Model
{
   class="type">uint    u32;
   class="type">class="kw">ushort  u16;
   class="type">uchar   u8,
           array[class="kw">sizeof(class="type">uint)];
};

class="type">void OnStart(class="type">void)
{
   class="kw">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;
   PrintFormat("Translation personal.\n" +
               "FUNCTION: [%s]\n" +
               "ui => 0x%s\n" +
               "us => 0x%s\n" +
               class=class="str">"cmt">// ...
}

class="type">void Procedure(class="kw">const class="type">uchar &arg[])
{
   un_Model data;
   Print("Translation personal.\n" +
         "FUNCTION: ", __FUNCTION__);
   for (class="type">uchar c = class="num">0; c < arg.Size(); )
   {
      ZeroMemory(data.array);
      c += (class="type">uchar)(ArrayCopy(data.array, arg, class="num">0, c + class="num">1, arg[c]) + class="num">1);
      Print("0x", ValueToString(data.u32, FORMAT_HEX), " B&class="macro">#x27;", ValueToString(data.u32, FORMAT_BINARY), "&class="macro">#x27;");
   }
}

class=class="str">"cmt">// 片段:value = (value << class="num">8) | arg[c];
class=class="str">"cmt">// Print("0x", ValueToString(value, FORMAT_HEX), " B&class="macro">#x27;", ValueToString(value, FORMAT_BINARY), "&class="macro">#x27;");
交给小布盯盘看盘口之外的代码体检
这些联合与数组混用的内存诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到结构占用提示,把重复劳动交给小布,你专注决策。

常见问题

关键看数组声明在联合内部还是作为联合类型的变量成员;联合大小由最大成员决定,内部静态数组会直接撑大整体字节数,需回顾上篇基础篇的字节对齐规则。
可以,小布盯盘的 AIGC 模块对常见联合与数组嵌套结构有内置体积推算,不用自己手算 u32_bits 这类成员的上限。
两者都涉及连续内存与类型重解释,联合解决“同一块内存多种看法”,数组解决“同类型连续排布”,进阶用法常互相嵌套。
数组总字节等于单元素大小乘长度,再参与联合最大成员比较;若数组总字节超过其他成员,联合大小就取该数组总值,可能涉及对齐填充。