拷贝构造函数
有三种情况,会以一个object的内容作为另一个class object的初值。最明显的一种情况当然就是对一个object做明确得初始化操作,像这样:
class X{…};
X x;
//明确地以一个object的内容作为另一个class object的初值
X xx = x;
另两种情况是当object被当做参数交给某个函数时,以及当函数传回一个class object时。
假设class设计者明确定义了一个拷贝构造函数,像下面这样:
X::X( const X& x );
Y::Y( const Y& y, int = 0 );
那么在大部分情况下,当一个class object以另一个同类实体作为初值时,上述的拷贝构造函数就会被调用。这可能会导致一些临时性对象的产生或程序代码的蜕变。
Default Memberwise Initialization
如果类没有提供一个explicit copy constructor又当如何?当class object以相同类的另外一个object作为初值时,其内部是以所谓的default memberwise initialization手法完成的,也就是把每一个内建的或派生的data member的值,从某一个对象拷贝一份到另外一个对象身上。不过它并不会拷贝其中的member class object,而是以递归的方式施行memberwise initialization。例如,考虑下面这个类声明:
class String{
public:
//...
private:
char *str;
int len;
};
一个String object的default memberwise initialization发生在这种情况之下:
String noun(“book”);
String verb = noun;
其完成方式就好像个别设定每一个members一样:
//语意相等
verb.str = noun.str;
verb.len = noun.len;
如果一个String object被声明为另一个class的member,像下面这样:
class Word{
public:
//...
private:
int _occurs;
String _word;
};
那么一个Word object的default memberwise initialization会拷贝其内建的member _occurs,然后再于String member object _word身上递归实施memberwise initialization。
就像缺省构造函数一样,C++ Standard上说,如果类没有声明一个拷贝构造函数,就会有隐含的声明(implicitly declared)或隐含的定义(implicitly defined)出现。和以前一样,C++ Standard把拷贝构造函数区分为trivial和nontrivial两种。只有nontrivial的实体才会被合成于程序之中。决定一个拷贝构造函数是否为trivial的标准在于class是否展现出所谓的“bitwise copy semantics”。
Bitwise Copy Semantics(位逐次拷贝)
在下面的程序片段中:
#include "word.h"
Word noun("book");
void foo()
{
Word verb = noun;
//...
}
很明显verb是根据noun来初始化。但是在尚未看过Word类的声明之前,我们不可能预测这个初始化操作的程序行为。如果Word的设计者定义了一个拷贝构造函数,verb的初始化操作就会调用它。但是如果该类没有定义explicit copy constructor,那么是否会有一个编译器合成的实体被调用呢?这就得视该class是否展现“bitwise copy semantics”而定。举个例子,已知下面得class Word声明:
class Word{
public:
Word( const char * );
~Word(){ delete[] str; }
//...
private:
int cnt;
char *str;
};
这种情况下,并不需要合成一个缺省拷贝构造函数,因为上述声明展现了“default copy semantics”,而verb的初始化操作也就不需要以一个函数调用收场(当然,该类的定义存在着严重的缺陷)。然而,如果class object是这样声明:
class Word{
public:
Word( const String& );
~Word(){ }
//...
private:
int cnt;
String str;
};
其中String声明了一个explicit copy constructor:
class String{
public:
String( const char * );
String( const String & );
//...
};
在这个情况下,编译器必须合成一个拷贝构造函数以便调用member class string object的拷贝构造函数:
//C++伪码
Inline Word::Word( const Word& wd )
{
str.String::String(wd.str);
cnt = wd.cnt;
}
有一点需要特别注意:在这个被合成出来的拷贝构造函数中,如整数、指针、数组等等的nonclass members也都会被复制,正如我们所期待的一样。
不要Bitwise Copy Semantics!
什么时候一个类不展现“bitwise copy semantics”呢?有以下4种情况:
n 当类内含一个member object,而后者的class声明有一个拷贝构造函数时(不论是被类设计者明确地声明,或是被编译器合成的)
n 当类继承自一个基类而后者存在有一个拷贝构造函数时(再次强调,不论是被明确声明或是被合成而得)
n 当类声明了一个或多个虚函数时
n 当类派生自一个继承串链,其中有一个或多个虚基类时
前两种情况中,编译器必须将member或base class的“copy constructors调用操作”安插到被合成的拷贝构造函数中。后面两种情况较为复杂一些,接下来将详细地讨论。
重新设定虚表的指针
假设类声明了一个或多个虚函数,编译期间会进行程序扩张操作:
n 增加一个虚函数表,内含每一个有作用的虚函数的地址
n 将一个指向虚函数表的指针,安插在每一个类对象中
显然,如果编译器对于每一个新产生的类对象的vptr不能成功而正确地设好其初值,将导致错误的结果。因此,当编译器导入一个vptr到class中时,该class就不再展现bitwise semantics了。现在,编译器需要合成出一个copy constructor,以便将vptr适当地初始化,下面是个例子:
首先,定义两个类,ZooAnimal和Bear:
class ZooAnimal{
public:
ZooAnimal();
virtual ~ZooAnimal();
virtual void animate();
virtual void draw();
//...
private:
//ZooAnimal的animate()和draw()所需要的数据
}
class Bear : public ZooAnimal{
public:
Bear();
void animate(); //虽然没有明写virtual,它实际上也是virtual
void draw(); //虽然没有明写virtual,它实际上也是virtual
virtual void dance();
//....
private:
//ZooAnimal的animate()、draw()和dance()所需要的数据
}
ZooAnimal class object以另一个ZooAnimal class object作为初值,或Bear class object以另一个Bear class object作为初值,都可以直接靠“bitwise copy semantics”完成(除了可能会有的pointer member之外,为了简化,这里不考虑这种情况)。举例:
Bear yogi;
Bear winnie = yogi;
yogi会被default Bear consturctor初始化。而在构造函数中,yogi的vptr被设定指向Bear class的virtual table。因此,把yogi的vptr值拷贝给winnie的vptr是安全的。
当一个基类对象以其派生类的对象做初始化操作时,其vptr复制操作也必须保证安全,例如:
ZooAnimal franny = yogi;//这会发生切割(sliced)行为
franny的vptr不可以被设定为指向Bear class的virtual table。合成出来的ZooAnimal copy constructor会明确设定object的vptr指向ZooAnimal class的virtual table,而不是从右手边的class object中将其vptr现值拷贝过来。
处理Virtual Base Class Subobject
每一个编译器对虚拟继承的支持承诺,都表示必须让派生类对象中的virtual base class subobject位置在执行期就准备妥当。维护“位置的完整性”是编译器的责任。“bitwise copy semantics”可能会破坏这个位置,所以编译器必须在它自己合成出来的拷贝构造函数中做出仲裁。举个例子,在下面的声明中,ZooAnimal成为Raccoon的一个虚拟基类,同时RedPanda public继承自Raccoon:
class Raccoon : public virtual ZooAnimal{
public:
Raccoon(){}
Raccoon( int val ){}
//…
private:
//…
}
class RedPanda : public Raccoon{
public:
RedPanda(){}
RedPanda( int val ){}
//…
private:
//…
}
如果以一个Raccoon object作为另一个Raccoon object的初值,那么“bitwise copy”就绰绰有余了:
Raccoon rocky;
Raccoon little_critter = rocky;
然而如果企图以一个RedPanda object作为little_critter的初值,编译器必须判断“后续当程序员企图存取其ZooAnimal subobject时是否能够正确地执行”:
RedPanda rocky;
Raccoon little_critter = rocky;
在这种情况下,为了完成正确的little_critter初值设定,编译器必须合成一个拷贝构造函数,安插一些代码以设定virtual base class pointer/offset的初值,对每一个members执行必要的memberwise初始化操作,以及执行其它的内存相关操作。
参考资料:
《深度探索C++对象模型》P48~60