C++对象模型之复制构造函数的构造操作

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ljianhui/article/details/46295913
复制构造函数用于根据一个已有的对象来构造一个新的对象。

1、构造函数何时被调用
有三种情况会以一个对象的内容作为另一个类的对象的初值构造一个对象,分别是:
1)对一个对象做显示的初始化操作时,如
class X { ... };
X x;
X xx = x; // 或 X xx(x);
2)当对象被当作参数传递给某个函数时
3)当函数返回一个类的对象时

2、默认的成员复制初始化
如果class没有提供一个显式的复制构造函数,当class的对象以另一个对象作为初值进行构造时,其内部是以这样的方式完成的:对于基本的类型(如int、数组)的成员变量,会使用位复制从一个对象复制到另一个对象;对于类类型的成员变量,会以递归的方式调用其复制构造函数。

3、复制构造函数何时被编译器合成
当复制构造函数必要时,它会被编译器构造出来。何为必要的时候呢?就是指当class不展现所谓的bitwise copy semantics(逐位复制语义)时。

与默认构造函数一样,若class没有声明一个复制构造函数,就会有一个隐式声明的或隐式定义的复制构造函数出现。复制构造函数分为trivial(没有用的)和nontrivial(有用的)两种。只有nontrivial的复制构造函数才会被编译器合成。判断一个复制构造函数是否为trivial的标准在于class是否展现出bitwise copy semantics。

下面解释什么是bitwise copy semantics。

4、bitwise copy semantics(逐位复制)
在下面的代码片断中:
class Person
{
    public:
        Person(const char *name, int age);
    private:
        char *mName;
        int mAge;
};

int main()
{
    Person p1("p1", "20");
    Person p2(p1);
    return 0;
}

在上面的代码片断中,p2要根据p1来初始化的。类Person没有定义复制构造函数,且根据类Person的定义,它的成员变量全都是基本类型的变量(指针和int),没有类类型的成员变量,没有定义virtual函数,也不是某个类的派生类。这时复制构造操作可以通过逐位复制(也是默认的复制操作)来完成。所以在这种情况 下该类的定义展现了bitwise copy semantics,所以并不会合成一个复制构造函数。

下面讨论在什么情况下,类表现出非bitwise copy semantics。

5、非bitwise copy semantics
下面四种情况下的class不展现出bitwise copy semantics。下面一一列出,并详细说明。

1)当class内含有一个成员对象,而该对象的类声明了复制构造函数时(不论是显式声明的还是被编译合成的)
当Person类的定义如下时:
class Person
{
    public:
        Person(const String &name, int age);
    private:
        String mName;
        int mAge;
};
class String
{
    public:
        String(const char *str)
        String(const String &rhs);
    private:
        char *mStr;
        int mLen;
};

由于Person类没有显式地定义一个复制构造函数,但其内含有一个成员对象(mStr),且该对象所属的类(String)定义了一个复制构造函数,所以此时的Person展现出了非bitwise copy semantics。编译器会为其合成一个复制构造函数,该复制构造函数调用String的复制构造函数来完成成员变量mStr的初始化,并通过逐位复制的方式完成其他的基本类型的成员变量(mAge)的初始化。

2)当class继承自一个基类,而该基类存在一个复制构造函数时(不论是显式声明的还是被编译合成的)
例如下面的代码片断:
class Student : public Person
{
    public:
        Student(const String &name, int age, int no);
    private:
        int mNo;
};

如前所述,类Person因有一个String类型的成员变量而存在一个编译器合成的复制构造函数。而类Student继承于类Person,所以在此情况下,类Student展现了非bitwise copy semantics。编译器会为其合成一个复制构造函数,该复制构造函数调用其基类的复制构造函数完成基类部分的初始化,再初始化其派生类的成员变量。

3)当class声明了一个或多个virtual函数时
当类中声明了一个虚函数后编译器为支持虚函数机制,在编译时会进行如下操作:
1. 增加一个虚函数表(vtbl),内含有每一个有作用的虚函数的地址。
2. 在类的每个对象中安插一个指向该类虚函数表的指针(vptr)。

为了正确地实现虚函数机制,编译器对于每一个新产生的类对象的vptr都要成功而正确地设定其初值。所以编译器要合成一个复制构造函数,用来正确地把vptr初始化。

在类Person和类Student中加入一个virtual函数print,如下:
class Person
{
    public:
        Person(const char *name, int age);
        virtual ~Person();
        virtual void print();
    private:
        const char *mName;
        int mAge;
};
class Student : public Person
{
    public:
        Student(const char *name, int age, int no);
        virtual ~Student();
        virtual void print();
    private:
        int mNo;
};

现在考虑如下的代码:
Student s1("s1", 22, 1001);
Student s2 = s1; // 注释 1
Person p1 = s1; // 注释 2

当一个类的对象以该类的另一个对象为初值进行构造时,由于这两个对象的vptr都应该指向该类的虚函数表,此时把另一个对象的vptr复制给该对象的vptr是安全的。所以在这种情况下,是可以使用bitwise copy semantics完成的。例如,在上述的代码中,注释1对应的就是这种情况。

但是当一个类的对象以其派生类的对象为初值进行构造时,直接复制派生类对象的vptr的值到基类对象的vptr中,却会引起重大的错误。例如,在上述代码中,注释2对应的就是这种情况。在这种情况下,编译器为一个类合成出来的复制构造函数必须显式地设定该类对象的vptr指向该类的虚函数表,而不是直接复制其派生类对象的vptr的值,并根据该类的类型正确地复制初始化对象的成员。

总的来说,编译器合成的复制构造函数,会根据对象的类型正确地设定其对象的vptr指针的指向。

4)当class派生自一个继承串链,其中有一个或多个virtual基类时
virtual基类的存在需要特别处理。一个类的对象以另一个对象为初值进行构造时,而后者拥有一个virtual基类子对象,那么会使bitwise copy semantics失效。每个编译器都会让派生类对象中的virtual基类子对象的位置在执行期间准备妥当(如G++把virtual基类子对象放在派生类对象的末端),而bitwise copy semantics可能会破坏该这个位置,所以编译器必须在它自己合成出来的复制构造函数中做出判断。

例如,在如下代码中:
class Base
{
    public:
        Base(){mBase = 100;}
        virtual ~Base(){}
        virtual void print(){}
        int mBase;
};
class VBase : virtual public Base
{
    public:
        VBase(){mVBase = 101;}
        virtual ~VBase(){}
        virtual void print(){}
        int mVBase;
};
class Derived : public VBase
{
    public:
        Derived(){mDerived = 102;}
        virtual ~Derived(){}
        virtual void print(){}
        int mDerived;
};

考虑如下的代码:
VBase vb1;
VBase vb2 = vb1;

与第3)点时讨论的一样,如果一个类的对象与该类的另一个对象为初值进行构造,那么使用bitwise copy semantics即可完成相关的操作。问题仍然是发生在以一个派生类的对象作为其基类对象的初值进行初始化时。

考虑如下代码:
Derived d;
VBase vb = d;

在这种情况下,为了完全正确地完成vb的初值的设定,编译器必须合成一个复制构造函数,安插一些代码,来完成根据派生类的对象完成其基类对象部分成员变量的初始化,并正确设定的基类的vptr的值。

以g++为例,类Derived的对象的内存分布大概如下:

类VBase的对象的内存分布大概如下:

从类Derived和类VBase的内存结构图可以非常容易地看出使用bitwise copy semantics并不能完成以一个派生类的对象为初值构造一个基类的对象。编译合成的复制构造函数,把类Derived对象d的基类子对象中的成员变量(mVBase)复制到类VBase对象vb相应的成员变量,再把对象d的虚基类子对象中的成员变量(mBase)复制到对象vb相应的成员变量中(即复制初始化图中黄色的部分)。最后,设置对象vb的两个vptr,使其指向正确的位置。

注:类Derived的两个vptr与类VBase的两个vptr互不相等,它们与类Base的vptr也互不相等。

使用如下代码遍历三个以上三个类的对象的代码如下:
注:运行环境:32位Ubuntu 14.04,g++4.8.2
int main()
{
    Derived d;
    VBase vb = d;

    int *p = (int*)&d;
    for (int i = 0; i < sizeof(d) / sizeof(int); ++i)
    {
        cout << *p << endl;
        ++p;
    }

    p = (int*)&vb;
    cout << endl;
    for (int i = 0; i < sizeof(vb) / sizeof(int); ++i)
    {
        cout << *p << endl;
        ++p;
    }

    Base b;
    cout << endl;
    p = (int*)&b;
    for (int i = 0; i < sizeof(b) / sizeof(int); ++i)
    {
        cout << *p << endl;
        ++p;
    }
    return 0;
}

其输出如下图所示:


运行结果可以看出,三个类的所有vptr各不相同。

猜你喜欢

转载自blog.csdn.net/ljianhui/article/details/46295913