天天看點

C++對象模型之複制構造函數的構造操作

複制構造函數用于根據一個已有的對象來構造一個新的對象。

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的對象的記憶體分布大概如下:

C++對象模型之複制構造函數的構造操作

類VBase的對象的記憶體分布大概如下:

C++對象模型之複制構造函數的構造操作

從類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;
}
           

其輸出如下圖所示:

C++對象模型之複制構造函數的構造操作

從運作結果可以看出,三個類的所有vptr各不相同。

繼續閱讀