2.1 默认构造函数的构造操作

只有当编译器需要一个默认构造函数才会合成默认构造函数

如果 class A 包含一个或一个以上的 member class objects ,那么 class A 的每一个构造函数都必须调用每一个 member classes 的构造函数

如果在 class A 的默认构造函数中只调用了一个 member class 的构造函数,那么编译器将会自动扩张,调用所有的构造函数,并且是按照声明顺序


带有一个虚函数的Class
image.png

  1. class Widget{
  2. public:
  3. virtual void flip() = 0;
  4. };
  5. void flip(const Widget& widget){widget.flip();}
  6. //假设Bell和Whistle都派生自Widget
  7. void foo(){
  8. Bell b;
  9. Whistle w;
  10. flip(b);
  11. flip(w);
  12. }

下面两个扩张行为会在编译期间发生:

  • 一个虚函数表(vtbl)被编译器产生,内放 class 的虚函数地址。
  • 在每一个class object中,一个额外的 vptr 会合成,指向相关的 vtbl

其中的widget.flip()的 virtual invocation 会被重新改写,来使用 widget 的 vptr 和 vtbl 中的flip()条目:

  1. (*widget.vptr[1])(&widget);
  • 其中 1 表示 flip() 在虚函数表中的固定索引;
  • &widget代表要交给“被调用的某个flip()实体”的 this 指针

在合成的默认构造函数中,只有 base class subobjects 和 member class objects 会被初始化所有其他的非静态数据成员(如整数、整数指针、整数数组等)都不会被初始化

2.2 拷贝构造函数的构造操作

如果 class 没有声明一个拷贝构造函数,就会有隐式的声明或定义出现。C++标准将拷贝构造区分为 trivial 和 nontrivial 只有 nontrivial 的实例才会被合成于程序中

决定拷贝构造是否是 trivial 的标准在于 class 是否展现出位逐次拷贝(bitwise)。

class不展现出位逐次拷贝的情况:

  • 当 class 内含一个 member object ,而后者的 class 声明有一个拷贝构造函数。
  • 当 class 继承自一个 base class,而后者存在一个拷贝构造函数。(无论是否是合成的)
  • 当 class 声明了一个或多个虚函数时。
  • 当 class 派生自一个继承串链,其中有一个或多个虚基类。

阿秀的笔记:
image.png

e.g.

  1. class String{
  2. public:
  3. //... 没有显式拷贝构造函数
  4. private:
  5. char* str;
  6. int len;
  7. };

此处的 String 就展现出位逐次拷贝,当需要拷贝构造的时候,编译器不合成拷贝构造的实例,而是直接为它的每个成员逐个拷贝

  1. class String{
  2. public:
  3. String(const String&);
  4. private:
  5. char* str;
  6. int len;
  7. };
  8. class Word{
  9. public:
  10. //...没有显式的拷贝构造函数
  11. private:
  12. int _occurs;
  13. String _word; //String object成为了Word的一个member!
  14. };

这里有一个 String 类实例作为 Word 的成员,而且 String 声明了拷贝构造函数,所以 Word 不展现位逐次拷贝,而且编译器需要在 Word 初始化时调用 String 的构造函数,所以编译器必须为Word合成一个拷贝构造函数


重新设定 Virtual Table 的指针

如果一个编译器对每一个新产生的类对象的 vptr 不能正确地赋初值,那么后果很严重。因此,当编译器导入一个 vptr 到 class 中时,该 class 就不再展现位逐次拷贝了。所以编译器需要合成拷贝构造,将 vptr 合适地初始化。

e.g.

  1. class ZooAnimal{
  2. public:
  3. ZooAnimal();
  4. virtual ~ZooAnimal();
  5. virtual void animate();
  6. virtual void draw();
  7. private:
  8. };
  9. class Bear:public ZooAnimal{
  10. public:
  11. Bear();
  12. void animate();
  13. void draw();
  14. virtual void dance();
  15. private:
  16. };

一个 ZooAnimal 以另一个 ZooAnimal 初始化,或者一个 Bear 以另一个 Bear 初始化,都可以直接靠位逐次拷贝完成。e.g.

  1. Bear yogi;
  2. Bear winnie = yogi;

yogi 会被默认构造初始化,在构造函数中,yogi 的 vptr 被设定指向 Bear class 的 vtbl 。因此,将 yogi 的 vptr 拷贝给 winnie 的 vptr 是安全的。
image.png

当一个基类对象以其派生类对象做初始化操作时,其 vptr 复制操作必须保证安全:

  1. ZooAnimal franny = yogi; //会发生切割行为

franny 的 vptr 不可以被设定指向 Bear class 的 vtbl 。所以此时不会展现出位逐次拷贝,需要合成构造函数,来正确设置 vptr 。

image.png
合成的拷贝构造函数会显式设定 object 的 vptr 指向 ZooAnimal class 的 vtbl 而不是直接将右边的 vptr 拷贝来。

2.3 程序转化语意学

显式初始化操作

必要的程序转化有两个阶段:

  1. 重写每一个定义,其中的初始化操作被剥离。(在C++中,定义是指占用内存的行为)
  2. class 的拷贝构造调用操作会被安插进去。
  1. void foo_bar(){
  2. X x1(x0);
  3. X x2 = x0;
  4. X x3 = X(x0);
  5. }

可能会被编译器转化为:
image.png

参数的初始化

C++规定,在把一个类对象当做参数传给一个函数(或作为函数返回值)时,相当于以下形式的初始化操作:X xx = arg; 其中,xx表示形参(或返回值)arg表示真正的参数值。

返回值的初始化

  1. X bar(){
  2. X xx;
  3. ...
  4. return xx;
  5. }

bar()的返回值从局部对象 xx 中拷贝过来,做了双阶段转化:

  1. 首先给函数加上一个额外参数,类型是类对象的引用。这个参数用来放置拷贝构造得来的返回值。
  2. 在 return 指令之前安插一个拷贝构造调用,将欲传回的 object 的内容作为上述引用的初值。

转化结果如下:
image.png

在使用者层面做优化

  • 定义一个“计算用”的构造。

程序员不再写:

  1. X bar(const T &y){
  2. X xx;
  3. ...
  4. return xx;
  5. }

这会要求 xx 被拷贝到编译器产生的__result直接计算 xx 的值:

  1. X bar(const T& y){
  2. return X(y);
  3. }

此函数被转化后形如
image.png

在编译器层面做初始化

NRV (具名返回值优化)优化:自动执行上述在使用者层面进行优化 。将待赋值的左值直接作为引用传入函数中,进行原地构造,不需要额外空间和额外多一次的拷贝操作。

  1. X x1(1024);
  2. X x2 = X(1024)
  3. X x3 = (X)1024;

x2 和 x3 调用两个构造函数,产生一个临时 object,先初始化该临时 object ,再以它调用拷贝构造函数构造x2和x3。

2.4 成员初始化列表(Member Initialization List)

在下列情况下必须使用初始化列表:

  1. 有 reference 成员时
  2. 有 const 成员时
  3. 当调用一个 base class 构造函数,而它有一组参数
  4. 当调用一个 member class 的构造函数,而它有一组参数

1 和 2 的原因是:当类中含有一个 const 对象或者 reference 时必须通过成员初始化列表进行初始化,因为这两种对象要在声明之后立刻初始化,而在构造函数体中做的是赋值操作。另外,只进行赋值操作会带来一次隐含的默认构造函数,因为没有初始化~
当使用 initialization list 时,编译器会一一操作,以参数在 class 内声明的顺序在构造体内安排初始化操作,并且在任何显式用户代码之前