运算符重载
在 表达式 一章中,我们学习了为内置类型定义的各种运算。例如,对于 double 类型的变量,我们可以计算以下表达式的值:
double a = 2.0, b = 3.0, c = 5.0;
|
使用类似的语法处理用户定义类型(例如矩阵)会很方便:
Matrix a(3, 3), b(3, 3), c(3, 3); // creating 3x3 matrices
|
MQL5 通过运算符重载提供了这样的功能。
这种技术的组织方式是:通过描述一些方法来实现,其名称以 operator 关键字开头,后跟一个受支持的运算符号(或符号序列)。一般来说,这可以表示为如下形式:
result_type operator@ ( [type parameter_name] ); |
此处 @ - 运算符号。
完整的 MQL5 运算列表已在 运算优先级一节中提供,但并非所有运算都允许重载。
以下运算禁止重载:
- 冒号 '::',上下文权限;
- 括号 '()'、“函数调用”或“分组”;
- 点 '.'、“取消引用”;
- 与号 '&'、“取地址”、一元运算符(但与号可用作二元运算符“按位与”);
- 条件三元运算符 '?:';
- 逗号 ','。
所有其他运算符均可重载。重载运算符的优先级无法更改,它们保持与标准优先级相同,因此如有必要,应使用括号进行分组。
您不能为标准列表中未包含的某些新字符创建重载。
所有运算符的重载都会考虑其一元性和二元性,即保留所需操作数的数量。与任何类方法一样,运算符重载可以返回某种类型的值。在这种情况下,选择类型本身时应根据规划好的逻辑,也就是在表达式中使用函数所产生的结果(请参见下文)。
运算符重载方法的格式如下(用所需运算符的符号代替 '@' 符号):
名称 |
方法头 |
表达式中的 |
函数 |
---|---|---|---|
一元前缀 |
type operator@() |
@object |
object.operator@() |
一元后缀 |
type operator@(int) |
object@ |
object.operator@(0) |
二元 |
type operator@(type parameter_name) |
object@argument |
object.operator@(argument) |
索引 |
type operator[](type index_name) |
object[argument] |
object.operator[](argument) |
一元运算符不接受参数。在一元运算符中,只有递增运算符 '++' 和递减运算符 '--' 除了支持前缀形式外,还支持后缀形式,所有其他一元运算符仅支持前缀形式。指定 int 类型的匿名参数即表示后缀形式(以区别于前缀形式),但参数本身会被忽略。
二元运算符必须接受一个参数。对于同一个运算符,可以存在多个重载的变体,这些变体可带有不同类型的参数,包括与当前对象所属的类相同的类型。在这种情况下,对象作为参数只能按引用或按指针进行传递(“按指针”仅适用于类对象,不适用于结构体)。
重载运算符既可以通过表达式中的运算语法使用(这是重载的主要原因),也可以通过方法调用语法使用;这两种方式都显示在上表中。这种功能等效性清晰地表明,从技术角度来说,运算符不过是对对象的方法调用,对于前缀运算符,对象在右侧,而对于所有其他运算符,对象在左侧。二元运算符方法将作为自变量传递给运算符右侧的值或表达式(特别要指出,这可以是另一个对象或内置类型的变量)。
由此可见,重载运算符不具有交换律属性:a@b 通常不等于 b@a,因为对于 a,@ 运算符可以重载,但 b 不能。此外,如果 b 是内置类型的变量或值,那么原则上不能重载它的标准行为。
举个例子,试想用于从斐波那契数列生成数字的 Fibo 类(我们已经使用函数完成了此任务的一个实现,请参见 函数定义)。在类中,我们将提供 2 个字段,分别用于存储行的当前编号和上一个编号:current 和 previous。默认构造函数会将它们的值初始化为 1 和 0。我们还将提供一个拷贝构造函数 (FiboMonad.mq5)。
class Fibo
|
对象的初始状态:当前编号为 1,上一个编号为 0。为了查找序列中的下一个编号,我们重载了前缀和后缀递增运算符。
Fibo *operator++() // prefix
|
请注意,前缀方法在编号修改后不会返回指向当前 Fibo 对象的指针,而后缀方法则会返回一个保存了上一个计数器的新对象,这符合后缀递增的原则。
当然,如果需要,程序员可以任意方式重载任何运算。例如,可以计算乘积、将数字输出到日志中,或者在递增的实现中执行其他操作。但还是建议遵循以下原则:使用运算符重载执行直观操作。
我们以类似的方式实现递减运算:递减运算将返回序列的上一个数字。
Fibo *operator--() // prefix
|
要获取给定数字在序列中的编号,我们需要重载索引访问运算。
Fibo *operator[](int index)
|
要获取 current 变量中包含的当前数字,我们需要重载 '~' 运算符(因为它很少使用)。
int operator~() const
|
如果没有此重载,您仍然需要实现一些公共方法来读取私有字段 current。我们将使用此运算符通过 Print 输出数字。
为了方便起见,您也应该重载赋值运算。
Fibo *operator=(const Fibo &other)
|
我们来看看它是如何工作的。
void OnStart()
|
结果符合预期。不过,我们需要考虑一个细节。
Fibo f5;
|
指针赋值运算符的重载仅在通过对象访问时有效。如果通过指针访问,则采用指针间的标准赋值。
重载运算符的返回类型可以是内置类型、对象类型(类或结构体)或指针(仅限类对象)中的一种。
要返回对象(实例,而非引用),类必须实现拷贝构造函数。这种方式会导致实例重复,从而影响代码效率。如果可能,应该返回一个指针。
但在返回指针时,需要确保返回的不是局部自动对象(该对象在函数退出时会被删除,指针也将失效),而是某个已经存在的对象。通常会返回 &this。
通过返回对象或对象的指针,您可以将一个重载运算符的结果“发送”给另一个重载运算符,从而以我们处理内置类型惯用的方式构建复杂的表达式。返回 void 将导致无法在表达式中使用该运算符。例如,如果 '=' 运算符定义为 void 类型,则多重赋值将失效:
Type x, y, z = 1; // constructors and initialization of variables of a certain class
|
赋值链从右到左运行,y = z 将返回空。
如果对象仅包含内置类型的字段(包括数组),则无需重新定义来自同一类中对象的赋值/拷贝运算符 '=':MQL5 默认提供所有字段的“一对一”拷贝。请勿将赋值/拷贝运算符与拷贝构造函数和初始化混淆。
现在,我们来看第二个示例:使用矩阵 (Matrix.mq5)。
顺便提一下,内置对象类型 矩阵和向量 最近已出现在 MQL5 中。使用内置类型还是自定义类型(或者将它们组合使用)由每位开发者自行决定。内置类型中包含许多常用方法的现成快速实现,非常方便,避免了繁琐的编程工作。而自定义类允许您根据任务调整算法。我们在这里提供了 Matrix 类作为教程。
在矩阵类中,我们将其元素存储在一维动态数组 m 中。在“sizes”下,选择变量 rows 和 columns。
class Matrix
|
主构造函数接受两个参数(矩阵维度)并为数组分配内存。还有一个拷贝构造函数来自另一个矩阵 other。此处和下文中大量使用了用于处理数组的内置函数(具体而言是 ArrayCopy、ArrayResize、ArrayInitialize),我们将在单独 章节中讨论这些函数。
我们通过重载赋值运算符来安排从外部数组填充元素的操作:
Matrix *operator=(const double &a[])
|
为了实现两个矩阵的加法,我们重载了 '+=' 和 '+' 运算符:
Matrix *operator+=(const Matrix &other)
|
请注意,'+=' 运算符在修改当前对象后返回该对象的指针,而 '+' 运算符则按值返回一个新实例(将使用拷贝构造函数),并且该运算符本身带有 const 修饰符,因此不会更改当前对象。
'+' 运算符本质上是一个包装器,它先创建当前矩阵的临时副本以备调用(命名为 temp),然后将所有工作委派给 ‘+=’ 运算符。因此,通过内部调用 '+=' 运算符(会修改 temp)将 temp 与 other 相加,然后返回 ' +' 运算的结果。
矩阵乘法的重载方式类似,使用两个运算符 '*=' 和 '*'。
Matrix *operator*=(const Matrix &other)
|
下面,我们将矩阵乘以一个数字:
Matrix *operator*=(const double v)
|
为了比较两个矩阵,我们使用运算符 '==' 和 '!=':
bool operator==(const Matrix &other) const
|
为了方便调试,我们将矩阵数组的输出写入日志。
void print() const
|
除了上述重载之外,Matrix 类还提供了运算符 [] 的重载:此运算符可返回嵌套类 MatrixRow 的对象,即具有给定编号的行。
MatrixRow operator[](int r)
|
MatrixRow 类本身通过重载相同的运算符 [] 提供了对矩阵元素的更深层访问(也就是说,对于矩阵,可以自然地指定两个索引 m[i][j])。
class MatrixRow
|
对 int 类型的参数,[] 运算符会返回 MatrixElement 类的对象,您可以通过该对象在数组中写入特定元素。要读取元素,需要将 [] 运算符与 uint 类型参数一起使用。这看起来像是一个技巧,但实际上是语言方面的局限性:重载的参数类型必须不同。作为读取元素的替代方法,MatrixElement 类提供了运算符 '~' 的重载。
使用矩阵时,通常需要一个单位矩阵,因此我们来为其创建一个派生类:
class MatrixIdentity : public Matrix
|
现在,我们开始实际使用矩阵表达式。
void OnStart()
|
这里,我们分别创建了两个矩阵:3*2 维和 2*3 维,然后用数组中的值填充这两个矩阵,并使用两个索引 [][] 的语法编辑选定元素。最后,我们计算了表达式 m * n + p,其中所有操作数都是矩阵。下面一行以方法调用的形式显示了相同的表达式。我们得到了相同的结果。
与 C++ 不同,MQL5 不支持全局级别的运算符重载。在 MQL5 中,运算符只能在类或结构体的上下文中重载,也就是说,通过类或结构体的方法进行重载。另外,MQL5 不支持重载类型转换运算符 new 和 delete。