书中的第二章,主要讲解了 C++中的构造函数。是不是没有构造函数时,编译器都会合成一个默认的?C++的成员变量是不是和 Java 一样都初始化为0?拷贝构造函数做了哪些工作?成员初始化列表到底有没有必要?
默认构造函数
按照 C++ standard 的描述,当没有用户申明的 constructor 时,编译器会默认的生成一个。从概念上可以这么理解,但是合成的构造函数也分 trivial 的和 nontrivial 的。一个 trivial 的构造函数,实际上什么工作也没有做,实际上生没生成,也没有太大的区别。但有一点需要记住,不管是生成的哪种构造,data members 都是不会初始化的,因为这个是程序员的工作,而不是编译器的工作。那为什么全局的、静态的对象,data members 就初始化为0呢?这个工作不是构造函数来做的,而是加载程序之时,data segment 会全部初始化为0。在以下的四种情况下,编译器会为我们生成一个 nontrivial 的构造函数,它完成一些必要的工作。
Data member带有默认构造函数
当一个 class 中包含了一个或几个带有默认构造函数的data member(不管是用户自己定义的,还是编译器生成 nontrivial 的)。则编译器必须为这个 class 生成一个默认构造函数,这个构造函数会按照 data member 的声明顺序,一次调用其默认构造函数。不仅如此,这个 class 所有的 constructors 都会调用这些 data member 的构造函数,这个过程发生在显示的用户代码之前。
Base Class 带有默认构造函数
当一个 class 的基类带有默认构造函数,编译器也会为它合成一个默认构造函数,在用户代码之前调用其基类的默认构造函数。当有多个 base class 都带有默认构造函数时,会按照继承列表,依次调用其默认构造函数(有时继承列表的顺序会被编译器优化)。这个规则应用于这个类的所有构造函数。
带有虚函数的类
当一个类带有一个或多个虚函数时,编译器会为它合成一个默认构造函数。这个构造函数会给类的 vfptr 赋值,使其指向正确的vftable(虚函数表)。在构造一个对象的时候,分为两步。第一步是分配一块内存(栈或堆),此时内存中的数据是无意义的,包括 vfptr。第二步则是在此内存之上,调用对象的构造函数。编译器虽然不为我们初始化data member,但是 vfptr 却必须由其正确的指定。每一个带有虚函数的类,编译器都会生成一个 vftable,里面包含了正确的虚函数地址。构造函数之中,必须要将vftable 的值填入 vfptr 当中。
带有虚基类的类
当一个类中带有虚基类(直接或简洁),对象中会有一个 vbptr。与 vfptr 同理,每一个类都有一个 vbtable,其中存放了共享虚基类在对象内存中的偏移量(每一个 slot 存放一个虚基类的偏移量)。构造函数需要将类的vbptr 设置为正确的 vbtable 值。
执行顺序
上面四种情况,编译器会为我们生成一个 nontrival 的默认构造函数,每一种情况都会合成一定的操作,这些操作之间带有严格的顺序。在分配了对象的内存之后,其构造函数执行操作的顺序如下:
- 按照继承列表的顺序,依次将对象转为基类对象,调用基类的构造函数。(最顶层的对象,还要负责初始化共享虚基类)
- 设置 vfptr 和 vbptr,这两个操作的顺序似乎没什么影响。
- 调用带有默认构造函数的 data member 的构造函数。
拷贝构造函数
不管有没有用户定义的构造函数,编译器都会为我们合成一个拷贝构造函数(用户自定义了拷贝构造函数除外),以支持用一个对象来初始化另一个对象。拷贝构造函数的参数通常都为其类型的 const 引用(以支持左值和转换)。拷贝构造函数的调用会发生在以下三种情况:
- 用一个对象初始化另一个对象(同一类型)。
- 以对象作为函数参数(不是对象的指针或引用)。
- 以对象作为返回值(不是对象的指针或引用)。
拷贝构造函数也会根据类的定义分为两种类型。如果类的定义展现了“bitwise copy semanitcs”,则逐次拷贝其内容。这种情况下,有可能并没有实际生成并调用拷贝构造函数,而是使用 memcpy 即可。在以下几种情况,则不展现“bitwise copy semanitcs”:
Data member带有拷贝构造函数
按照声明次序,依次拷贝 data members。带有拷贝构造函数的成员,调用其拷贝构造函数;其它成员使用赋值操作。
Base Class 带有拷贝构造函数
按照继承列表,一次调用基类的拷贝构造函数。
带有虚函数的类
因为 C++的对象不展现多态性(只有使用指针或引用,才展现多态性)。所以在拷贝一个对象(有可能是派生类的对象)时,需要将其 vfptr 设置为正确的 vftable 值。
带有虚基类的类
如果类带有虚基类,则拷贝它时(可能来自于派生类的对象),需要按照参数对象来初始化共享虚基类的对象,并且设置正确的 vbptr 的值。
成员初始化列表
初始化成员列表是 C++特有的(相对于 Java)。有可能会觉得它没必要,因为在构造函数里的用户代码一样可以初始化data member,例如:
class X { public: X() { s = "a"; i = 0; } X() : i(0), s("a") {} private: string s; int i; };
这两种初始化方式有区别吗?对于 i 来说是没有的,但是对于 s 就不一样了。按照之前默认构造函数的第一种场景,编译器会在用户代码之前初始化带有默认构造函数的 data member,所以其实编译器已经默默的构造了 s(默认值为"")。用户代码 s = "a" ,实际上是调用 string(const char *)生成一个临时的 string,然后再调用其拷贝构造函数,将临时的 string 赋值(不是拷贝构造)给 s 。成员初始化列表实际上就是告诉编译器,改怎么样去生成用户代码之前的构造操作。这些操作是编译器合成的,在用户代码之前,所以只能通过成员初始化列表来操作。并且有一些情况下,是必须通过成员初始化列表,而不是用户代码来操作的:
- Data member不具备默认构造函数,这时必须在成员初始化列表中,指定参数来调用其构造函数。
- Base class 不具备默认构造函数,此时也必须在成员初始化列表中,指定参数来调用基类的构造函数。
- const成员除了在类定义中赋初值,也只能成员初始化列表中初始化。
- 引用成员也必须在成员初始化列表中初始化。