網(wǎng)站建設(shè)自己可以建網(wǎng)站嗎最新的域名網(wǎng)站
虛表和虛基表
- 虛表
- 虛基表
- 虛擬繼承和虛函數(shù)都存在時的對象模型
虛表
我們知道,如果類中聲明了的方法是用virtual進(jìn)行修飾的,則說明當(dāng)前這個方法要作為虛函數(shù),而虛函數(shù)的存儲和普通函數(shù)的存儲是有區(qū)別的
當(dāng)有虛函數(shù)聲明時,編譯器會創(chuàng)建一個虛函數(shù)表,將當(dāng)前的虛函數(shù)按照聲明次序放入虛函數(shù)表中,而這個虛函數(shù)表實(shí)際上就是一個函數(shù)指針數(shù)組,然后將當(dāng)前這個虛函數(shù)表的地址放入對象模型的最起始位置。
class A
{
public:virtual void fun1(){cout << "A::fun1()" << endl;}virtual void fun2(){cout << "A::fun2()" << endl;}virtual void fun3(){cout << "A::fun3()" << endl;}int _a;
};
它對應(yīng)的對象模型是這樣的:
所以說,本質(zhì)上虛函數(shù)表是一個函數(shù)指針數(shù)組,而對象模型中存放的是虛函數(shù)表的首地址,當(dāng)我們需要調(diào)用虛函數(shù)時,傳遞對應(yīng)的對象,就可以通過對象的地址獲取對象的虛表指針,從而獲取虛表,進(jìn)而得到對應(yīng)虛函數(shù)表中某個虛函數(shù)的地址,以此來進(jìn)行調(diào)用(知道函數(shù)的入口地址,就可以調(diào)用對應(yīng)的函數(shù))
虛基表
我們知道,當(dāng)出現(xiàn)菱形繼承時,一定會出現(xiàn)對象模型中有多個基類對象成員。
//普通繼承
class A
{
public:int _a;
};
class B : public A
{
public:int _b;
};
class C : public A
{
public:int _c;
};
class D : public B, public C
{
public:int _d;
};
上述代碼中,D類對象中一定會存在B類和C類對象繼承自A類對象的_a這個成員,這樣就出現(xiàn)了兩份_a成員,導(dǎo)致訪問_a時出現(xiàn)二義性,并且隨著繼承深度和廣度的增加,對象成員會越來越冗余。
為了解決這個問題,出現(xiàn)了虛擬繼承。
//菱形虛擬繼承
class A
{
public:int _a;
};
class B : virtual public A
{
public:int _b;
};
class C : virtual public A
{
public:int _c;
};
class D : public B, public C
{
public:int _d;
};
通過讓B類和C類虛擬繼承A類后,對象模型就從圖1變成了圖2
這樣的轉(zhuǎn)變,使得B類和C類雖然繼承了A類,但是B類和C類中并沒有存儲A類的對象(基類對象只有一份,被存放在了整個對象模型的最后),除了子類新增之外,只有一個指針,這個指針就被稱為虛基表指針。
虛基表指針?biāo)赶虻氖且粋€虛基表,對于B類ptr1這個虛基表,總大小為8個字節(jié)(32位系統(tǒng)下),前4個字節(jié)存儲的是子類對象相對于自己起始位置的偏移量,(目前來看是0,當(dāng)存在虛函數(shù)的虛擬繼承時,就不是0了),后4個字節(jié)存儲是子類對象相對于基類部分的偏移量。
ptr2指向C類這個對象的虛基表,總大小為8個字節(jié)(32位系統(tǒng)下),前4個字節(jié)存儲的是子類對象相對于自己起始位置的偏移量,(目前來看是0,當(dāng)存在虛函數(shù)的虛擬繼承時,就不是0了),后4個字節(jié)存儲是子類對象相對于基類部分的偏移量。
可以發(fā)現(xiàn),虛表在整個類對象中只存儲一份,也就是說一個類的不同對象共享同一份虛表。而虛基表有多份,取決于當(dāng)前類是否虛擬繼承了基類,若虛擬繼承了基類,就會創(chuàng)建一個虛基表指針,指向一個虛基表。
虛擬繼承和虛函數(shù)都存在時的對象模型
那么就存在另外一個問題,當(dāng)虛擬繼承和虛函數(shù)同時出現(xiàn)在繼承體系中,對象模型又是什么樣子呢?
class A
{
public:virtual void fun1(){cout << "A::fun1()" << endl;}virtual void fun2(){cout << "A::fun2()" << endl;}int _a;
}
class B : virtual public A
{
public:virtual void fun1(){cout << "B::fun1()" << endl;}virtual void fun3(){cout << "B::fun3()" << endl;}int _b;
};
class C : virtual public A
{
public:virtual void fun2(){cout << "C::fun2()" << endl;}virtual void fun4(){cout << "C::fun4()" << endl;}int _c;
};
class D : public B, public C
{
public:virtual void fun1(){cout << "D::fun1()" << endl;}virtual void fun2(){cout << "D::fun2()" << endl;}virtual void fun5(){cout << "D::fun5()" << endl;}int _d;
}
上述代碼中,B類和C類都繼承自A類,并且對A類中的虛函數(shù)進(jìn)行了重寫,同時也新增了虛函數(shù)。D類繼承了B和C類,對B和C類中的虛函數(shù)進(jìn)行了重寫,同時也新增了虛函數(shù)。
那么當(dāng)前在這個繼承體系下,對象模型是什么樣子呢?
其實(shí)不難想到,由于B類和C類都是虛擬繼承,那么A類成員只會保留一份在最下方,同時B類和C類都會保存自己的虛基表指針,而D類由于是普通繼承,按照順序,新增的虛函數(shù)被放到B類的虛表中。
我們通過取地址發(fā)現(xiàn),對象模型確實(shí)是上述的樣子,但是在D類和A類之間,放了00000000作為對象分割區(qū)分(猜測)
請注意:當(dāng)前的驗(yàn)證情況是在vs2019中進(jìn)行驗(yàn)證的。
總結(jié):當(dāng)虛基表和虛表同時存在(虛擬繼承和虛函數(shù)同時存在時),對象模型從整體上來說還是和虛擬繼承相同(基類對象順序按照聲明的順序從上到下排列,對象中沒有祖父類的成員,祖父類成員被放到了模型的最下方)。但是由于有虛函數(shù)的存在,B類對A類的虛函數(shù)進(jìn)行重寫的虛函數(shù)在A類中直接修改,B類新增的虛函數(shù)被放到B類內(nèi)部的虛表中,C類對A類的虛函數(shù)進(jìn)行重寫的虛函數(shù)在A類中直接修改,C類新增的虛函數(shù)被放到C類內(nèi)部的虛表中。D類對B類和C類進(jìn)行重寫的虛函數(shù)直接在對應(yīng)類中進(jìn)行修改,D類新增的就直接放到B類的虛表中。
通過上述的描述,可以知道對于B類,C類和A類的虛表中存放的虛函數(shù)分別為:
而對于虛基表來說,表示的是子類對象相對于自己起始位置的偏移量,如果是B類,B類對象的起始位置已經(jīng)有了一個虛表指針,那么虛基表中前四個字節(jié)要表示相對自己起始位置的偏移量就需要為-4,而后四個字節(jié)是相對于基類的偏移量是正常的計算方式。
對于B類C類的虛基表來說,其中的值為: