虛繼承
虛繼承解決了菱形繼承中最派生類擁有多個間接父類執行個體的情況。虛繼承的派生類的記憶體布局與普通繼承很多不同,主要展現在:
- 虛繼承的派生類,如果定義了新的虛函數,則編譯器為其生成一個虛函數指針(vptr)以及一張虛函數表。該vptr位于對象記憶體最前面。(非虛繼承時,派生類新的虛函數直接擴充在基類虛函數表的下面。)
- 虛繼承的派生類有單獨的虛函數表,基類也有單獨的虛函數表,兩部分之間用一個四個位元組的0x00000000來作為分界。
- 虛繼承的派生類對象中,含有四位元組的虛基表指針。
在C++對象模型中,虛繼承而來的派生類會生成一個隐藏的虛基類指針(vbptr),在Microsoft Visual C++中,虛基類表指針總是在虛函數表指針之後,因而,對某個類執行個體來說,如果它有虛基類指針,那麼虛基類指針可能在執行個體的0位元組偏移處(該類沒有vptr時,vbptr就處于類執行個體記憶體布局的最前面,否則vptr處于類執行個體記憶體布局的最前面),也可能在類執行個體的4位元組偏移處。
一個類的虛基類指針指向的虛基類表,與虛函數表一樣,虛基類表也由多個條目組成,條目中存放的是偏移值。第一個條目存放虛基類表指針(vbptr)所在位址到該類記憶體首位址的偏移值,由上面的分析我們知道,這個偏移值為0(類沒有vptr)或者-4(類有虛函數,此時有vptr)。虛基類表的第二、第三…個條目依次為該類的最左虛繼承父類、次左虛繼承父類…的記憶體位址相對于虛基類表指針的偏移值。我們通過一張圖來更好地了解。

代碼
class B
{
public:
int ib;
public:
B(int i = 1) :ib(i) {}
virtual void f() { cout << "B::f()" ; }
virtual void Bf() { cout << "B::Bf()" ; }
};
class B1 : virtual public B
{
public:
int ib1;
public:
B1(int i = 100) :ib1(i) {}
virtual void f() { cout << "B1::f()" ; }
virtual void f1() { cout << "B1::f1()" ; }
virtual void Bf1() { cout << "B1::Bf1()" ; }
};
對象模型
代碼示範
typedef void(*Fun)(void);
int main()
{
B1 a;
cout << "B1對象記憶體大小為:" << sizeof(a) << endl;
//取得B1的虛函數表
cout << "[0]B1::vptr";
cout << "\t位址:" << (int*)(&a) << endl;
//輸出虛表B1::vptr中的函數
for (int i = 0; i < 2; ++i)
{
cout << " [" << i << "]";
Fun fun1 = (Fun) * ((int*)*(int*)(&a) + i);
fun1();
cout << "\t位址:\t" << *((int*)*(int*)(&a) + i) << endl;
}
//[1]
cout << "[1]vbptr ";
cout << "\t位址:" << (int*)(&a) + 1 << endl; //虛表指針的位址
//輸出虛基類指針條目所指的内容
for (int i = 0; i < 2; i++)
{
cout << " [" << i << "]";
cout << *(int*)((int*)*((int*)(&a) + 1) + i);
cout << endl;
}
//[2]
cout << "[2]B1::ib1=" << *(int*)((int*)(&a) + 2);
cout << "\t位址:" << (int*)(&a) + 2;
cout << endl;
//[3]
cout << "[3]值=" << *(int*)((int*)(&a) + 3);
cout << "\t\t位址:" << (int*)(&a) + 3;
cout << endl;
//[4]
cout << "[4]B::vptr";
cout << "\t位址:" << (int*)(&a) + 4 << endl;
//輸出B::vptr中的虛函數
for (int i = 0; i < 2; ++i)
{
cout << " [" << i << "]";
Fun fun1 = (Fun) * ((int*)*((int*)(&a) + 4) + i);
fun1();
cout << "\t位址:\t" << *((int*)*((int*)(&a) + 4) + i) << endl;
}
//[5]
cout << "[5]B::ib=" << *(int*)((int*)(&a) + 5);
cout << "\t位址: " << (int*)(&a) + 5;
cout << endl;
}
結果
這個結果與我們的C++對象模型圖完全符合。這時我們可以來分析一下虛表指針的第二個條目值12的具體來源了,回憶上文講到的:
第二、第三…個條目依次為該類的最左虛繼承父類、次左虛繼承父類…的記憶體位址相對于虛基類表指針的偏移值。
在我們的例子中,也就是B類執行個體記憶體位址相對于vbptr的偏移值,也即是:[4]-[1]的偏移值,結果即為12,從位址上也可以計算出來:00F8FDE4-00F8FDD8結果的十進制數是12。現在,我們對虛基類表的構成應該有了一個更好的了解。
菱形繼承
class B
{
public:
int ib;
public:
B(int i = 1) :ib(i) {}
virtual void f() { cout << "B::f()" << endl; }
virtual void Bf() { cout << "B::Bf()" << endl; }
};
class B1 : virtual public B
{
public:
int ib1;
public:
B1(int i = 100) :ib1(i) {}
virtual void f() { cout << "B1::f()" << endl; }
virtual void f1() { cout << "B1::f1()" << endl; }
virtual void Bf1() { cout << "B1::Bf1()" << endl; }
};
class B2 : virtual public B
{
public:
int ib2;
public:
B2(int i = 1000) :ib2(i) {}
virtual void f() { cout << "B2::f()" << endl; }
virtual void f2() { cout << "B2::f2()" << endl; }
virtual void Bf2() { cout << "B2::Bf2()" << endl; }
};
class D : public B1, public B2
{
public:
int id;
public:
D(int i = 10000) :id(i) {}
virtual void f() { cout << "D::f()" << endl; }
virtual void f1() { cout << "D::f1()" << endl; }
virtual void f2() { cout << "D::f2()" << endl; }
virtual void Df() { cout << "D::Df()" << endl; }
};
菱形虛拟繼承下,最派生類D類的對象模型又有不同的構成了。在D類對象的記憶體構成上,有以下幾點:
- 在D類對象記憶體中,基類出現的順序是:先是B1(最左父類),然後是B2(次左父類),最後是B(虛祖父類)。
- D類對象的資料成員id放在B類前面,兩部分資料依舊以0來分隔。
- 編譯器沒有為D類生成一個它自己的vptr,而是覆寫并擴充了最左父類的虛基類表,與簡單繼承的對象模型相同。
- 共同虛基類B的内容放到了派生類對象D記憶體布局的最後。
代碼示範
typedef void(*Fun)(void);
int main()
{
D d;
cout << "D對象記憶體大小為:" << sizeof(d) << endl;
//取得B1的虛函數表
cout << "[0]B1::vptr";
cout << "\t位址:" << (int*)(&d) << endl;
//輸出虛表B1::vptr中的函數
for (int i = 0; i < 3; ++i)
{
cout << " [" << i << "]";
Fun fun1 = (Fun) * ((int*)*(int*)(&d) + i);
fun1();
cout << "\t位址:\t" << *((int*)*(int*)(&d) + i) << endl;
}
//[1]
cout << "[1]B1::vbptr ";
cout << "\t位址:" << (int*)(&d) + 1 << endl; //虛表指針的位址
//輸出虛基類指針條目所指的内容
for (int i = 0; i < 2; i++)
{
cout << " [" << i << "]";
cout << *(int*)((int*)*((int*)(&d) + 1) + i);
cout << endl;
}
//[2]
cout << "[2]B1::ib1=" << *(int*)((int*)(&d) + 2);
cout << "\t位址:" << (int*)(&d) + 2;
cout << endl;
//[3]
cout << "[3]B2::vptr";
cout << "\t位址:" << (int*)(&d) + 3 << endl;
//輸出B2::vptr中的虛函數
for (int i = 0; i < 2; ++i)
{
cout << " [" << i << "]";
Fun fun1 = (Fun) * ((int*)*((int*)(&d) + 3) + i);
fun1();
cout << "\t位址:\t" << *((int*)*((int*)(&d) + 3) + i) << endl;
}
//[4]
cout << "[4]B2::vbptr ";
cout << "\t位址:" << (int*)(&d) + 4 << endl; //虛表指針的位址
//輸出虛基類指針條目所指的内容
for (int i = 0; i < 2; i++)
{
cout << " [" << i << "]";
cout << *(int*)((int*)*((int*)(&d) + 4) + i);
cout << endl;
}
//[5]
cout << "[5]B2::ib2=" << *(int*)((int*)(&d) + 5);
cout << "\t位址: " << (int*)(&d) + 5;
cout << endl;
//[6]
cout << "[6]D::id=" << *(int*)((int*)(&d) + 6);
cout << "\t位址: " << (int*)(&d) + 6;
cout << endl;
//[7]
cout << "[7]值=" << *(int*)((int*)(&d) + 7);
cout << "\t\t位址:" << (int*)(&d) + 7;
cout << endl;
//間接父類
//[8]
cout << "[8]B::vptr";
cout << "\t位址:" << (int*)(&d) + 8 << endl;
//輸出B::vptr中的虛函數
for (int i = 0; i < 2; ++i)
{
cout << " [" << i << "]";
Fun fun1 = (Fun) * ((int*)*((int*)(&d) + 8) + i);
fun1();
cout << "\t位址:\t" << *((int*)*((int*)(&d) + 8) + i) << endl;
}
//[9]
cout << "[9]B::id=" << *(int*)((int*)(&d) + 9);
cout << "\t位址: " << (int*)(&d) + 9;
cout << endl;
getchar();
}
資料成員如何通路(直接取址)
跟實際對象模型相關聯,根據對象起始位址+偏移量取得。
函數成員如何通路(間接取址)
跟實際對象模型相關聯,普通函數(nonstatic、static)根據編譯、連結的結果直接擷取函數位址;如果是虛函數根據對象模型,取出對于虛函數位址,然後在虛函數表中查找函數位址。
多态如何實作?
多态(Polymorphisn)在C++中是通過虛函數實作的。如果類中有虛函數,編譯器就會自動生成一個虛函數表,對象中包含一個指向虛函數表的指針。能夠實作多态的關鍵在于:虛函數是允許被派生類重寫的,在虛函數表中,派生類函數對覆寫(override)基類函數。除此之外,還必須通過指針或引用調用方法才行,将派生類對象賦給基類對象。
為什麼析構函數設為虛函數是必要的
析構函數應當都是虛函數,除非明确該類不做基類(不被其他類繼承)。基類的析構函數聲明為虛函數,這樣做是為了確定釋放派生對象時,按照正确的順序調用析構函數。
如果析構函數不定義為虛函數,那麼派生類就不會重寫基類的析構函數,在有多态行為的時候,派生類的析構函數不會被調用到(有記憶體洩漏的風險!)。例如,通過new一個派生類對象,賦給基類指針,然後delete基類指針,缺少了派生類的析構函數調用。把析構函數聲明為虛函數,調用就正常了。