默认构造函数的构造操作
一个类的默认构造函数,在什么时候会被编译器构造出来?
对于编译器来说,只有在“需要”的时候才会被编译器构造出来。注意,这里的“需要”指的是编译器需要,而不是程序需要。对于程序来说,一些初始化操作是必要的,比如将成员变量初始化为零等。但是编译器是不需要承担这个任务的。对于编译器来说,它只需要完成以下的工作:
- 构造类中所有的成员变量。
- 构造继承的基类。
- 如果有虚函数的话,生成虚函数表,安插vptr.
- 如果虚继承自一个基类,那么必须在执行期准备好基类在每一个派生类对象的位置(如安插一个指针指向基类).
在构造类中的成员变量时,是按照定义的顺序进行的,并且在基类的构造函数被调用之后才会执行。
拷贝构造函数的构造操作
默认的拷贝构造函数,执行时相当于把一个对象中各个成员拷贝到另一个对象中(递归地执行成员变量的拷贝构造函数)。在有的类中,会呈现出逐位拷贝语义,即调用拷贝构造函数的效果和直接复制内存无异。
在以下的四种情况中,不显示逐位拷贝语义:
- 类中有一个成员变量具有复制构造函数时;
- 类中有一个基类具有复制构造函数时;
- 类中有虚函数时;
- 类有虚继承时;
接下类详细说明为何后两种情况不会呈现逐位拷贝语义。对于第三种情况,假设一个基类对象Base被赋值为一个派生类Derived对象,这两个对象的虚函数不完全相同,那么必须调整Base的vptr为基类的vptr.如果使用了逐位拷贝,那么Base的vptr就会指向Derived的vtable,这是严重的错误。对于第四种情况,假设类B虚拟继承类A,类C继承类B,如果用一个类C的对象拷贝给一个类B的对象,在类B的对象中,为了保证类A能被顺利存取,必须要设置指向基类的指针/偏移量。在3.4节,可以发现类A被放置在整个对象的最后面。所以偏移和指针肯定不是完全一致的。诸位拷贝会导致严重错误。
初始化的语意
在一个类对象被定义时,发生了什么?
首先,重写每一个定义,其中的初始化操作将会被剥除;
然后,对定义的变量执行构造函数。
在一个类被作为参数被传进函数时,发生了什么?
一部分编译器将会产生一个和实参完全一致的临时对象(通过拷贝构造),并将函数的参数改为引用传递,传入该临时参数的引用。
在函数返回一个类对象时,发生了什么?
最初的实现进行了一个双阶段转化:1.将返回值的引用作为参数传递进去;2.将产生的结果复制到返回值中。
这种方式可以优化为以下的形式:返回值的引用仍然传递进函数,但是所有的操作直接对返回值进行,无需再通过复制构造函数将运算结果复制到返回值。
在成员只有基本类型的时候,不需要复制构造函数。
初始化列表
在初始化的时候,有必要使用初始化列表。在以下的情况,必须使用初始化列表:
- 初始化一个常量成员时
- 初始化一个引用成员时
- 构造需要参数来初始化的基类时
- 构造需要参数来初始化的成员时
记住,成员初始化顺序与成员声明顺序一致,而不是初始化列表的顺序!同时切勿在初始化列表里用成员变量来初始化别的成员变量!