2.1 默认构造函数的构造操作
只有当编译器需要一个默认构造函数时,才会合成默认构造函数。
如果 class A 包含一个或一个以上的 member class objects ,那么 class A 的每一个构造函数都必须调用每一个 member classes 的构造函数。
如果在 class A 的默认构造函数中只调用了一个 member class 的构造函数,那么编译器将会自动扩张,调用所有的构造函数,并且是按照声明顺序。
带有一个虚函数的Class
class Widget{
public:
virtual void flip() = 0;
};
void flip(const Widget& widget){widget.flip();}
//假设Bell和Whistle都派生自Widget
void foo(){
Bell b;
Whistle w;
flip(b);
flip(w);
}
下面两个扩张行为会在编译期间发生:
- 一个虚函数表(vtbl)被编译器产生,内放 class 的虚函数地址。
- 在每一个class object中,一个额外的 vptr 会合成,指向相关的 vtbl。
其中的widget.flip()
的 virtual invocation 会被重新改写,来使用 widget 的 vptr 和 vtbl 中的flip()
条目:
(*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 派生自一个继承串链,其中有一个或多个虚基类。
阿秀的笔记:
e.g.
class String{
public:
//... 没有显式拷贝构造函数
private:
char* str;
int len;
};
此处的 String 就展现出位逐次拷贝,当需要拷贝构造的时候,编译器不合成拷贝构造的实例,而是直接为它的每个成员逐个拷贝。
class String{
public:
String(const String&);
private:
char* str;
int len;
};
class Word{
public:
//...没有显式的拷贝构造函数
private:
int _occurs;
String _word; //String object成为了Word的一个member!
};
这里有一个 String 类实例作为 Word 的成员,而且 String 声明了拷贝构造函数,所以 Word 不展现位逐次拷贝,而且编译器需要在 Word 初始化时调用 String 的构造函数,所以编译器必须为Word合成一个拷贝构造函数。
重新设定 Virtual Table 的指针
如果一个编译器对每一个新产生的类对象的 vptr 不能正确地赋初值,那么后果很严重。因此,当编译器导入一个 vptr 到 class 中时,该 class 就不再展现位逐次拷贝了。所以编译器需要合成拷贝构造,将 vptr 合适地初始化。
e.g.
class ZooAnimal{
public:
ZooAnimal();
virtual ~ZooAnimal();
virtual void animate();
virtual void draw();
private:
};
class Bear:public ZooAnimal{
public:
Bear();
void animate();
void draw();
virtual void dance();
private:
};
一个 ZooAnimal 以另一个 ZooAnimal 初始化,或者一个 Bear 以另一个 Bear 初始化,都可以直接靠位逐次拷贝完成。e.g.
Bear yogi;
Bear winnie = yogi;
yogi 会被默认构造初始化,在构造函数中,yogi 的 vptr 被设定指向 Bear class 的 vtbl 。因此,将 yogi 的 vptr 拷贝给 winnie 的 vptr 是安全的。
当一个基类对象以其派生类对象做初始化操作时,其 vptr 复制操作必须保证安全:
ZooAnimal franny = yogi; //会发生切割行为
franny 的 vptr 不可以被设定指向 Bear class 的 vtbl 。所以此时不会展现出位逐次拷贝,需要合成构造函数,来正确设置 vptr 。
即合成的拷贝构造函数会显式设定 object 的 vptr 指向 ZooAnimal class 的 vtbl ,而不是直接将右边的 vptr 拷贝来。
2.3 程序转化语意学
显式初始化操作
必要的程序转化有两个阶段:
- 重写每一个定义,其中的初始化操作被剥离。(在C++中,定义是指占用内存的行为)
- class 的拷贝构造调用操作会被安插进去。
void foo_bar(){
X x1(x0);
X x2 = x0;
X x3 = X(x0);
}
可能会被编译器转化为:
参数的初始化
C++规定,在把一个类对象当做参数传给一个函数(或作为函数返回值)时,相当于以下形式的初始化操作:X xx = arg;
其中,xx
表示形参(或返回值)arg
表示真正的参数值。
返回值的初始化
X bar(){
X xx;
...
return xx;
}
bar()
的返回值从局部对象 xx 中拷贝过来,做了双阶段转化:
- 首先给函数加上一个额外参数,类型是类对象的引用。这个参数用来放置拷贝构造得来的返回值。
- 在 return 指令之前安插一个拷贝构造调用,将欲传回的 object 的内容作为上述引用的初值。
转化结果如下:
在使用者层面做优化
- 定义一个“计算用”的构造。
程序员不再写:
X bar(const T &y){
X xx;
...
return xx;
}
这会要求 xx 被拷贝到编译器产生的__result
中,直接计算 xx 的值:
X bar(const T& y){
return X(y);
}
此函数被转化后形如
:
在编译器层面做初始化
NRV (具名返回值优化)优化:自动执行上述在使用者层面进行优化 。将待赋值的左值直接作为引用传入函数中,进行原地构造,不需要额外空间和额外多一次的拷贝操作。
X x1(1024);
X x2 = X(1024)
X x3 = (X)1024;
x2 和 x3 调用两个构造函数,产生一个临时 object,先初始化该临时 object ,再以它调用拷贝构造函数构造x2和x3。
2.4 成员初始化列表(Member Initialization List)
在下列情况下必须使用初始化列表:
- 有 reference 成员时
- 有 const 成员时
- 当调用一个 base class 构造函数,而它有一组参数
- 当调用一个 member class 的构造函数,而它有一组参数
1 和 2 的原因是:当类中含有一个 const 对象或者 reference 时,必须通过成员初始化列表进行初始化,因为这两种对象要在声明之后立刻初始化,而在构造函数体中做的是赋值操作。另外,只进行赋值操作会带来一次隐含的默认构造函数,因为没有初始化~
当使用 initialization list 时,编译器会一一操作,以参数在 class 内声明的顺序在构造体内安排初始化操作,并且在任何显式用户代码之前。