【深度探索C++對象模型】第二章 構造函數語意學(中)

第二章 構造函數語意學(The Semantics of Constructors)(中)
—— 本書作者:Stanley B.Lippman
(接上篇)
三、Copy Constructor 的建構操作
    學習目標:
  • 什麼是 拷貝構造函數(Copy Constructor) ?
  • 何時會調用 拷貝構造函數(Copy Constructor)? 
  • 編譯器什麼時候會爲我們合成一個“有用”的拷貝構造函數?
    何時會調用拷貝構造函數(Copy Constructor)?
    當一個 對象 通過另一個對象來初始化時,前者的 拷貝構造函數 會被調用。
    1. 第一種情況: 初始化。
class X
{
// ...
};
X x;    // 定義一個 X 對象
X xx = x;    // xx 用 x 來初始化,但如果是: X xx; xx = x; 這裏調用的就是 operater = 了,原因很明顯,前面纔是初始化,這裏是賦值。
    2. 第二種情況:對象 作爲函數參數。
extern void foo(X x);

void bar()
{
    X xx;
    // 以 對象 xx 作爲 foo(X x) 的參數x的初值。會調用 x 的Copy Constructor,這裏的 x 是一個臨時對象。
    foo(xx);
}
    3. 第三種情況:對象 作爲函數返回值。
X foo_bar()
{
    X xx;
    // ...
    return xx;
}
    拷貝構造函數的表現形式:
class X
{
    X( const X& x);
    // 也可能是多參形式,第二個參數及其後的參數有默認值
    // X( const X& x, int other = 0);
}
    Default Memberwise Initialization
    如果你沒有給你的 class 顯示的定義 拷貝構造函數,那麼在發生需要調用拷貝構造函數的情形時,內部是通過 default Memberwise Initialization 手法完成的。
    Default Memberwise Initialization 看起來像什麼樣子呢?
class A
{
public:
    A(const A& a);
private:
    int a;
    int b;
}
// 編譯器合成的 Copy Constructor 可能像這樣:
A::A(const A& a)
{
    this->a = a.a;
    this->b = a.b;
}
    【注】當你的類裏有指針成員的時候,要格外小心。具體什麼問題,可參考 Effective C++ 裏的條例。
    當我們沒有顯示定義 拷貝構造函數 時,編譯器什麼時候纔會爲我們合成一個?
    當一個 class 沒有顯示定義 copy constructor 的時候,編譯器會在必要的時候爲這個class合成出來。所謂的必要時,是指 class 沒有展現出 Bitwise Copy Semantics(位逐次拷貝) 時。而當 class 展現出 位逐次拷貝 時,並不需要合成一個 Copy Constructor,默認的 Memberwise Initialization 就已足夠(但有風險:指針問題)。
    什麼時候一個 class 不展現出 Bitwise Copy Semantics 呢?有四種情況:
    1. 當 class 內含一個 成員對象,且該成員對象的類聲明有一個 copy constructor 時(不一定是明確聲明的,也可能是由編譯器合成的 copy constructor)。如,你的 class 裏有一個 std::string 成員時(std::string 的copy constructor 不是合成的,是明確聲明的),這時如果你沒有明確聲明一個 copy constructor 編譯器會爲你合成一個。
    2. 當 class 繼承自一個 base class ,而後者存在一個 copy constructor 時(合成的或者聲明的都算)。
    3. 當 class 聲明瞭一個或多個 virtual functions 時。
    4. 當 class 的繼承串鏈中,其中有一個或者多個 virtual base classes 時。
    重新設定 Virtual Table 的指針__vptr
    前面曾學到過,C++對象模型是如何支持 virtual 機制的。是通過在 class 內安插了一個 vptr 指向其相應的 vtbl。因此,如果編譯器不能正確的初始化 vptr,將導致嚴重的後果。所以,當一個 class 有 virtual functions 時,就不在展現出 Bitwise semantics。於是編譯器將會合成一個 copy constructor 來初始化 vptr。下面是列子。
    【注】一個 class 對應一個 vtbl。相同 class 的 objects 的 vtbl 相同,也即 vptr 指向的地址相同。
class Base
{
public:
    Base();
    virtual ~Base(); // 基類的析構函數一定要 virtual,否則會導致嚴重後果。
    
    virtual void draw();
};

class Derive : public Base
{
public:
    Derive();
    void draw(); // 雖沒寫明 virtual,但實際上是 virtual。
};

Derive yogi;
Derive winnie = yogi; // OK! winnie 和 yogi 的 vtbl 地址相同。可以直接拷貝 vptr 的值。
Base franny = yogi; // 注意,franny 的 vptr 和 yogi 的 vptr 指向不同的 vtbl,因此,copy contructor 需要重新設定 franny 的值,防止其指向 Derive 的vtbl.
// 這裏還涉及到以子類初始化基類時的切割問題
    也就是說:合成出來的 Base class 的拷貝構造函數會明確設定其對象的 vptr 指向 Base class 的 vtbl,而不是直接從右手邊的 class object 中的 vptr 地址拷貝過來。
    處理 Virtual Base Class Subobject
    對於虛擬繼承(即:繼承自一個虛基類),編譯器必須維護 派生類中虛基類的位置(要理解這個“位置”的含義,首先你的瞭解繼承後的對象模型是怎樣的)。
    直接按位拷貝可能會破壞這個"位置"。
class Raccoon : public virtual ZooAnimal
{
// ...
}
class RedPanda : public Raccoon
{
// ...
}
    當我們以一個Raccoon對象作爲另一個Raccoon對象的初值時,不會有任何問題,默認的Bitwise semantics就足以搞定。但當我們以Derived class:RedPanda作爲Raccoon的初值時,就需要編譯器合成出一個 拷貝構造函數,以正確的通過 RedPanda(子類) 初始化 Raccoon(父類) 。

【說明】近期項目上線,工作較忙,更新像蝸牛!後續加快節奏!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章