由結構體對齊所引發的對C++類物件記憶體模型的思考(二)

Editor發表於2017-12-12

虛基類的影響

1.多繼承

很多時候,一個子類可能有多個父類,比如美人魚既是人也是魚,冬蟲夏草,可以看視訊可以上網的手機,為了增強程式碼複用能力,就有了多繼承,示例程式碼如下:


class Base_A

{

public:

Base_A() :a(0x10), b(0x20)

{  }

int a;

int b;

};

class Base_B

{

public:

Base_B() :c(0x30), d(0x40)

{  }

int c;

int d;

};

class Inherit :public Base_A, public Base_B

{

public:

Inherit() :e(0x50)

{  }

int e;

};

int main()

{

Inherit obj;

return 0;

}

程式碼中,Inherit的物件,就能夠使用從兩個父類繼承下來的所有資料和方法(需要考慮許可權問題)。我們來看一下它的記憶體模型:

由結構體對齊所引發的對C++類物件記憶體模型的思考(二)

可以看到,子類物件包含著父類的全部資料,我們再看另外一種情況:


class Base_A

{

public:

Base_A() :a(0x10), b(0x20)

{  }

int a;

int b;

};

class Base_B

{

public:

Base_B() :c(0x30), d(0x40)

{  }

int c;

int d;

};

class Inherit :public Base_B, public Base_A

{

public:

Inherit() :e(0x50)

{  }

int e;

};

int main()

{

Inherit obj;

return 0;

}

記憶體模型如下:

  由結構體對齊所引發的對C++類物件記憶體模型的思考(二) 

此時我們可以得出一些簡單的結論:

派生類放在最下面 多個父類的情況下,誰在上,誰在下,由繼承順序決定。 子類總是包含全部的父類

2.多繼承中的二義性問題

暮光之城中有這麼一種物種叫做狼人, 暮色之時是人類,新月到破曉就是狼人了,它有著鋒利的牙齒,恐怖的速度,還能兩個腿奔跑。它可以由狼類和人類共同派生出來。但是有一個問題,就是狼類中可能會有腿的數量,牙齒的數量等等屬性,恰好人類中也有腿的數量,牙齒的數量等等屬性。我們知道子類會具有全部父類的所有成員。那麼此時此刻,狼人物件訪問腿的數量,牙齒的數量的時候,會訪問哪個父類的成員呢?有人已經想出了辦法,就是把狼和人都有的成員抽象出來,形成一個爺爺類,比如叫做動物類,在狼類和人類的上面,形成如下圖所示的情況:由結構體對齊所引發的對C++類物件記憶體模型的思考(二)
為了解決我們心中的疑惑,我們可以做個試驗,先看下面這段程式碼:



class Animal

{

public:

Animal() :m_nNumberOfLegs(5)//預設5條腿^o^

{  }

public:

int m_nNumberOfLegs;

};

class Wolf :public Animal

{

public:

Wolf() :m_nWolfSomeThing(0x10)

{  }

public:

int m_nWolfSomeThing;

};

class Human :public Animal

{

public:

Human() :m_nHumanSomeThing(0x20)

{  }

public:

int m_nHumanSomeThing;

};

class Werwolf :public Wolf, public Human

{

public:

Werwolf() :m_nWerwolfSomeThing(0x30)

{  }

public:

int m_nWerwolfSomeThing;

};

int main()

{

Werwolf obj;

return 0;

}

檢視狼人類記憶體模型:

  由結構體對齊所引發的對C++類物件記憶體模型的思考(二)

我們發現有兩份腿的數量,這是因為子類物件會包含全部的父類成員。對於狼來說,自然會包含動物類中的腿的數量。對於人來說,也是如此。對於狼人來說,會同時包含狼類和人類的所有成員。故而腿的數量這個欄位,在狼人物件中依然是出現兩份,一份在狼中,一份在人中,這是典型的菱形繼承問題。

3.虛繼承

為了解決上面這個問題,產生了一種叫做虛繼承的機制:虛繼承是為了解決二義性的問題而產生的語法。用法是在繼承之前加上一個virtual,我們來看一下最為簡單的情況,下面的例子可以幫助我們理解虛繼承:


class Base

{

public:

Base() :m_B(0x10)

{  }

public:

int m_B;

};

class Inherit :virtual public Base

{

public:

Inherit() :m_I(0x20)

{  }

public:

int m_I;

};

int main()

{

Inherit obj;

printf("虛繼承的物件大小%d", sizeof(obj));

return 0;

}

我們可以看一看輸出結果:(結果可能會讓你大吃一驚哦)

  由結構體對齊所引發的對C++類物件記憶體模型的思考(二)

有人可能會問不是應該為8個位元組麼,怎麼會是12呢,那多出來的四個位元組究竟是什麼?

好,下面我們看一看它的記憶體模型:

  由結構體對齊所引發的對C++類物件記憶體模型的思考(二)

我們可以看到在整個物件的開頭多了一個奇怪的資料,並且神奇的是子類資料位於基類資料的上面,我們來解釋它在幹什麼:

通過查閱相關文獻,得知頭四個位元組實際上是一個地址,即0x01186b30,

我們可以檢視一下:

  由結構體對齊所引發的對C++類物件記憶體模型的思考(二)

剛才的那個地址,我們稱之為虛基類表指標,指向的位置儲存的是一共有兩個元素,分別是兩個差值:

1 本類地址與虛基類表指標地址的差

2 虛基類地址與虛基類表指標地址的差


struct VirtualBase

{

int   Offset1;

int   Offset2;

}

這裡我們著重關注第二個,它能夠實現這樣的事情:基類與派生類可以不挨在一起,是通過虛基類表中的差值,從派生類就可以找到基類的資料。


我們直接看複雜一些的情況,結合上面的例子更加容易理解一些:


class Base

{

public:

Base() :m_Base(0x10)

{  }

public:

int m_Base;

};

class Inherit_A :virtual public Base

{

public:

Inherit_A() :m_A(0x20)

{  }

public:

int m_A;

};

class Inherit_B :virtual public Base

{

public:

Inherit_B() :m_B(0x30)

{  }

public:

int m_B;

};

class Test :public Inherit_A, public Inherit_B

{

public:

Test() :m_T(0x40)

{  }

public:

int m_T;

};

int main()

{

Test obj;

printf("虛繼承的物件大小%d", sizeof(obj));

return 0;

}

輸出結果:

  由結構體對齊所引發的對C++類物件記憶體模型的思考(二)

這個結果估計大多數人都沒有猜到,呵呵我們可以來看一下它的記憶體模型:

  由結構體對齊所引發的對C++類物件記憶體模型的思考(二)

可以看出: 從上到下的順序是A,B,派生類,基類Base。Base類被甩到了最後,並且只有一個。

Inherit_A與Inherit_B共用一個虛基類。這個機制,無論是幾個中間內一層的類,都能保證虛基類的資料只有一份,這就是虛繼承解決多繼承中二義性的問題.


小結一下:

進行如圖所示的虛繼承
由結構體對齊所引發的對C++類物件記憶體模型的思考(二)
編譯器會把虛基類單獨置於一處,派生類通過虛基類表指標指向位置儲存的差值能夠找到虛基類,當類似於圖示的情況下的時候,使得孫子類無論從哪一條支路尋找爺爺類(虛基類),找到的都是同一個爺爺。
由結構體對齊所引發的對C++類物件記憶體模型的思考(二) 
對於類物件大小,每一個虛繼承的子類由於都會有一個虛基類指標,故而多一個虛繼承,整個物件的大小就會比正常大4個位元組。(這一點與虛擬函式那邊有點類似,呵呵) 虛基類實際上不需要一定放在下面,放在任何位置都可以,因為大家是通過一個差值找到的它。

終於寫完了,呵呵,這是本人在看雪上面發的第一個帖子,因本人水平有限 ,難免會有紕漏,還請各位多提寶貴意見。(本來寫個目錄導航的,結果用toc寫出來之後不知怎麼搞的竟然生成多份目錄,不知什麼原因)

相關文章