深入探索C++對象模型之二 --- 構造函數語意學

深入探索C++對象模型之二 — 構造函數語意學

2.1 默認構造函數

當編譯器需要的時候,default Constructor纔會被合成出來,而且只執行編譯器需要的任務。

class Foo {
    public:
        int val;
        Foo *pnext;
}

對於上面的類對象是不會對兩個成員進行初始化的,因爲default Constructor只是做編譯器需要的任務,而初始化成員變量很顯然是程序員的事情。所以這個default Constructor是沒啥用的(我們後面會看到對於這種情況的類,default Constructor都不會被合成出來!)

那麼什麼情況下才會是一個有用的default Constructor,才需要執行編譯器的任務呢?
C++ standard一共敘述了四種情況:

  1. “帶有Default Constructor” 的Member Class Object
    如果一個類還有一個member Object,並且該Object有default Constructor, 那麼便一起就會自動合成一個default Constructor,當然這個合成過程只有在constructor真正被調用的時候纔會發生。合成的default Constructor會在內部調用member Object的默認構造函數初始化該member Object。那麼如果有多個memeber Object則會按照聲明的順序依次調用它們的default Constructor,而如果已經有定義好的constructor函數該怎麼辦呢?編譯器會在這些構造函數中插入必要的代碼來初始化member Object,之後再執行user code。
  2. “帶有Default Constructor” 的Base Class
    如果一個類是派生自“帶有default constructor”的base class,那麼這時候編譯器也會爲該類合成一個default Constructor,它將調用base class 的constructor。同樣的如果程序設計者已經聲明瞭constructor,那麼編譯器就會插入必要的代碼優先初始化base class。當第一種情況和第二種情況並存的時候,優先執行base class的constructor,然後再執行member Object的default Constructor,最後再是user code。
  3. “帶有一個Virtual Function” 的Class
    如果一個類聲明或者繼承一個virtual Function,那麼也編譯器也需要合成default constructor。因爲類存在虛函數,那麼編譯期間編譯器會爲該類產生一個virtual Function table(vtbl),裏面存放virtual Functions的地址。而對於每個類對象,在初始化的時候就需要由編譯器爲它產生一個pointer member(vptr)指向vtbl。這樣子就可以在執行期的時候根據該vptr完成多態的機制。

    另外,對於調用虛函數的地方也會被重寫,都會被改寫成vptr+函數索引的調用方式。諸如:

        (*widget.vptr[1])(&widget)
  4. “帶有一個Virtual Base Class” 的Class
    如果類繼承自一個或者多個virtual Base class,那麼編譯器必須確保使virtual base class在其每一個derived class object中的位置,能夠於執行期準備妥當。如下case:

    class X {public: int i;};
    class A : public virtual X {public : int j;};
    class B : public virtual X {public : double d;};
    class C : public A, public B {public : int k;};
    
    void foo(const A *pa) {pa->i = 1024;}

    上面的代碼中pa必須要等到執行期才能確定下來,因爲pa的類型不固定,可能是A也可能是C,這時候就需要在class object中的每一個virtual base class中安插一個指針,所有經由reference或者pointer存取virtual base class的操作都可以通過相關指針來完成。

    void foo(const A *pa) { pa->_vbcX->i = 1024;}

    上面的_vbcX指針就是編譯器所產生的指針,用來指向virtual base class X。

以上講述的四種情況下編譯器是需要合成一個default Constructor來完成它自己需要完成的任務的,並且合成的default constructor也只是完成上面四種情況需要完成的任務,對於其它一些member data的初始化是不操作的。

總結:

以下兩種觀點是錯誤的:
1. 任何class如果沒有定義default constructor,就會被合成出一個來
2. 編譯器合成出來的default constructor會明確設定“class內每一個data member的默認值”

根據上面的分析以上兩種觀點都是錯誤的。

2.2 拷貝構造函數 Copy Constructor

什麼情況下我們會需要Copy Constructor函數呢?就是以一個object的內容作爲另一個class object的初值
1. 對一個object做明確的初始化操作
2. 當object被當作參數交給某個函數時
3. 當函數傳回一個class object時

假如class設計者明確定義了一個copy constructor,當遇到上面的情況時候就會調用該copy constructor,這可能會導致一個暫時性class object的產生或者程序代碼的優化。那麼如果class沒有提供一個顯式的copy constructor呢?

這裏內部會以所謂的default memberwise initialization手法完成,也就是把每一個內建的或派生的data member的值,從某個object拷貝一份倒另一個object中,不過並不會拷貝其中的member class object,而是以遞歸的方式施行memberwise initialization。

這裏我們要引入兩個概念(位拷貝和值拷貝、淺拷貝和深拷貝)
位拷貝是指將一個對象的內存映像按位原封不動的複製給另一個對象,而值拷貝就是將原對象的值複製一份給新對象,值拷貝會複製對象持有的資源。

上面提到的default memberwise initialization就是位拷貝,它只是簡單的將原先的內容拷貝一份到目標object中,如果類中存在指針之類的資源,那麼使用位拷貝的話兩個object的指針都是指向同一個內存區域,當其中一個object釋放了資源的話另外一個object此時的指針就是野指針,這很明顯是錯誤的,此時就需要深拷貝,就應該編寫operator= 和copy constructor來實現值拷貝。

可以得到:如果類不表現爲bitwise Copy Semantics的時候,此時default memberwise initialization的位拷貝行不通,那麼編譯器就有義務合成一個copy constructor來執行深拷貝。

那麼什麼情況下類纔不表現爲bitwise copy Semantics呢?
1. 當class內含一個member object而後者的class聲明中有一個copy constructor時
2. 當class繼承自一個base class而後者有一個copy constructor時
3. 當class聲明瞭一個或者多個virtual functions時
之前說過對於有virtual function的class,對於每一個class object都會安插一個vptr指針指向虛函數表,那麼編譯器必須初始化這個指針指向正確的地址,因此當安插了vptr的時候,class就不再表現爲bitwise Senmantics了。
4. 當class派生自一個繼承單鏈,其中有一個或多個virtual base classes時
如果是相同class的object之間複製那麼bitwise copy已經是綽綽有餘了,但是問題的關鍵是以derived class object複製給base class object,這時候編譯器就需要判斷後續程序調用base class subobject的時候是否能夠正確的執行。這時候就需要合成copy constructor來設定virtual base class pointer/offset的初值。

總結:
通過上面的分析,可以得到對於那些展現爲bitwise copy Semantics的class,此時default memberwise initialization淺拷貝已經足夠了,但是對於滿足四種情況的不展現出bitwise Semantics的class,此時編譯器必須要合成一個copy constructor來進行必要的工作。

2.3 程序轉化語意學

這裏考慮兩種情況,第一種是object作爲函數的參數傳遞,第二種情況是object作爲函數的返回值

明確的初始化操作

c++
X x0;
void foo_bar() {
X x1(x0);
X x2 = x0;
X x3 = X(x0);
}
那麼程序可能會被轉化成:
void foo_bar() {
X x1;
X x2;
X x3;
x1.X::X(x0);
x2.X::X(x0);
x3.X::X(x0); //編譯器安插了copy constructor的調用
}

參數的初始化

X xx;
foo(xx);

會被改寫成:
X _temp0;
_temp0.X::X(xx);
foo(_temp0);

這裏就會產生一個臨時性的對象_temp0

返回值的初始化

X bar() {
    X xx;
    //...
    return xx;
}
可能被轉化成:
void bar(X &_result) {
    X xx;
    xx.X::X();
    //....
    _result.X::X(xx);
    return;
}

通過在函數中安插一個額外的參數來放置返回值

在使用者層面做優化

在編譯器層面做優化

上面所說的返回值優化的方式成爲Named Return Value(NRV)優化,但是這種優化必須有一個copy constructor。

以下三種初始化操作在語意上是相等的,但是第一個效率最高,因爲第二個會先產生一個臨時性的object,初始化該object之後再複製給目標object

X xx0(1024);
X xx1 = X(1024);
X xx2 = (X)1024;

通過上面的分析,當object作爲函數的參數傳入或者是函數的返回值return的時候,都可能會進行必要的優化,優化都調用到了copy constructor。那麼如果class表現出bitwise Semantics的時候,默認的memberwise initialization已經足夠了,但是如果我們預期該類會有大量的memberwise初始化操作的時候,那麼我們可以提供一個explicit inline copy constructor,這樣子可以讓編譯器開啓NRV優化。

2.4 成員們的初始化隊伍

當我們在constructor中設置class member的初值時,要麼在member initialization list中,就是在函數體之內。一般情況下兩種方式都差不多,只有以下四種情況必須使用member initialization list纔可以:
1. 當初始化一個reference member時
2. 當初始化一個const member時
3. 當調用一個base class的constructor,而它擁有一組參數時
4. 當調用一個member class的constructor,而它擁有一組參數時

編譯器會對member initialization list中的順序重新排序,初始化是按照變量的聲明順序來的,這些都會在user code之前執行。

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