2. Semantics of Constructor
第二章讨论Constructor构造函数的工作原理:主要是默认构造函数和复制构造函数何时被编译器生成,给程序效率带来的意义。
其实觉得semantics这个词用得过于抽象了,而且贯穿全书,在书里翻译为“语意学”,应该指的是编译器对C++语法特性的语义分析吧。这一章读起来已经有点绕了,译者侯捷已经尽力复原作者的意思,但读起来并不连贯,有些地方读不通。
2.1 Default Constructor 默认构造函数
编译器什么时候产生一个默认构造函数呢?(对于一个类,如果编译器不需要,用户也没有显式声明,则这个隐式声明的默认构造函数是trivial没用的,编译器不会去合成)
相对应地,编译器需要产生默认构造函数时,该构造函数才是nontrivial的,这个构造函数才会被编译器合成出来。以下四种情况的(隐式)构造函数是nontrivial:
- A. 当类没有构造函数,但有一个其他有默认构造函数的类的对象作为成员(即类包含),这个类的默认构造函数是nontrivial的。合成操作在构造函数需要被调用时才发生。如果类含有多个类对象成员,那么类的每一个构造函数必须调用每一个类成员的默认构造函数。编译器会扩张已有的构造函数,插入一部分代码,是的用户代码被执行之前先调用必要的默认构造函数。
class A {
public:
B b;
C c;
D d;
// ...
private:
int m;
}
如果类A没有定义默认构造函数,就会有一个nontrivial的构造函数被合成,依次调用B、C、D的默认构造函数。如果A定义了默认构造函数(初始化m),编译器会在初始化m之前插入调用B、C、D构造函数的代码。
B. 当类没有构造函数,但继承了一个有默认构造函数的基类时(即类继承),这个派生类的默认构造函数是nontrivial的。它将调用上一层基类的默认构造函数。如果提供了多个构造函数,但没有默认构造函数,编译器会扩张现有的每一个构造函数,加入调用默认构造函数所需要的代码。
C. 当类继承了一个虚函数,编译器生成默认构造函数来设定vptr的初值,正确指向类相关的vtbl,vbtl也是编译器产生,含有类的虚函数地址。
D. 当类派生自一个继承串链,其中有一个或多个虚基类(抽象类)时,一个原则:必须使虚基类在其每一个派生类对象中的位置,能够在执行器准备妥当,
这四种情况以外,又没有声明任何构造函数的类,则认为拥有隐式trivial默认构造函数,实际上不会生成。
在编译器生成的默认构造函数中,只有基类子对象和类对象成员会被初始化,其他的非静态数据成员(整型、指针、数组)都不会被初始化。这些操作对于编译器而言非必要,需要程序员去设置初值。
C++新手两大误区:
- 任何类,如果没有定义默认构造函数,就会被合成出来。
- 编译器合成出来的默认构造函数会显式设定类内每一个数据成员的默认值。
2.2 Copy Constructor 复制构造函数
有三种情况会以一个对象的内容作为另一个对象的初值(即拷贝对象来赋值):
- 直接拷贝赋值来初始化一个对象
- 对象作为函数参数传入
- 对象作为函数返回值
如果一个类没有提供显式的复制构造函数?
当对象是以相同类的另一个对象作为初值,内部会以default memberwise intialization完成。逐个成员来复制,遇到其他类的对象成员,则在该类上递归试试memberwise intialization。
和默认构造函数一样,如果类没有声明复制构造函数,就会有隐式声明或定义出现。相似地,C++ Standard把复制构造函数区分为trivial和nontrivial两种,只有nontrivial的实例才会被合成。和默认构造函数一样,复制构造函数只有在必要的时候,编译器才会产生(所以“如果类没有定义复制构造函数,编译器自动为它产生一个”是错误的)。
必要的时候是指当类不展现、不具有bitwise copy semantics时,该类就不具有bitwise copy semantics,编译器就会产生复制构造函数;反之,当类具有bitwise copy semantics时,编译器就不会产生复制构造函数了,编译器为其产生bitwise copies。
所谓的类具有bitwise copy semantics,应该是指该类的成员都是可以逐个bit复制的(如整型、指针等),而没有包含其他类的对象作为成员,不需要通过递归来完成复制。(个人理解)
有以下4种情况,类不具有bitwise copy semantics,即复制构造函数是nontrivial的(和默认构造函数类似):
- A. 当类内含有一个对象成员,其的类声明有一个复制构造函数(设计者自定义声明或者编译器合成)时
- B. 当类继承自一个基类,该基类存在一个复制构造函数(设计者自定义声明或者编译器合成)时,前两种情况,编译器必须将成员或者基类的复制构造函数安插到合成的复制构造函数之中。
- C. 当类声明了一个或多个虚函数时,当编译器导入一个vptr到类之中,该类不再具有bitwise copy semantics。
- D. 当类派生自一个继承串链,其中有一个或多个虚基类时,当一个类如果以另一个带有虚基类子对象的类作为初值,那么bitwise copy semantics失效。