掌握 MQL5:从入门到精通(第四部分):关于数组、函数和全局终端变量·进阶篇
🧩

掌握 MQL5:从入门到精通(第四部分):关于数组、函数和全局终端变量·进阶篇

(2/3)· 从静态到动态数组、函数参数传递与全局终端变量,补齐 EA 内部数据组织的最后拼图

进阶 第 2/3 篇
不少交易者写 EA 时把历史价格硬塞进固定长度变量,行情一变长就数组越界崩溃。还有人试图用全局变量在指标间传数据,却分不清终端全局变量和局部变量的生命周期。先把数组维度与函数参数传递搞明白,能少掉很多莫名其妙的运行时报错。

序列索引反向这坑,先搞清再写EA

MT5 里开盘、收盘、最高、最低、 tick 量、真实量、价差、K线开盘时间以及指标值,都归为时间序列。程序员不能直接摸这些内部序列,只能靠一组预定义函数(CopyClose、CopyTime、iBarShift 等)把数据搬进自己的变量里再用。 搬完之后最反直觉的一点:物理内存里序列和普通动态数组一样,从最旧排到最新;但序列函数的编号从右往左,0 号是最右边那根还没收线的当前 K。普通数组不认这套,所以它眼里的「最后一根」刚好是序列的 0 号,方向完全反着。 图 4 和图 5 直观展示了这点——CopyRates 这类函数把数据拷进常规数组后,序列的第一个元素会变成数组的最后一个。EURUSD H4 上实测:未设序列时打印出 04:00 在前、08:00 在后;ArraySetAsSeries 设成 true 后,同样两个值变成 08:00 在前、04:00 在后。 两种写法可消掉这个别扭。若算法总是从最后一根 K 往前取数,直接对目标动态数组调 ArraySetAsSeries(arr, true),让标准和你自己代码用同一套索引。若是从图表任意位置抠小片段、且每步都明确知道数量,就老实接受反向索引,另写个校验函数兜住取值逻辑,别在表达式里裸写容易看走眼。 外汇与贵金属杠杆高、滑点跳空频繁,序列索引错一位可能让策略在错误 K 线上下单,实盘前务必在策略测试器用历史数据跑一遍索引打印。

MQL5 / C++
class="type">class="kw">datetime lastBarTime;      class=class="str">"cmt">// We&class="macro">#x27;ll try to write the last candle&class="macro">#x27;s time into this variable
class="type">class="kw">datetime lastTimeValues[]; class=class="str">"cmt">// The array will store the time of the last two candles. 
                           class=class="str">"cmt">//   It&class="macro">#x27;s dynamic so that it can be made into a time series to test indices
class=class="str">"cmt">// Get the start time of the current candlestick using the iTime function
lastBarTime = iTime(
							Symbol(),      class=class="str">"cmt">// Use the current symbol
							PERIOD_CURRENT, class=class="str">"cmt">// For the current timeframe
							class="num">0              class=class="str">"cmt">// Current candlestick
						);

Print("Start time of the class="num">0 bar is ", lastBarTime);
class=class="str">"cmt">// Get the start time of the last two candlesticks using the CopyTime function
CopyTime(
	Symbol(),      class=class="str">"cmt">// Use the current symbol
	PERIOD_CURRENT, class=class="str">"cmt">// For the current timeframe
	class="num">0,             class=class="str">"cmt">// Start with position class="num">0
	class="num">2,             class=class="str">"cmt">// Take two values
	lastTimeValues  class=class="str">"cmt">// Write them to array lastTimeValues("regular") array
);
Print("No series");
ArrayPrint(lastTimeValues,_Digits,"; "); class=class="str">"cmt">// Print the entire array to log. The separator between elements is a semicolon
ArraySetAsSeries(lastTimeValues,true);   class=class="str">"cmt">// Convert the array into a time series
Print("Series");
ArrayPrint(lastTimeValues,_Digits,"; "); class=class="str">"cmt">// Print the entire array again. Note the order of the data
class=class="str">"cmt">/* Script output:
class="num">2024.08.class="num">01 class="num">09:class="num">43:class="num">27.000	PrintArraySeries(EURUSD,H4)	Start time of the class="num">0 bar is class="num">2024.08.class="num">01 class="num">08:class="num">00:class="num">00
class="num">2024.08.class="num">01 class="num">09:class="num">43:class="num">27.051	PrintArraySeries(EURUSD,H4)	No series
class="num">2024.08.class="num">01 class="num">09:class="num">43:class="num">27.061	PrintArraySeries(EURUSD,H4)	class="num">2024.08.class="num">01 class="num">04:class="num">00:class="num">00; class="num">2024.08.class="num">01 class="num">08:class="num">00:class="num">00
class="num">2024.08.class="num">01 class="num">09:class="num">43:class="num">27.061	PrintArraySeries(EURUSD,H4)	Series
class="num">2024.08.class="num">01 class="num">09:class="num">43:class="num">27.061	PrintArraySeries(EURUSD,H4)	class="num">2024.08.class="num">01 class="num">08:class="num">00:class="num">00; class="num">2024.08.class="num">01 class="num">04:class="num">00:class="num">00
*/

◍ 函数定义的两个阶段与形参实参

MQL5 里写函数用的是统一模板:先写返回类型,再写函数名,括号里列参数类型和名字,大括号里放执行逻辑和 return。返回类型或参数列表都能用 void 表示“无”,比如无返回值的函数就不必写 return,也不存在结果变量。 一个硬限制是:函数不能嵌套定义,只能写在全部函数之外;同名函数可以重载,只要参数数量或类型、返回类型不同即可,但你必须自己厘清调用时到底命中哪一个。MT5 编译器最多允许一个函数带 63 个形式参数,日常写指标或 EA 基本碰不到上限。 定义函数只是“形式化算法”,不调用就不会发生任何事。定义时写的参数叫形式参数,代表事先未知、任意传入的数据;调用时填进去的具体变量、常量或字面量叫实际参数,类型和数量必须和形式参数对应,否则编译直接报错。下面这段代码里 diff 用两个整数做形参,return (a-b) 返回差值;若你调用时写 diff(1) 或 diff(1,2,3),编译器会立刻拦截。 重载时函数名能复用,比如把 diff 改成接收 double 参数就能算实数差,但调用方得清楚传的是整型还是双精度,不然结果类型可能偏离预期。

MQL5 / C++
ResultType Function_Name(TypeOfParameter1 nameOfParameter1, TypeOfParameter2 nameOfParameter2 …)
  {
   class=class="str">"cmt">// Description of the result variable and other local variables
   ResultType result;
   class=class="str">"cmt">// …
class=class="str">"cmt">//---
       class=class="str">"cmt">// Main actions are performed here
   class=class="str">"cmt">//---
   class="kw">return resut;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Example class="num">1                                                        |
class=class="str">"cmt">//| Comments are often used to describe what a function does,        |
class=class="str">"cmt">//|   what date it needs and why. For example, like this:            |
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//| The function returns difference between two integers.            |
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//| Parameters:                                                      |
class=class="str">"cmt">//|   class="type">int a is a minuend                                             |
class=class="str">"cmt">//|   class="type">int b is a subtrahend                                          |
class=class="str">"cmt">//| Return value:                                                   |
class=class="str">"cmt">//|   difference between a and b                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int diff(class="type">int a, class="type">int b)
{
class=class="str">"cmt">// The action is very simple, we do not create a variable for the result.
  class="kw">return (a-b);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Example 1a                                                       |
class=class="str">"cmt">//| The function returns the difference between two real numbers.    |
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//| Function name is as in the previous example, but parameter type  |
class=class="str">"cmt">//| differs                                                          |
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//| Parameters:                                                      |
class=class="str">"cmt">//|   class="type">class="kw">double a is a minuendе                                         |
class=class="str">"cmt">//|   class="type">class="kw">double b is a subtrahend                                       |
class=class="str">"cmt">//| Return value:                                                    |
class=class="str">"cmt">//|   difference between a and b                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+

「void 与无参函数的调用细节」

在 MQL5 里,函数返回类型写成 void 就代表不往外递任何值,调用时只管跑内部逻辑,不需要 return 收尾。上面 test() 里直接拿 Print 把 diff 的结果打出来,省掉了中间变量,适合做快速自检。 diff 本身接收两个 double 参数并返回差值。传整数 3 和 4 时,Print(diff(3,4)) 输出 -1;传双精度 3.0 和 4.0 时输出 -1.0。这个现象说明:实参类型会影响终端显示精度,但函数签名锁死的是形参类型,编译器按声明做隐式转换。 无参函数有两种写法:括号留空,或显式写 void。示例里的 name(void) 返回固定字符串 "Needed Name",在 EA 初始化阶段拿来给外部参数赋默认标签很顺手。外汇与贵金属波动剧烈、杠杆风险高,这类工具函数只解决拼接与调试,不替代仓位判断。 开 MT5 按 F4 建个脚本,把下面代码原样贴进去编译,切回图表看专家日志,能直接验证 -1 与 -1.0 的差异。

MQL5 / C++
class="type">class="kw">double diff(class="type">class="kw">double a, class="type">class="kw">double b)
{
class="kw">return (a-b);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Example class="num">2                                                        |
class=class="str">"cmt">//| Illustrates the use of "class="type">void".                                   |
class=class="str">"cmt">//| Calls(uses) the diff function                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void test()
{
class=class="str">"cmt">// You can do whatever you want.
class=class="str">"cmt">// For example, use the function from Example class="num">1.
  Print(diff(class="num">3,class="num">4)); class=class="str">"cmt">// the result is -class="num">1
class=class="str">"cmt">// Since when calling the diff function, integer parameters were
class=class="str">"cmt">//  passed in parentheses, the result is also class="type">int.
class=class="str">"cmt">// Now let&class="macro">#x27;s try to call the same function with class="type">class="kw">double precision parameters
  Print(diff(class="num">3.0,class="num">4.0)); class=class="str">"cmt">// the result is -class="num">1.0
class=class="str">"cmt">// Since the function is declared as "class="type">void", the "class="kw">return" statement is not needed
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Example class="num">3                                                        |
class=class="str">"cmt">//| The function has no parameters to process. We could use         |
class=class="str">"cmt">//|   empty parentheses as in the previous example or explicitly use |
class=class="str">"cmt">//|   the word "class="type">void"                                               |
class=class="str">"cmt">//| Return value:                                                   |
class=class="str">"cmt">//|   class="type">class="kw">string nameForReturn is some name , always the same           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string name(class="type">void)
{
  class="type">class="kw">string nameForReturn="Needed Name";
  class="kw">return nameForReturn;
}

局部与全局:变量在哪活、在哪死

在 MT5 写 EA 或脚本时,变量声明位置直接决定谁能读它、它能活多久。函数内部(含形参)声明的就是局部变量,调用时诞生、退出时销毁,别的函數根本看不到。 如果变量写在所有函数之外,它就变成全局变量,整个程序任何函数都能用,生命周期等同于程序运行期。上面的代码里 int globalVariable = 345;OnStart 外,因此任意函数里 Print(globalVariable) 都能输出 345。 花括号会切出更窄的块作用域。例程中 int k=4; 写在函数内的 {} 里,只能在那对括号内用;把 Print(k) 移到括号外编译直接报错。 同名遮蔽是实盘调试时的暗坑。局部变量可以和全局变量重名,此时函数内默认读到的是局部值。代码里全局 globalVariable=5,函数内又声明 int globalVariable=10Print(globalVariable) 打出 10;想强制读全局,得写 ::globalVariable,输出才是 5。外汇与贵金属波动剧烈、杠杆高风险大,这类作用域误读可能引发下单逻辑偏差,改完建议开 MT5 编译跑一遍验证。

MQL5 / C++
class="type">void OnStart()
  {
class=class="str">"cmt">//--- Local variable inside a function is visible to all blocks of that function, but not beyond it
    class="type">int myString = "This is local class="type">class="kw">string";
    class=class="str">"cmt">// Curly braces describe  a block inside a function
    {
      class="type">int k=class="num">4;  class=class="str">"cmt">// Block local variable - visible only inside curly braces
      Print(k);  class=class="str">"cmt">// It&class="macro">#x27;s ok
      Print(myString); 
    }
class=class="str">"cmt">// Print(k);    // Compilation error. The variable k does not exist outside the curly brace block.
  }
class="type">int globalVariable = class="num">345;
class="type">void OnStart()
  {
class=class="str">"cmt">//---
    Print(globalVariable); class=class="str">"cmt">// It&class="macro">#x27;s ok
  }
class="type">int globalVariable=class="num">5;
class="type">void OnStart()
  {
    class="type">int globalVariable=class="num">10;  class=class="str">"cmt">// The variable is described according to all the rules, including the type. 
                            class=class="str">"cmt">//   If the type were not declared, this expression would change the global variable
class=class="str">"cmt">//---
  
    Print(globalVariable);  class=class="str">"cmt">// The result is class="num">10 - that is, the value of the local variable
    Print(::globalVariable); class=class="str">"cmt">// The result is class="num">5. To print the value of a global variable, not the local one,
                             class=class="str">"cmt">//   we use two colons before the name
  }

◍ 用 static 把计数留在函数里

MT5 里普通局部变量随函数退出就释放,但有些场景你得在函数外面还记着上一次的状态。最典型的就是数函数被调了几次,或者盯盘时判断新 K 线有没有出来——后者要在每次 tick 拿当前时间跟上次存的时间比,全局变量能用但容易让别的模块误改,埋坑。 这时候给局部变量加一个 static 前缀,生命周期就拉到跟全局一样长,作用域却还锁在函数内。下面这段演示了静态计数器:连续三次调用,日志依次打出 1 calls、2 calls、3 calls,说明 counter 没被重置。 [CODE] //+------------------------------------------------------------------+

//Script program start function

//+------------------------------------------------------------------+ void OnStart() { //--- HowManyCalls(); HowManyCalls(); HowManyCalls(); } //+------------------------------------------------------------------+

//The function counts the number of requests

//+------------------------------------------------------------------+ void HowManyCalls() { //--- Variable description. The variable is local, but its lifetime is long. static int counter=0; // Since 'static' keyword is used, the variable is initialized only // before he first function call // (more precisely, before the OnInit function call) //--- Main actions counter++; // During program execution, the value will be stored till the end //--- Operation result Print( IntegerToString(counter)+" calls"); } // Script output: // 1 calls // 2 calls // 3 calls [/CODE] 逐行看关键点:static int counter=0 只在第一轮(严格说 OnInit 前)初始化一次;之后每次进函数 counter++ 都基于上次保留的值累加;Print 把整数转字符串拼上 " calls" 输出。 把这套搬去 EA 里,static datetime lastbar 就能低成本判新柱,不必开全局变量。外汇和贵金属行情跳空频繁,这类状态变量要留意重置时机,否则可能误判 bar 边界。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
 {
class=class="str">"cmt">//---
  HowManyCalls();  
  HowManyCalls();  
  HowManyCalls();  
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| The function counts the number of requests                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void  HowManyCalls()
 {
class=class="str">"cmt">//--- Variable description. The variable is local, but its lifetime is class="type">long.
  class="kw">static class="type">int counter=class="num">0;  class=class="str">"cmt">// Since &class="macro">#x27;class="kw">static&class="macro">#x27; keyword is used, the variable is initialized only
                         class=class="str">"cmt">//   before he first function call 
                         class=class="str">"cmt">//   (more precisely, before the OnInit function call)
class=class="str">"cmt">//--- Main actions
  counter++;             class=class="str">"cmt">// During program execution, the value will be stored till the end
class=class="str">"cmt">//--- Operation result
  Print( IntegerToString(counter)+" calls");
 }
class=class="str">"cmt">// Script output:
class=class="str">"cmt">// class="num">1 calls
class=class="str">"cmt">// class="num">2 calls
class=class="str">"cmt">// class="num">3 calls

「改不动的还是改得动的:参数传递的引用门槛」

MQL5 里函数默认拿的是数据副本,也就是「按值」传参。你在函数里把形参改了天翻地覆,外面的原变量纹丝不动。想让函数直接动到源数据,形参前面得挂个 &(与号),这叫「按引用」传递。 上面这段脚本把差异摊得很直白:OnStart 里 first=3、second=77,Swap 走引用,调完之后日志显示 first=77、second=3,原值被调换了;CheckLocal 走值传递,里面把 a、b 改成 5 和 10,出来一打印还是 77 和 3,外部毫无波动。 有个硬规矩得刻进肌肉记忆:枚举、结构、对象、任何数组这类复杂类型,编译器只认引用传递。你敢写按值传,MT5 编译阶段直接报错,根本跑不起来。 全局变量活到程序退出,局部变量随声明块消亡;同名时局部覆盖全局。写 EA 或脚本时,若你期望子函数回写计算结果,漏写 & 就会静默失效,这种 bug 在盯盘逻辑里可能让信号判断偏离预期。外汇与贵金属杠杆高,策略代码错一处,实盘风险会被放大。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart(class="type">void)
  {
class=class="str">"cmt">//--- Declare and initialize two local variables
   class="type">int first = class="num">3;
   class="type">int second = class="num">77;
class=class="str">"cmt">//--- Print their values BEFORE all changes
   Print("Before swap: first = " + first + " second = " + second);
class=class="str">"cmt">//--- Use the Swap function, which takes data by reference
   Swap(first,second);
class=class="str">"cmt">//--- See what happened
   Print("After swap: first = " + first + " second = " + second);
class=class="str">"cmt">//---
class=class="str">"cmt">//--- Apply the CheckLocal function to the received data
class=class="str">"cmt">//---   This function takes parameters by value
   CheckLocal(first,second);
class=class="str">"cmt">//--- Print the result again
   Print("After CheckLocal: first = " + first + " second = " + second);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Swaps the values of two integer variables                        |
class=class="str">"cmt">//|   Data is passed by reference, so the originals will be modified  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Swap(class="type">int &a, class="type">int& b) class=class="str">"cmt">// It can be done in any way, both positions are correct
  {
   class="type">int temp;
class=class="str">"cmt">//---
   temp = a;
   a = b;
   b = temp;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Takes parameters by value, that is why changes happen             |
class=class="str">"cmt">//|   only locally                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CheckLocal(class="type">int a, class="type">int b)
  {
   a = class="num">5;
   b = class="num">10;
  }
class=class="str">"cmt">// Script output:
class=class="str">"cmt">// Before swap: first = class="num">3 second = class="num">77
class=class="str">"cmt">// After swap: first = class="num">77 second = class="num">3
class=class="str">"cmt">// After CheckLocal: first = class="num">77 second = class="num">3
把重复劳动交给小布
这些数组越界与跨程序变量诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到告警,你专注策略逻辑而非底层排错。

常见问题

总维数不超过 4,任意维度最大元素数为 2147483647,超出会在编译或运行时受限。
时间序列数组由终端按时间倒序填充,索引 0 为最新一根 K,访问历史需按偏移取,普通数组则按你写入顺序正序编号。
可以,小布盯盘的 AIGC 模块会扫描按引用传参导致的外部状态污染,并在品种页提示可能的风险点。
静态变量随程序实例存活,EA 重载即重置;全局终端变量存在终端层,重启终端前跨 EA 调用都还在。
动态数组由 ArrayResize 或系统时间序列绑定在运行时分配,编译期留空方括号即表示动态,属合法写法。