从基础到中级:联合(二)·进阶篇
(2/3)· 继入门篇铺完联合概念后,本篇用 6 个小节拆穿“联合只能装原始类型”的错觉,并顺手扩写数组的非常规用法
「用双指针翻转位容器里的字节序」
在自定义联合体 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 结构时可能用得上,但涉及实盘解析须警惕字节对齐差异带来的高风险。
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' 当作探针,跑完对比终端输出的十六进制与二进制串,就能验证联合体内存视图是否如预期镜像。外汇与贵金属编程测试请先在策略测试器用模拟环境,实盘高风险。
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' 开头的对应关系。
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 数组拆开排查字节序,外汇与贵金属杠杆品种波动剧烈,这类底层操作务必先在策略测试器用小样本验证,避免误读导致下单异常。
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。这种变长解析写法在外汇行情打包解包里很实用,但联合体别名在不同编译器下行为可能有差异,上真仓前务必在策略测试器里先验证一遍。
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 把代码跑一遍核对字节序。外汇与贵金属杠杆高,这类底层结构只用于行情数据解析辅助,不预示任何价格方向。
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;");