值参数和引用参数

自变量可以通过两种方式传递给函数:通过值传递和通过引用传递。

到目前为止,我们看到的所有情况都是通过值传递的。这种方法意味着,通过调用代码片段准备的自变量值被复制到一个新变量中,也就是与函数对应的输入变量。否则,自变量和输入变量无法关联起来。函数内部变量的所有后续操作都不会以任何方式影响自变量。

为说明引用参数,请在类型的右侧添加一个 & 符号。许多程序员喜欢在参数名后面附加一个 & 符号,从而强调参数是对给定类型的引用。例如,以下各项是等效的:

void func(int &parameter);
void func(int & parameter); 
void func(intparameter);

函数被调用时,不会为引用参数创建对应的局部变量。相反,为该参数指定的自变量,在函数内部以输入参数的名称(别名)出现。因此,该值不会被拷贝,而是在内存中的相同地址处使用。因此,对函数中参数的修改体现在其关联自变量的状态。由此出现一个重要的特征。

只能指定一个变量(左值,参见 赋值运算符)作为引用参数的自变量。否则,我们将遇到错误“参数作为引用传递,应为变量”。

按引用传递用于以下几种情况:

  • 通过消除值拷贝来提高程序的效率;
  • 在使用 return 返回单个值还不够的情况下,将修改后的数据从函数传递给发起调用的代码;

第一点对于潜在的大型变量(如字符串或数组)尤其重要。

为了区分引用参数的第一个和第二个用途,如果作者不希望改变函数内部的参数时,则建议添加 const 修饰符。这将提醒您并向其他开发人员表明,在函数内部传递变量不会带来副作用。

如果可以对引用参数应用 const 修饰符而未应用,则会导致整个函数调用层次结构出现问题。事实上,调用这样的函数需要非常量自变量。否则,将出现错误“常量变量不能作为引用传递”。结果可能会逐渐导致为了代码的可编译性,而不得不从所有函数中的所有参数中去掉 const 修饰符。这实际上扩大了潜在 bug 的范围,无意中破坏了变量。应该以相反的方式纠正这种情况:在不需要返回和修改值的地方使用 const

为了比较在 FuncDeclaration.mq5 脚本中传递参数的几种方式,实现了几个函数:FuncByValue ― 按值传递,FuncByReference― 按引用传递, FuncByConstReference ― 按常量引用传递。

void FuncByValue(int v)
{
   ++v;
   // we are doing something else with v
}
 
void FuncByReference(int &v)
{
   ++v;
}
 
void FuncByConstReference(const int &v)
{
   // error
   // ++v; // 'v' - constant cannot be modified
   Print(v); 
}

OnStart 函数中,我们调用所有这些函数,并观察它们对用作自变量的 i 变量的影响。注意,通过引用传递参数不会改变函数调用语法。

void OnStart()
{
   int i = 0;
   FuncByValue(i);          // i cannot change
   Print(i);                // 0
   FuncByReference(i);      // i is changing
   Print(i);                // 1
   FuncByConstReference(i); // i cannot change, 1
   const int j = 1;
   // error
   // 'j' - constant variable cannot be passed as a reference
   // FuncByReference(j);
   
   FuncByValue(10);         // ok
   // error: '10' - parameter passed as reference, variable expected
   // FuncByReference(10);
}

字面量只能传递给 FuncByValue 函数,因为其他函数需要引用(即变量)作为自变量。

FuncByReference 函数不能用变量 j 调用,因为后者被声明为常量,并且该函数声明了改变其参数的能力(或意图),因为它没有 const 修饰符。这将产生错误“常量变量不能作为引用传递”。

脚本还介绍了 Transpose 函数:该函数用于转置一个 2x2 矩阵,该矩阵通过引用作为二维数组传递。

void Transpose(double &m[][2])
{
   double temp = m[1][0];
   m[1][0] = m[0][1];
   m[0][1] = temp;
}

OnStart 函数调用演示了本地数组 a内容的预期变化。

double a[2][2] = {{-12}, {30}};
Transpose(a);
ArrayPrint(a);

在 MQL5 中,数组参数总是作为动态数组的内部结构传递(参见 数组特性 一节)。因此,这种参数的说明必须在第一维中具有开放大小,即第一对方括号内为空。

这并不妨碍在必要时向函数传递实际自变量,实际自变量是一个定长数组(如我们的例子所示)。然而,ArrayResize 之类的函数无法调整大小或以其他方式重新组织这种掩码定长数组。

除了第一维之外,数组在所有维度上的大小都必须与参数和自变量相匹配。否则,我们将遇到错误“不允许参数转换”。具体来说,TransposeVector 函数在例子中定义如下:

void TransposeVector(double &v[])
{
}

尝试对二维数组 a 调用该函数在 OnStart 中被注释掉了,因为这生成了上面的错误:数组维数不匹配。

除了通过值或引用来传递参数,还有另一种方法:传递指针。与 C++ 不同,MQL5 只支持用于对象类型的 指针用于)。我们将在第三章研究这个特性。