C++拷贝构造函数详解

有三种情况,会以一个object的内容作为另一个class object的初值:

  1. 对一个object做明确的初始化操作,像这样:

    class X{...};
    X x;
    //明确地以一个object的内容作为另一个class object的初值
    X xx = x;
    
  2. 当object 被当作参数交给某个函数时,例如:

    extern void foo(X x);
    
    void test(){
     X xx;
     foo(xx);    //以xx作为foo()第一个参数的初值(不明显的初始化操作)
    }
    
  3. 当函数传回一个class object时,例如:

    X foo_bar(){
     X xx;
     return xx;
    }
    

Default Memberwise Initialization

如果函数并没有提供一个explicit copy constructor,那么其拷贝同类型对象的操作由default memberwise initialization完成,其执行策略为:对每一个内建或派生的data member的值,从某一个object拷贝到另一个object。不过它不会拷贝其中的member class object,而是实施递归式的memberwise initialization(对每一个对象依次执行default memberwise initialization)。

Default constructors 和 copy constructors在必要的时候才由编译器产生出来。

Bitwise Copy Semantics(位逐次拷贝)

看看下面的程序段:

#include"Word.h"
Word noun("book");
void foo()
{
    Word verb = noun; //...
}

我们不可能预测这个初始化操作的程序行为。如果class Word的设计者定义了一个copy constructor,verb的初始化操作会调用它。但如果该class 没有定义explicit copy constructor,那么是否会有一个编译器合成的实体被调用呢?这就得视该class 是否展现"bitwise copy semantics”而定。

举个例子,已知下面的class Word 声明:

//以下声明展现了bitwise copy semantics
class Word{
public:
    Word(const char*);
    ~Word(){ delete []str;}     //..
private:
    int cnt;
    char *str;
};

这种情况下并不需要合成出一个default copy constructor,因为上述声明展现了“default copy semantics”。

再如下面的例子:

//以下声明并未展现出bitwise copy semantics
class Word{
public:
    Word(const String&);
    ~Word(){ delete []str;}     //..
private:
    int cnt;
    String str;
};

其中String声明了一个显示的拷贝构造函数

class String{
    public:
        String(const char*);
        ~String();
        //...
};

在这个情况下,编译器必须合成出一个copy constructor 以便调用member class String object的 copy constructor:

什么时候一个class不展现出“bitwise copy semantics”呢?有四种情况:

  1. 当class内含一个member object而后者的class声明有一个copy constructor时(不论是被class 设计者明确地声明,就像前面的String那样;或是被编译器合成,像class Word那样)。
  2. 当class继承自一个base class而后者存在有一个copy constructor时(再次强调,不论是被明确声明或是被合成而得)。
  3. 当class声明了一个或多个virtual functions时。
  4. 当class派生自一个继承串链,其中有一个或多个virtual base classes时。

前两种情况中,编译器必须将member或base class的“copy constructors调用操作”安插到被合成的copy constructor中。

虚函数指针

编译时期有两个程序的扩张操作:

■ 增加一个virtual function table(vtbl),内含每一个有作用的 virtual function的地址。
■ 将一个指向virtual function table的指针(vptr),安插在每一个class object内。

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

以一个派生类对象作为其父类对象的初始值,编译器需要”判断“

判断如何去拷贝构造

程序转化语义

下面的程序:

#include 

X foo(){
    X xx;
    // ...
    return xx;
}

可能会做出以下假设:

  1. 每次foo0被调用,就传回xx的值。
  2. 如果class X定义了一个copy constructor,那么当foo0被调用时,保证该copy constructor 也会被调用。

第一个假设的真实性,必须视class X如何定义而定。第二个假设的真实性,虽然也有部分必须视class X如何定义而定,但最主要还是视你的C++编译器所提供的进取性优化程度(degree of aggressive optimization)而定。你甚至可以假设在一个高品质的C++编译器中,上述两点对于class X的nontrivial definitions都不正确。

显式初始化操作(Explicit Initialization)

下面的例子是三种显示的拷贝构造操作:

void test() {
  X x0;
  X x1(x0);     // 第一种显示拷贝构造
  X x2 = x0;    // 第二种显示拷贝构造
  X x3 = X(x0); // 第三种显示拷贝构造
}

上面的程序转化可能会有两个阶段:

  1. 重写每一个定义,其中的初始化操作会被剥除。
  2. class的copy constructor 调用操作会被安插进去。

可能会变成下面这样

void test_Cpp()
{
    // ... x0的构造函数
    // 三个声明
    X x1,x2,x3;
    // 三个定义
    x1.X::X(x0);
    x2.X::X(x0);
    x3.X::X(x0);
}

参数的初始化(Argument Initialization)

C++标准说,把一个class object 当做参数传给一个函数,相当于一下形式的初始化操作:

(定义一个类X,作为test函数的参数)

X xx;
test(xx);

其中test函数的声明如下:

void test(X x0);

上面的参数xx这个变量作为参数传到函数test中,会产生一个临时对象,并且会调用拷贝构造函数将这个临时对象初始化,经过这些步骤这个参数才真正传入函数中进行使用。

所以上面的代码可能会转换为:

X __temp;   // 临时对象产生
__temp.X::X(xx);    //编译器对拷贝构造函数的调用
test(__temp);       //重新改写函数的调用操作

然而上面的做法只完成了一般,因为函数xx的声明还是一个传值的参数,test的声明也也须被转化,形式参数必须从原先的一个class X object改变为一个class X reference,像这样:

void test(X& x0);

返回值的初始化(Return Value Initialization)

例如下面的例子:

X test()
{
    X xx;
    return xx;
}

编译器可能会做如下NRV优化:

X __result;
void test(X& __result)
{
    __result.X::X();
    return;
}

当类显示Bitwise的情况下,应该不去定义一个拷贝构造函数,使用编译器合成就非常高效和安全!

你可能感兴趣的:(C++拷贝构造函数详解)