构造函数:默认、参数化和拷贝
我们已经在结构体一章中遇到过构造函数(参见 构造函数和析构函数一节)。对于类来说,它们的工作方式大致相同。我们回到重点,考虑更多特性。
构造函数是与类同名的方法,其类型为 void,即不返回值。通常,在构造函数名称前省略 void 关键字。一个类可以有多个构造函数:它们必须在参数数量或类型上有所不同。创建新对象时,程序会调用构造函数,以便为字段设置初始值。
要创建对象,我们采用的方法是在代码中描述相应类的变量。将对该字符串调用构造函数。这将自动进行。
根据参数是否存在以及类型,构造函数分为:
- 默认构造函数:无参数;
- 拷贝构造函数:只有一个参数,类型是对同一类中对象的引用;
- 参数化构造函数:具有任意参数集,除了上述用于拷贝的单一引用。
默认构造函数
最简单的构造函数,没有参数,称为默认构造函数。与 C++ 不同,MQL5 不会将“具有参数且所有参数均为默认值的构造函数”视为默认构造函数(也就是说,所有参数都是可选的,请参见 可选参数一节)。
我们为 Shape 类定义一个默认构造函数。
class Shape
|
当然,这应该在类的公共段内定义。
有时构造函数会被故意设置为受保护或私有,这是为了控制对象的创建方式(例如,通过工厂方法)。但在本例中,我们考虑的是类组合的标准版本。
要为对象变量设置初始值,我们可以使用通常的赋值语句:
public:
|
不过,构造函数语法提供了另一种选择。它被称为初始化列表,写在函数头之后,用冒号隔开。列表本身是以逗号分隔的字段名序列,每个字段名右侧是所需的初始值(用圆括号括起)。
例如,Shape 构造函数的代码如下:
public:
|
这种语法比在构造函数体中赋值变量更受推崇,原因如下:
首先,函数体中的赋值是在创建相应变量后进行的。根据变量的类型,这可能意味着首先调用默认构造函数,然后重写新值(这意味着有额外开销)。如果是初始化列表,则会立即用所需值创建变量。在没有初始化列表的情况下,编译器很可能会优化赋值,但在一般情况下,这并不能保证。
其次,某些类字段可以用 const 修饰符声明。然后,它们只能在初始化列表中设置。
第三,用户定义类型的字段变量可能没有默认构造函数(也就是说,其类中所有可用的构造函数都有参数)。这意味着在创建变量时,需要为其传递实际参数,而初始化列表允许您这样做:自变量值在圆括号内指定,就像在显式构造函数调用中一样。初始化列表可以在构造函数定义中使用,但不能在其他方法中使用。
参数化构造函数
根据定义,参数化构造函数有多个参数(至少一个)。
举例来说,假设为坐标 x 和 y 描述了一个具有参数化构造函数的特殊结构:
struct Pair
|
这样,我们就可以使用新类型 Pair 的 coordinates 字段来代替 Shape 类中的两个整数字段 x 和 y。这种对象结构称为“包含”或“组合聚合”。Pair 对象是 Shape 对象的必要组成部分。坐标对与“宿主”对象一起自动创建和销毁。
由于 Pair 没有无参数构造函数,因此必须在 Shape 构造函数的初始化列表中指定 coordinates 字段,并指定两个参数 (int, int):
class Shape
|
如果没有初始化列表,就无法创建此类自动对象。
鉴于对象中坐标存储方式的改变,我们需要更新 toString 方法:
string toString() const
|
但这还不是最终版本:我们很快还会做更多修改。
回顾一下,自动变量在 声明/定义指令 一节中介绍过。它们之所以被称为自动变量,是因为自动变量是由编译器自动创建的(分配内存),并在程序执行离开创建变量的上下文(代码块)时自动删除。
对于对象变量,自动创建不仅意味着内存分配,还意味着构造函数调用。自动删除对象时,也会调用其析构函数(参见下面的 析构函数一节)。此外,如果对象是另一个对象的一部分,那么它的生命周期与其“所有者”的生命周期一致,例如,coordinates 字段就是 Shape 对象中 Pair 的实例。
静态(包括全局)对象也由编译器自动管理。
一种替代自动分配的方法是 通过指针进行动态对象创建和操作。
在 继承 一节中,我们将学习类的继承。这种情况下,只有通过初始化列表才能调用基类的参数化构造函数(不同于默认构造函数的情况,编译器不会隐式自动生成带参数的构造函数调用)。
我们为 Shape 类添加另一个构造函数,用于为变量设置特定值。它将只是一个参数化构造函数(您可以创建任意多个构造函数:用于不同的目的,使用不同的参数集)。
Shape(int px, int py, color back) :
|
初始化列表可确保在执行构造函数主体时,所有内部字段(包括嵌套对象,如有)都已创建并初始化。
类成员的初始化顺序与初始化列表无关,而是与它们在类中的声明顺序一致。
如果在类中声明了带参数的构造函数,并且需要允许创建不带自变量的对象,程序员必须明确实现默认构造函数
如果类中没有任何构造函数,编译器会以存根的形式隐式提供一个默认构造函数,负责初始化以下类型的字段:字符串、动态数组和具有默认构造函数的自动对象。如果没有此类字段,则隐式默认构造函数不会执行任何操作。其他类型的字段不受隐式构造函数的影响,因此它们将包含随机“垃圾”。为了避免这种情况,程序员必须明确声明构造函数并设置初始值。
拷贝构造函数
使用拷贝构造函数,您在创建对象时可以基于另一个对象,后者是通过引用而传递的,并作为唯一参数。
例如,对于 Shape 类,拷贝构造函数可能如下所示:
class Shape
|
请注意,由于权限是在类层面上生效,所以当前对象可以访问同一类的另一个对象的受保护和私有成员。换句话说,当给定一个引用(或 指针)时,同一个类的两个对象可以互相访问彼此的数据。
如果有这样一个构造函数,就可以使用两种语法类型之一来创建对象:
void OnStart()
|
有必要区分创建对象期间初始化与赋值之间的区别。
即使没有拷贝构造函数,但有默认构造函数,也可以使用第二种方法(标有“syntax 2”注释)。在这种情况下,编译器生成的代码效率较低:首先,编译器会使用默认构造函数创建一个接收变量的空实例(本例中为 s3),然后逐元素复制样本的字段(本例中为 s)。事实上,这种情况与 s4 变量的情况相同,该变量的定义和赋值是通过单独的语句完成的。
如果没有拷贝构造函数,那么尝试使用第一种语法将出现错误“不允许参数转换”,因为编译器会尝试使用某个其他具有不同参数集的构造函数。
切记,如果类包含带 const 修饰符的字段,则禁止为此类对象赋值,原因显而易见:常量字段不能更改,只能在创建对象时设置一次。因此,拷贝构造函数是复制对象的唯一方式。
特别是在下面几个章节中,我们将完成 Shape1.mq5 示例,下面的字段将出现在 Shape 类中(具有说明字符串 type)。然后,赋值运算符就会产生错误(尤其是在包含 s4 变量的行中):
attempting to reference deleted function
|
得益于编译器详尽的提示信息,您可以清楚地理解问题的核心和缘由:首先,错误信息中指出的是赋值运算符 ('='),而非拷贝构造函数;其次,它说明了赋值运算符由于 const 修饰符的存在而被隐式地移除了。在这里,我们会遇到一些还没学过的概念,稍后我们将对其进行研究: 类中的运算符重载、 对象类型转换以及将方法标记为 已删除的功能。
在 继承一节中,学习了如何描述派生类之后,我们需要对类层次结构中的拷贝构造函数做一些说明。