有3种情况,可以将一个object的内容作为另外个object的初值:
1. 明确的拷贝:X c_x2; X c_x = c_x2;
2. 函数参数:void foo(X xx);
3. 函数返回值:X foo() { X xx; return xx;};
假如类的设计者定义了一个拷贝构造,比如:
X::X( const X &x);
Y::Y( const Y &y, int = 0);
那么在大部分情况下,当一个类对象以另一个同类实体作为初值时,上述的构造会被调用。
位逐次拷贝:
1: //...
2: Word noun("book");
3:
4: void foo()
5: {
6: Word verb = noun;
7: }
很明显,verb是根据noun来进行初始化。但如果没见过Word的声明,无法确定是否显式的声明了拷贝构造?如果没有声明,那么是否编译器会自动合成实体调用呢?
1: //声明方式1:语意拷贝
2: class Word
3: {
4: public:
5: Word(const char*);
6: ~Word(){delete []str;};
7: private:
8: int cnt;
9: char *str;
10: }
在这样的拷贝声明下,当Word verb = noun时,verb和noun的str指针都指向相同的字符串“book”。因为Word(const char*)时,拷贝的并不是一个字符串内容,而是一个指向字符串“book”的指针(该指针指向字符串的地址)。这是灾难性的,一旦其中一个对象被销毁,另外个对象str指针便指向了一个危险的地址。
如果采用另外种声明方式:
1: class Word
2: {
3: public:
4: Word(const Word &word);
5: ~Word();
6: private:
7: int cnt;
8: char *str;
9: }
回忆下2个扩张操作:
1. 增加一个“虚函数表”(vtbl),内含有每一个有作用的虚函数地址;
2. 将一个指向虚函数表的指针(vptr),安插在每一个类对象内;
所以这里的可怕点在于,vptr是否被正确设置好初值(如果没有指向正确的vtbl,那么后果会很严重)。
1 class ZooAnimal 2 { 3 public: 4 ZooAnimal(); 5 virtual ~ZooAnimal(); 6 7 virtual void animate(); 8 virtual void draw(); 9 private: 10 // 各种数据 11 } 12 13 class Bear : public ZooAnimal 14 { 15 public: 16 Bear(); 17 void animate(); 18 void draw(); 19 virtual dance(); 20 private: 21 // 各种数据 22 }
然后这样:
Bear weini;
Bear huangseweini = weini;
这里的Bear类对象以另外一个Bear对象作为初值,都是依靠位拷贝来完成。
首先weini对象,先由Bear默认构造来完成初始化,这里的vptr(weini)被设定指向Bear类对象的虚表(这里由编译器安插代码来完成)。所以,把weini的vptr指针拷贝给huangseweini的vptr是安全的。
当一个基类对象以其子类作为对象内容进行初始化时,势必发生切割行为。此时vptr的复制操作也必须保证安全。