《深度探索c++記憶體模型》讀書筆記 (二)

2020-10-18 12:01:00

前言

c++ 編譯器會在人意想不到的地方做一些隱式操作。例如,只含有一個引數的建構函式,會被當做型別轉換運運算元。而關鍵字explict就是為了阻止這一機制。

預設建構函式

c++ 編譯器會在需要的時候自動生成預設建構函式。

帶有預設建構函式的類物件成員

如果一個類沒有任何的建構函式,但是它有一個物件成員,這個物件成員有一個預設建構函式。那麼編譯器將會為這個類生成一個預設建構函式。但是這個生成的時機會是在這個類被呼叫的時候。
如果有兩個檔案中都呼叫了這個類,那麼預設建構函式將可能會被生成兩次。如何避免這種衝突呢?解決方法是把合成的建構函式內聯(inline, 一個行內函式有靜態連結 static linkage)。如果建構函式太複雜,不適合內聯,那麼就會合成一個顯式的非內聯靜態實體(explicit noinline static)。

例如:

class Foo {
public:
	Foo()
};
class Bar {
Foo foo;
char *str;
};
// 編譯器將會為Bar合成預設建構函式,初始化foo成員。但是並不會初始化str成員!!!

如果一個類Foo有自己定義的建構函式(預設的,或者帶有引數的),並且這個類還有一個或者多個類成員。那麼編譯器將會保證這些類成員的建構函式在Foo的建構函式中至少被呼叫一次。這些成員的構造順序與它們在class中的宣告順序一致。

帶預設建構函式的基礎類別

類似的,如果一個沒有建構函式的派生類繼承自一個帶有預設建構函式的基礎類別。那麼編譯器將會為這個派生類生成預設建構函式。在這個生成的建構函式中按照宣告的順序呼叫基礎類別的建構函式。

如果派生類有建構函式,但是沒有預設建構函式。編譯器將會為現有的建構函式中安插呼叫基礎類別建構函式的程式碼,但是並不會生成一個新的預設建構函式。

如果這個派生類同時有類成員(這些類成員有預設建構函式)。那麼編譯器將會在基礎類別建構函式的程式碼後面安插呼叫資料成員建構函式的程式碼。

帶有虛擬函式的類

有以下兩種情況會生成預設建構函式

  • 類宣告或者繼承一個虛擬函式
  • 類派生自一個繼承串鏈,其中有一個或者更多的虛基礎類別(virtual base class)

在編譯期間,編譯器會做兩件事情:

  1. 生成一個虛擬函式表
  2. 為每一個物件新增一個額外的虛擬函式表指標。

對於類所定義的每一個建構函式,編譯器將會安插一些程式碼,為虛表指標賦初值。帶有一個虛基礎類別的類編譯器會使一個虛基礎類別在派生類中的位置 在執行期間所決定??
例如

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;};
​
// 無法在編譯時期決定i的位置(偏移?)
void foo(const A *pa) {
    pa->i = 1024;
}
​
int main ()
{
    foo (new C);
    foo (new A);
    ...
}

編譯器的一種可能的實現方式為:
在每一個派生類的虛基礎類別中安插一個指標。經由參照或者指標來存取一個虛基礎類別的功能都可以由這個指標來實現。
例如:

// 可能的轉換操作
void foo(const A *pa) {
    pa->__vbcX->i = 1024;
}

總結

在編譯器合成的預設建構函式中,會為那些有預設建構函式的成員或者基礎類別呼叫其預設建構函式。而那些其他的非靜態資料成員,例如整數,指標等都不會被初始化,需要類的設計者(就是程式設計師了)來做這些操作。

新手一般會有兩個誤解

  1. 任何類如果沒有定義預設建構函式,那麼就會被合成出一個來。
  2. 編譯器合成的預設建構函式會明確設定類中的每一個資料成員初始值。

這兩個沒有一個是真的!!!