拷贝构造函数

时间:2022-12-03 15:55:05

拷贝构造函数

有三种情况,会以一个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 objectdefault memberwise initialization发生在这种情况之下:

String noun(“book”);

String verb = noun;

其完成方式就好像个别设定每一个members一样:

//语意相等

verb.str = noun.str;

verb.len = noun.len;

如果一个String object被声明为另一个classmember,像下面这样:

class Word{

public:

    //...

private:

    int _occurs;

    String _word;

};

那么一个Word objectdefault memberwise initialization会拷贝其内建的member _occurs,然后再于String member object _word身上递归实施memberwise initialization

就像缺省构造函数一样,C++ Standard上说,如果类没有声明一个拷贝构造函数,就会有隐含的声明(implicitly declared)或隐含的定义(implicitly defined)出现。和以前一样,C++ Standard把拷贝构造函数区分为trivialnontrivial两种。只有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种情况:

当类内含一个member object,而后者的class声明有一个拷贝构造函数时(不论是被类设计者明确地声明,或是被编译器合成的)

当类继承自一个基类而后者存在有一个拷贝构造函数时(再次强调,不论是被明确声明或是被合成而得)

当类声明了一个或多个虚函数时

当类派生自一个继承串链,其中有一个或多个虚基类时

前两种情况中,编译器必须将memberbase class的“copy constructors调用操作”安插到被合成的拷贝构造函数中。后面两种情况较为复杂一些,接下来将详细地讨论。

 

重新设定虚表的指针

假设类声明了一个或多个虚函数,编译期间会进行程序扩张操作:

增加一个虚函数表,内含每一个有作用的虚函数的地址

将一个指向虚函数表的指针,安插在每一个类对象中

显然,如果编译器对于每一个新产生的类对象的vptr不能成功而正确地设好其初值,将导致错误的结果。因此,当编译器导入一个vptrclass中时,该class就不再展现bitwise semantics了。现在,编译器需要合成出一个copy constructor,以便将vptr适当地初始化,下面是个例子:

首先,定义两个类,ZooAnimalBear

class ZooAnimal{

public:

    ZooAnimal();

    virtual ~ZooAnimal();

    virtual void animate();

    virtual void draw();

    //...

private:

    //ZooAnimalanimate()draw()所需要的数据

}

 

class Bear : public ZooAnimal{

public:

    Bear();

    void animate();       //虽然没有明写virtual,它实际上也是virtual

    void draw();          //虽然没有明写virtual,它实际上也是virtual

    virtual void dance();

//....

private:

    //ZooAnimalanimate()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初始化。而在构造函数中,yogivptr被设定指向Bear classvirtual table。因此,把yogivptr值拷贝给winnievptr是安全的。

 

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

ZooAnimal franny = yogi//这会发生切割(sliced)行为

frannyvptr不可以被设定为指向Bear classvirtual table。合成出来的ZooAnimal copy constructor会明确设定objectvptr指向ZooAnimal classvirtual 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