在 3.2 中我们定义了对象的生命周期,周期内对象可以占用空间,持有数据。大多数面向对象语言提供了对象生命周期开始时进行特定顺序初始化的机制。当使用 subroutine 方式写时,这种机制被称为构造器。尽管这个名字可能有其他方面的暗示,但是构造器不分配空间;只初始化已经分配好的空间。有些语言提供了类似的析构机制,在对象生命周期结束自动调用。几个重要问题出现了:
- 选择一个构造器:面向对象语言可以有零个,一个,多个不同的构造器。后面的情况,不同的构造器可能会有不同的名字,或者有必要根据它们参数的数量和类型进行区分。
- 引用和值:如果变量是引用,每个对象必须被显式创建,这就很容易确保适当的构造器被调用。如果变量是值,对象创建可能是隐式的。后面这种情情况,语言必须允许对象未初始化,或者必须提供一种方法为每种对象选择合适的构造器。
- 执行顺序:C++的派生类,编译器必须保证基类的构造器先被执行。此外,如果一个类有一些其他类的成员,成员的构造器也必须先被调用。这些规则是语法和语义复杂性的来源之一:组合多个构造器,可变对象,多重继承之后,会出现复杂的构造器调用顺序,重载分析。其他语言会简单点。
- 垃圾收集:大多数面向对象语言提供了构造机制,析构相对少很多。主要目的是减轻 C++ 中手动垃圾回收的负担。如果语言实现中包括垃圾收集,析构是不需要的。
本章剩余部分,详细分析这几个方面的细节。
10.3.1 选择一个构造器
Smalltalk,Eiffel,C++,Java 和 C# 允许开发者指定超过一个构造器。在 C++,Java,C#中,构造器就像可以重载的方法:必须通过参数数量和类型区分。在 Smalltalk 和 Eiffel 中,不同的构造器可以有不同的名字,创建对象必须是显式的。
10.3.2 引用和值
【译者注:引用模型和值模型,译者理解就是堆变量和栈变量】
很多面向对象语言,包括 Simula, Smalltalk, Python, Ruby, Java 使用变量关联对象的编程模型。一些语言,包括 C++ 和 Ada 允许变量有值(对象(。Eiffel 默认使用引用模型,但是允许开发者指定类需要被展开,意味着变量有值。类似的,C# 和 Swift 使用 struct 定义有值的类型,class 定义引用的类型。
对于变量的引用模型,每个对象显式创建,很容易确保调用了正确的构造函数。对于变量的值模型,对象可以隐式创建。在 Ada 中,默认不提供构造器的自动调用,对象开始都是未初始化的,就可能在有值前使用值。在 C++中,编译器确保每个对象的构造器的调用,但是规则有时候很复杂。
如果 C++ 类类型 foo 的变量未初始化声明,编译器会调用零参数构造器(如果没有这样的构造器,但是有其他构造器,编译器会报错)。
如果开发者想要不同的构造器,必须显式声明构造器的参数。拷贝构造器:在声明一个变量时,即使使用 = ,也是调用拷贝构造器。赋值运算符重载被调用,只发生在已经存在的变量重新赋值。
【译者总结】:RVO(return value optimization) 与构造器。
10.3.3 执行顺序
正如我们看到的,C++ 坚持对象使用之前是被初始化的。此外,如果类是派生的,C++ 先调用基类的构造器,再调用派生类自己的。
foo::foo(foo_params): bar(bar_args) {}
Java 的规则也一样。语法更简单一点,super(args)
。
10.3.4 垃圾收集
当 C++ 对象被销毁,派生类的析构首先被调用,然后是基类的,与构造器的顺序相反。析构的最常见作用是自动释放申请的存储。
在现代 C++ 代码中,存储管理通常使用智能指针。具体实现就是,在析构器中判断,指针指向的对象是否还存在,不存在就回收指针指向的对象。
在自动收集的语言中,不需要析构器。事实上,