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; // 注釋 1Person 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各不相同。