掌握 MQL5:从入门到精通(第三部分)复杂数据类型和包含文件(基础篇)
◍ MQL5 里的复杂类型与 include 机制
MQL5 不止提供 int、double 这类标量,真正写策略时躲不开复杂数据类型:string、数组、struct、class 与动态对象。它们让你能把行情、订单、指标缓冲区组织成可复用的结构,而不是满屏散变量。 包含文件(.mqh)用 #include 引入,相当于把公共函数、常量、类定义抽出来共享。一个 EA 拆成主文件 + 多个 mqh,编译后仍合为单一可执行体,但工程可读性明显提升。 官方示例文档(2025-03-07 发布)在社区获得 1180 次查看、7 条讨论,说明复杂类型与 include 是实盘脚本绕不过的基建。开 MT5 按 F4 建个 .mqh 写个 struct,再在主文件 #include 它,比复制粘贴函数更不容易出 bug。
「从简单类型搭出可维护的复杂结构」
上一篇已经把 double、enum、string 这类简单数据类型,以及变量和常量的概念讲清了。但实盘写 EA 时,你迟早会遇到一种情况:把行情、持仓、参数绑在一起当成一个整体来处理,比散着用一堆独立变量顺手得多。本文要落地的,就是结构(struct)、联合(union)和初阶类(class)这几种“自己拼”的复杂类型。 MT5 编译器有个死规矩:用数据前必须先声明类型,因为它得提前算好要占多少内存。所以不管是简单类型还是后面要写的 struct,本质都是给编译器一张“内存排座表”。 代码能不能长期维护,关键看模块化程度。团队开发不用说,单人写策略也一样——两个月后回来加功能,在几百行平铺逻辑里找 bug,远不如在分好文件、拆好函数的片段里改来得快。把相关逻辑塞进独立数据结构、用 #include 拆文件,改起来心理负担小得多。
把零散变量收进一个结构里
做日内策略时,一笔交易的开仓小时、分钟、秒数往往要一起用。若拆成三个独立 int 变量,代码散、易错;MQL5 的 struct 能把它们打包成 IntradayTime 这种自定义类型,后面所有该类型变量都自带 hours、minutes、seconds 字段,用点号访问即可。 结构里的字段不限于基础类型,也能塞 string 甚至别的结构。下面这段代码先定义了带 stopLoss、takeProfit、magicNumber 的 TradeParameters,再把它作为 TradeSignal 的一个字段,相当于给信号结构体嵌套了参数块。 struct IntradayTime { int hours; int minutes; int seconds; string timeCodeString; }; // 注意花括号后的分号 IntradayTime dealEnterTime; dealEnterTime.hours = 8; dealEnterTime.minutes = 15; dealEnterTime.timeCodeString = "GMT+2"; // Nested structure struct TradeParameters { double stopLoss; double takeProfit; int magicNumber; }; // Main structure struct TradeSignal { string symbol; // Symbol name ENUM_ORDER_TYPE orderType; // Order type (BUY or SELL) double volume; // Order volume TradeParameters params; // Nested structure as parameter type }; // Using the structure void OnStart() { // Variable description for the structure TradeSignal signal; // Initializing structure fields signal.symbol = Symbol(); signal.orderType = ORDER_TYPE_BUY; signal.volume = 0.1; signal.params.stopLoss = 20; signal.params.takeProfit = 40; signal.params.magicNumber = 12345; // Using data in an expression Print("Symbol: ", signal.symbol); Print("Order type: ", signal.orderType); Print("Volume: ", signal.volume); Print("Stop Loss: ", signal.params.stopLoss); Print("Take Profit: ", signal.params.takeProfit); Print("Magic Number: ", signal.params.magicNumber); } TradeSignal signal = { "EURUSD", ORDER_TYPE_BUY,
- 1,
{20.0, 40.0, 12345} }; 用常量初始化时可省去逐字段赋值,花括号内按声明顺序写值就行;只写前几个,其余自动清零。MetaEditor 里输入变量名加小数点,会弹出字段列表,比翻文档快。 MQL5 自带 MqlDateTime、MqlTick 等预定义结构,字段用法同理。外汇与贵金属杠杆高、滑点突兀,结构体虽好,也只是把风险数据归置清楚,不替你过滤行情噪声。
class="kw">struct IntradayTime { class="type">int hours; class="type">int minutes; class="type">int seconds; class="type">class="kw">string timeCodeString; }; class=class="str">"cmt">// note the semicolon after the curly brace IntradayTime dealEnterTime; dealEnterTime.hours = class="num">8; dealEnterTime.minutes = class="num">15; dealEnterTime.timeCodeString = "GMT+class="num">2"; class=class="str">"cmt">// Nested structure class="kw">struct TradeParameters { class="type">class="kw">double stopLoss; class="type">class="kw">double takeProfit; class="type">int magicNumber; }; class=class="str">"cmt">// Main structure class="kw">struct TradeSignal { class="type">class="kw">string symbol; class=class="str">"cmt">// Symbol name ENUM_ORDER_TYPE orderType; class=class="str">"cmt">// Order type(BUY or SELL) class="type">class="kw">double volume; class=class="str">"cmt">// Order volume TradeParameters params; class=class="str">"cmt">// Nested structure as parameter type }; class=class="str">"cmt">// Using the structure class="type">void OnStart() { class=class="str">"cmt">// Variable description for the structure TradeSignal signal; class=class="str">"cmt">// Initializing structure fields signal.symbol = Symbol(); signal.orderType = ORDER_TYPE_BUY; signal.volume = class="num">0.1; signal.params.stopLoss = class="num">20; signal.params.takeProfit = class="num">40; signal.params.magicNumber = class="num">12345; class=class="str">"cmt">// Using data in an expression Print("Symbol: ", signal.symbol); Print("Order type: ", signal.orderType); Print("Volume: ", signal.volume); Print("Stop Loss: ", signal.params.stopLoss); Print("Take Profit: ", signal.params.takeProfit); Print("Magic Number: ", signal.params.magicNumber); } TradeSignal signal = { "EURUSD", ORDER_TYPE_BUY, class="num">0.1, {class="num">20.0, class="num">40.0, class="num">12345} };
◍ 结构对齐与联合体绕过类型墙
MQL5 里的 struct 默认是打包存储,字段一个挨一个排。想控制占用字节数,要么手动补占位成员,要么用 pack() 操作符显式对齐——括号里只收 1、2、4、8、16 这几个数,对应字段对齐到多少字节边界。 排成员有个经验:大块数据放前面(比如 long 先于 int),能少踩很多坑。想知道某个字段离结构体开头偏移多少字节,用 offsetof 就行。拿前面的 TradeParameters 试,offsetof(TradeParameters, stopLoss) 实际打出 0,说明该字段正卡在头部。 [CODE] struct pack(sizeof(long)) MyStruct1 { // structure members will be aligned on an 8-byte boundary }; // or struct MyStruct2 pack(sizeof(long)) { // structure members will be aligned on an 8-byte boundary }; Print (offsetof(TradeParameters, stopLoss)); // Result: 0 //--- enum ENUM_LEAVES { rounded, oblong, pinnate }; //--- struct Tree { int trunks; ENUM_LEAVES leaves; }; //--- struct Bush { int trunks; ENUM_LEAVES leaves; }; //--- union Plant { Bush bush; Tree tree; }; //--- void OnStart() { Tree tree = {1, rounded}; Bush bush; Plant plant; // bush = tree; // Error! // bush = (Bush) tree; // Error! plant.tree = tree; bush = plant.bush; // No problem... Print(EnumToString(bush.leaves)); } //+------------------------------------------------------------------+ // Creating a type union AnyNumber { long integerSigned; // Any valid data types (see further) ulong integerUnsigned; double doubleValue; }; // Using AnyNumber myVariable; myVariable.integerSigned = -345; Print(myVariable.integerUnsigned); Print(myVariable.doubleValue); [/CODE] 代码逐行拆:pack(sizeof(long)) 即按 8 字节对齐;offsetof 那行直接输出 0 验证偏移。Tree 和 Bush 字段完全相同,但 bush = tree 会报错,因为不是同型或直继关系。借 Plant 联合体先塞 tree 再取 bush,绕过了赋值限制。AnyNumber 联合体演示同一存储单元被解释为 long / ulong / double,赋值 -345 后打印另两个视图,能看到二进制 reinterpret 的结果。 不含字符串、类、指针、动态数组的结构叫简单结构,能直接丢给外部 DLL 函数,也能在同型或直继间互拷;其余情况只能逐字段搬。若真要把 Bush 当 Tree 用,联合体是最快路径,但数字字段互转才稳。 联合体的禁区得记牢:动态数组、字符串、对象/函数指针、类、带构造析构的结构,以及嵌套了上述任一类型的结构,全不能进 union。结构里沾了 string,编译期直接红。外汇与贵金属 EA 调 DLL 本就高风险,结构体布局错一个字节,传参就可能让仓位计算偏掉。
class="kw">struct pack(class="kw">sizeof(class="type">long)) MyStruct1 { class=class="str">"cmt">// structure members will be aligned on an class="num">8-byte boundary }; class=class="str">"cmt">// or class="kw">struct MyStruct2 pack(class="kw">sizeof(class="type">long)) { class=class="str">"cmt">// structure members will be aligned on an class="num">8-byte boundary }; Print(offsetof(TradeParameters, stopLoss)); class=class="str">"cmt">// Result: class="num">0 class=class="str">"cmt">//--- enum ENUM_LEAVES { rounded, oblong, pinnate }; class=class="str">"cmt">//--- class="kw">struct Tree { class="type">int trunks; ENUM_LEAVES leaves; }; class=class="str">"cmt">//--- class="kw">struct Bush { class="type">int trunks; ENUM_LEAVES leaves; }; class=class="str">"cmt">//--- union Plant { Bush bush; Tree tree; }; class=class="str">"cmt">//--- class="type">void OnStart() { Tree tree = {class="num">1, rounded}; Bush bush; Plant plant; class=class="str">"cmt">// bush = tree; // Error! class=class="str">"cmt">// bush = (Bush) tree; // Error! plant.tree = tree; bush = plant.bush; class=class="str">"cmt">// No problem... Print(EnumToString(bush.leaves)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// Creating a type union AnyNumber { class="type">long integerSigned; class=class="str">"cmt">// Any valid data types(see further) class="type">ulong integerUnsigned; class="type">class="kw">double doubleValue; }; class=class="str">"cmt">// Using AnyNumber myVariable; myVariable.integerSigned = -class="num">345; Print(myVariable.integerUnsigned); Print(myVariable.doubleValue);