构造函数语意学
》》构造函数语意学I—默认构造函数的构造操作《《
》》构造函数语意学II—拷贝构造函数的构造操作《《
》》构造函数语意学III—程序转化语意学《《
拷贝构造函数的构造操作
有三种情况,会以一个object的内容作为另一个class object的初值。
1.直接拷贝
class X{...};
X x;
X xx = x;
2.函数传参
void foo(X x){}
X xx;
foo(xx);
3.函数返回
X foobar()
{
X xx;
...
return xx;
}
假设class设计者显式定义了一个拷贝构造函数那么在大部分情况下,上述的拷贝构造函数会被调用。
默认成员初始化
如果class没有提供一个显式拷贝构造函数时,其内部是以所谓的默认成员初始化手法完成的,也就是把每一个内建的或派生的成员数据(例如一个指针或一个数组)的值,从某个对象拷贝一份到另一个对象身上。不过它并不会拷贝其中的成员类对象,而是以递归的方式施行成员初始化。
ARM所说:
默认构造和拷贝构造在必要的时候才由编译器产生出来。
这个句子中的“必要”意指当 class 不展现位逐次拷贝时。一个对象可用两种方式复制得到,一种是被初始化,另一种是被指定。从概念上而言,这两个操作分别是以拷贝构造和拷贝赋值运算符(=)完成的。
位逐次拷贝
C++标准把拷贝构造区分为平凡和非平凡两种。只有非平凡的实例才会被合成于程序之中。决定一个拷贝构造是否为平凡的标准在于class是否展现出所谓的“位逐次拷贝”。
以下情况并不需要合成出一个默认拷贝构造,因为上述声明展现了“位逐次拷贝”
class World{
public:
World(const char *);
~World(){delete [] str;}
private:
int cnt;
char *str;
}
这种情况下,编译器必须合成出一个拷贝构造,以便调用String的拷贝构造
class World{
public:
World(const string&);
~World(){delete [] str;}
private:
int cnt;
string str; //string含有显式拷贝构造
}
什么时候一个class不展现出“位逐次拷贝”呢?有4种情况:
- 当class内含一个成员对象而后者的class声明有一个拷贝构造时
- 当 class继承自一个基类而后者存在一个拷贝构造时
- 当 class声明了一个或多个虚函数时。
- 当 class派生自一个继承串链,其中有一个或多个虚基类时。
重新设定 Virtual Table的指针
编译期间的两个程序扩张操作:
- 增加一个虚函数表(vtbl),内含每一个有作用的虚函数的地址。
- 一个指向虚函数表的指针(vptr),安插在每一个 class object内。
如果编译器对于每一个新产生的class object 的vptr不能成功而正确地设好其初值,将导致可怕的后果。
class Bear : public ZooAnimal{...};
//ZooAnimal内含有虚函数animal(),draw()
//Bear内含有虚函数animal(),draw(),dance()
Bear yogi;
Bear winner = yogi;
yogi会被默认Bear构造初始化。而在构造函数中,yogi的vptr被设定指向Bear class 的 virtual table (靠编译器安插的代码完成)。因此,把 yogi的vptr值拷贝给winnie的vptr是安全的。
ZooAnimal franny = yogi;//发生切割行为
franny的vptr不可以被设定指向Bearclass的virtual table(但如果yogi的vptr被直接“逐次拷贝”的话,就会导致此结果),否则当上面程序片段中的draw()被调用而franny被传进去时,就会“炸毁”
让我再以上面这张图来补充说明yogi和franny的关系
也就是说,合成出来的ZooAnimal拷贝构造会显式设定object的vptr指向ZooAnimalclass的virtual table,而不是直接从右手边的class object中将其vptr现值拷贝过来。