初始化

在描述变量时,有可能会设置初始值;初始值是在变量名和 '=' 符号之后指定的,并且必须与变量类型对应,否则就会被强制转换为该类型(类型强制转换请参见相关 章节)。

int i = 3jk = 10;

此处 ik 被显式初始化,而 j 没有。

常量(相关类型的字面量)和表达式(一种计算公式)都可以被指定为初始值。我们将单独讨论 表达式 。先看一个简单的例子:

int i = 3j = ik = i + j;

此处,j 变量取与 i 变量相同的值,而 k 变量取 ij 之和。严格地说,这三种情况都涉及到表达式。不过常量 (3) 比较特殊,是一个退化表达式选项。在第二种情况下,唯一的变量名是一个表达式,即表达式结果将作为此变量的值(不进行任何转换)。在第三种情况下,表达式中访问 ij 两个变量,用它们的值执行加法运算,之后运算结果传递给 k 变量。

由于包含多个变量说明的语句是从左到右处理,所以编译器在分析另一个说明时已经知道了前面变量的名称。

一个程序通常包含许多带有变量说明的语句。编译器以自上而下的自然方式读取语句。在后面的初始化中,可以使用取自前面说明中的名称。以下是由两个不同语句描述的相同变量。

int i = 3j = i;
int k = i + j;

未进行显式初始化的变量也会获得一些初始值,但具体初始值取决于描述变量的位置,即变量的上下文。

在未进行初始化的情况下,局部变量在生成时取随机值:编译器根据类型大小为局部变量分配内存,而具体地址处的内容未知(计算机内存区域常因先前执行的程序不再需要而重新分配给其他程序使用)。

通常建议在算法代码中通过后续操作向未初始化的局部变量写入有效值,比如使用我们稍后要介绍的 赋值运算 。从语法上来说,这种操作类似于初始化,因为也会使用等号 '=’ 将值从它右边的结构体(可以是常量、变量、表达式或函数调用)传递给左边的变量。'=' 左边只能是变量。

程序员应该确保只有在为未初始化的变量赋予了有意义的值之后再读取其内容。否则编译器会显示一个警告(“可能使用了未初始化的变量”)。

而如果使用全局变量,情况就完全不同了。

全局变量的一个例子是第二章中 GoodTime2 脚本的 GreetingHour 输入参数。使用 input 关键字描述变量并不影响它作为变量而具备的其他特性。我们可以不对其进行初始化,以如下方式描述:

input uint GreetingHour;

这不会对程序产生任何影响,因为如果没有进行显式初始化,编译器会将全局变量隐式初始化为零(我们以前也将它显式初始化为零)。

无论是哪种变量类型,隐式初始化总是由一个等于零的值来执行。例如,对于 bool 变量,设置为 false,而对于 datetime 变量,设置为 D'1970.01.01 00:00:00'。对于字符串,则有一个特殊的值 NULL。可以说,这是一个比空引号 "" 更“空”的字符串,仍然空引号仍会被分配一些内存并存储终止空字符。

除了局部变量和全局变量,还有另一种类型,即静态变量。如果程序员没有显式地写入初始值,编译器会将它们隐式初始化为零。将在 下一节中介绍静态变量。

我们来创建一个新脚本,VariableScopes.mq5,用于演示局部变量和全局变量(MQL5/Scripts/MQL5Book/VariableScopes.mq5)。

// global variables
int ijk;    // all are 0s
int m = 1;      // m = 1                (place breakpoint on this line)
int n = i + m;  // n = 1
void OnStart()
{
  // local variables
  int xyz;
  int k = m// warning: declaration of 'k' hides global variable
  int j = j// warning: declaration of 'j' hides global variable
  // use variables in assignment statements  
  x = n;     // ok, 1
  z = y;     // warning: possible use of uninitialized variable 'y'
  j = 10;    // change local j, global j is still 0
}
// compilation error
// int bad = x; // 'x' - undeclared identifier

切记,在启动 MQL 程序时,终端首先初始化所有全局变量,然后调用一个作为相关类型程序的起始点的函数。在本例中,对于脚本来说,这个函数是 OnStart

此处,仅变量 i, j, k, m, n 是全局的,因为它们在函数外部描述(本例中只存在脚本所必需的 OnStart 函数)。 i, j, k 隐式取值 0。mn 包含 1。

可以在调试模式下逐步运行脚本,并确认变量值完全是以这种方式变化。为此,您应该预先在其中一个全局变量的初始化代码行上设置 断点 ,比如在 m 变量上。将文本光标放在这个字符串上,执行 Debug -> Toggle Breakpoint(F9),该字符串将在左边字段中以蓝色标记高亮显示,这表示如果程序在调试器中运行,则执行将此处就会停止。

然后,您应该实际运行程序进行调试,为此请执行命令 Debug -> Start on real data(F5)。此时,将在终端打开新的图表,并开始在此图表中执行脚本(右上角出现标题 "VariableScopes (Debugging)"),但是执行立即中止,系统回到 MetaEditor。我们应该看到一张图,如下所示。

在 MetaEditor 中进行逐步调试和查看变量

在 MetaEditor 中进行逐步调试和查看变量

包含断点的字符串以箭头标记,这是程序准备执行但尚未执行的当前语句。程序的当前栈显示在左下方,目前只包含一个条目:@global_initializations。可在右下角输入表达式来监控它们的实时值。我们想查看变量值,因此需要连续输入 i, j, k, m, n, x, y, z(每行输入一个变量)。

你将看到,MetaEditor 自动会添加当前上下文的变量以便我们查看(例如,函数内部执行语句时的局部变量和函数输入)。但是现在,我们要提前手动添加 x, yz,只是为了证明它们不是在函数外部定义的。

请注意,对于局部变量,会显示“未知标识符”而不是一个值,因为此时尚未出现这些局部变量所在的 OnStart 函数代码块。全局变量 ij 将首先获得零值。全局变量 k 没有在任何地方使用,因此,编译器排除了这个变量。

如果我们使用 Step Into(F11) 或者 Step Over (F10) 命令来执行程序的一个步骤(在当前代码行执行语句),我们将看到变量 m 取值为 1。下一步是继续初始化变量 n,该变量也会变为 1。

全局变量的说明在此处结束,正如我们所知,终端在全局变量初始化完成时调用 OnStart 函数。在这种情况下,要以步进模式进入 OnStart 函数,请再次按 F11(或者也可以在 OnStart 函数开头再设置一个断点)。

当程序语句执行到定义了局部变量的代码块时,局部变量会被初始化。因此,x, y, z 变量只有单步进入 OnStart 函数时才会被创建。

当调试器进入 OnStart 函数时,如果运气好的话,你可以看到 x, yz 中确实有初始随机值。这里的“运气”是指这类随机值很可能恰好是零。那么就无法将它们与编译器为全局变量隐式执行的隐式初始化(值为零)区分开来。如果脚本反复启动,局部变量中产生的“垃圾”可能会不同,这更能说明问题。局部变量没有被显式初始化,因此其内容可以是任何类型。

在以下的图像序列中,您可以使用调试器的逐步模式查看变量的变化过程。待执行(但尚未执行)的当前字符串在枚举的字段上用绿色箭头标记。

在 MetaEditor 中进行逐步调试和查看变量(字符串 23)

在 MetaEditor 中进行逐步调试和查看变量(字符串 23)

在 MetaEditor 中进行逐步调试和查看变量(字符串 24)

在 MetaEditor 中进行逐步调试和查看变量(字符串 24)

代码中进一步演示了如何以最简单的方式通过赋值运算符来使用这些变量。全局变量 n 的值被成功拷贝到局部变量 x 中,因为 n 已经初始化。但是,在将变量 y 的值复制到变量 z 的字符串中,编译器发出一个警告,因为 y 是局部变量,而到目前为止,还没有在其中写入任何值,即它没有被显式初始化,也没有其他运算符为它赋值。

在函数内部,允许使用与全局变量相同的名称来描述变量。如果在内部代码块中创建的变量的名称存在于外部代码块中,则在嵌套的局部代码块中也会出现类似的情况。但不建议采取这种做法,因为可能会导致逻辑错误。在这种情况下,编译器会给出警告(“声明隐藏了全局/局部变量”)。

由于这种重新定义,局部变量(如上例中的 k)会在函数内部覆盖同名的全局变量。虽然这两个变量名称相同,但实际上是两个不同的变量。局部变量 k 仅在 OnStart 内部可见,而全局变量 k 在除了 OnStart 以外的任何地方都是可见的。也就是说,变量 k 的任何代码块内操作只会影响局部变量。因此,OnStart 函数退出时(假设它不是脚本唯一的核心函数),我们会发现全局变量 k 仍然为零。

局部变量 j 不仅会覆盖全局变量 j,而且还会以后者的值完成初始化。在 OnStart 内包含 j 的说明的字符串中,当从 j 的局部版本读取初始值时,j 的局部版本还在创建过程中(创建还未完成)。在成功定义了局部变量 j 之后,它与全局版本同名,但它是局部版本,后续对 j 的改变都属于局部版本。

在源代码的结尾,我们注释了声明另一个全局变量 bad 的尝试,其初始化中调用了变量 x 的值。该字符串会导致编译器错误,因为变量 xOnStart 函数内定义的,在该函数外部则不可见。