外部变量

本节的内容十分复杂,可以选读。本节要求读者了解基于 C++ 类别的概念,以及下文中讨论的内容。同时,所述语言结构的效果可以通过其他方式实现,而这种灵活性则成为了潜在错误来源。

MQL5 允许将变量描述为外部变量。使用 extern 关键字即可实现,并且仅允许在 全局上下文中使用。

外部变量的语法基本与普通变量声明一致,但需额外添加 extern 关键字,且禁止初始化:

extern type identifier;

将变量描述为外部变量,意味着它的说明被延迟了,并且必须在源代码后面出现,通常是在另一个文件中(有关使用 #include 指令的连接文件 将在 预处理器相关章节中讨论)。多个不同的源文件可以具有同一个外部变量的说明,即具有相同类型和标识符的文件。所有这些说明都引用同一个变量。

假设该变量将在其中一个文件中完整描述。如果没有使用 extern 关键字在代码的任何位置定义变量,则返回编译错误“无法解析的外部变量”(这种情况下与 C++ 中的链接器错误类似)。

必须先描述外部变量,然后才能在特定文件的源代码中实际使用。也就是说,即使该变量并非在本模块中创建,也允许编译当前模块。

MQL5 中使用 extern 并不像在 C++ 中那样是强制要求,大多数情况下,可以通过以下方式来替代:启用一个头文件,其中包含要声明为 extern 的变量的一般说明。按照惯例进行这些定义就足够了。编译器确保每个附加文件仅被添加到源代码一次。鉴于在 MQL5 中,一个程序总始终包含一个可编译单元 mq5,因此不存在 C++ 中因在不同单元中启用了头文件导致变量定义重复的潜在错误。

即使在 #include 指令中附加了 mq5(并非 mqh)文件,也无法与启动编译的主单元公平竞争,而是被视为一个头文件。

与 C++ 不同,MQL5 不允许为外部变量指定初始值(C++ 中初始化会导致忽略 extern)。如果尝试设置一个初始值,会遇到编译错误“不允许外部变量初始化”。

总的来说,将变量描述为外部变量可以被认为是一种“软”说明:它确保了变量存在,并避免了在未使用 extern 修饰符的情况下,因在多个文件中描述变量而引发的覆盖错误。

然而,这也可能成为错误的来源。在不同的头文件中,如果出于不同目的碰巧描述了相同变量,那么在没有 extern 关键字的情况下,编译器可以识别名称冲突,而如果使用了 extern,则所有变量都成为了一个变量,程序运算逻辑很可能崩溃。

变量和函数都可以被描述为外部(这部分内容将在 下文介绍)。对于函数来说,通过属性将它们描述为外部是一种遗留用法(即,虽然被编译了,但便会进行任何更改)。函数的以下两种声明方式是等效的:

extern return_type name([parameters]);
      return_type name([parameters]);

从这个意义上来说,是否使用 extern 仅用于风格上的区分:函数的前向说明是来自当前单元(不使用 extern)还是外部单元(使用 extern)。

可在待编译的 mq5 单元和待附加的头文件中使用 extern

我们来分析 extern 的一些使用场景:它们写入在不同文件中,比如主脚本 ExternMain.mq5 和 3 个附加文件:ExternHeader1.mqhExternHeader2.mqhExternCommon.mqh

在主文件中,只附加了 ExternHeader1.mqhExternHeader2.mqh,而稍后我们需要 ExternCommon.mqh

// source code from mqh files will be substituted implicitly
// in the main mq5 file, instead of these directives
#include "ExternHeader1.mqh"
#include "ExternHeader2.mqh"

在头文件中,定义了两个条件性实用函数:在第一个头文件中,为 x 变量递增定义了 inc 函数,而在第二个头文件中,为 x 变量递减定义了 dec 函数。变量 x 在两个文件中都被描述为外部变量:

// ExternHeader1.mqh
extern int x;
void inc()
{
   x++;
}
// -----------------
// ExternHeader2.mqh
extern int x;
void dec()
{
   x--;
}

由于采用了这种说明,每个 mqh 文件都以常规方式编译。当它们一起包含在 mq5 文件中时,整个程序也能被编译。

如果在每个文件中定义变量时没有使用 extern 关键字,那么在编译整个程序时会发生“重新定义”错误。如果我们将 x 的定义从头文件转移到主单元,头文件就会停止编译(也许对某些人来说这不是问题;但在大型程序中,开发人员喜欢检查即时更正的编译能力,而不是编译整个项目)。

在主脚本中,我们定义了一个变量(在本例中,初始值为 2,如果我们没有指定值,将使用默认值 0),调用有条件的实用函数并打印 x 值。

int x = 2;
   
void OnStart()
{
   inc();  // uses x
   dec();  // uses x
   Print(x); // 2
   ...
}

short z 变量的说明(不使用 extern)包含在 ExternHeader1.mqh 文件中。主脚本中注释了一个类似的说明。如果我们激活这个字符串,将遇到之前提到的错误(“变量已定义”)。这样做是为了说明潜在的问题。

ExternHeader1.mqh 中,还描述了 extern long y。与此同时,在 ExternHeader2.mqh 文件中,同名异义的外部变量具有另一种类型:extern short y。如果后面的说明没有被预先转移到注释中,那么这里就会出现类型不兼容错误(“变量 'y' 已经用不同类型定义”)。总结:类型必须一致或者变量不能是外部变量。如果这两个条件都无法满足,这意味着其中一个变量的名称有拼写错误。

此外,应该注意,变量 y 没有显式初始化。但主脚本成功地调用了该变量,并在日志中打印 0:

long y;
   
void OnStart()
{
   ...
   Print(y); // 0
}

最后,脚本支持用户尝试替代外部双变量,以已知变量 x 为例。不用描述 extern int xExternHeader1.mqhExternHeader2.mqh 文件都可以包含另一个公共头文件 ExternCommon.mqh,其中包含 int x 的说明(不使用 extern)。它作为项目中对 x 的唯一说明。

激活 USE_INCLUDE_WORKAROUND 时,会启用这种组装程序的替代模式:它位于脚本开头的注释中:

#define USE_INCLUDE_WORKAROUND // this string was in the comment
#include "ExternHeader1.mqh"
#include "ExternHeader2.mqh"

在这种配置中,特定的 include 文件以及整个项目仍然是可编译的。在实际项目中,如果不使用这种方法,公共 mqh 文件将无条件地包含在 ExternHeader1.mqhExternHeader2.mqh 中(无 USE_INCLUDE_WORKAROUND 条件)。在本例中,指令的两个线程之间的切换是基于 USE_INCLUDE_WORKAROUND 宏,该宏仅用于演示这两种模式。例如,ExternHeader2.mqh 的简化版本应如下所示:

// ExternHeader2.mqh
#include "ExternCommon.mqh" // int x; now here
 
void dec()
{
   x--;
}

在 MetaEditor 日志中,我们可以发现 ExternCommon.mqh 文件仅被加载一次,虽然它在 ExternHeader1.mqhExternHeader2.mqh 文件中都被引用。

'ExternMain.mq5'
'ExternHeader1.mqh'
'ExternCommon.mqh'
'ExternHeader2.mqh'
code generated

如果 x 变量在 ExternCommon.mqh 中“注册”,我们不得在主单元中重新定义它(不使用 extern),因为这会导致编译错误,但我们可以在算法开头为该变量赋予所需的值。