您好,登錄后才能下訂單哦!
上一篇討論過了關于虛表的一些基礎知識,除了介紹了虛函數在內存中的存儲結構,還剖析了單繼承與多繼承的底層實現方式。
首先重提多態(tài),多態(tài)是指C++的多種狀態(tài)。
分為靜態(tài)多態(tài)和動態(tài)多態(tài)。靜態(tài)多態(tài),簡言之就是重載,是在編譯期決議確定函數地址。動態(tài)多態(tài)主要指的是通過繼承和重寫基類的虛函數實現的多態(tài),運行時決議確定,在虛表中確定調用函數的地址。C++ 實現多態(tài),其實是通過虛表來實現。這里就菱形繼承做出內存剖析。
菱形繼承:
菱形繼承,大致關系如下
了解過C++的人都知道,菱形繼承必然會帶來數據冗余和二義性問題,通過虛繼承可以來解決這個問題。首先給出一組測試代碼,進入內存底部,去查看沒有虛擬繼承下的菱形繼承,會帶來什么樣的問題。代碼如下:
class B { public: B() :_b_val(1){} virtual void test1() { cout << "B::test1()" <<endl; } protected: int _b_val; }; class B1:public B { public: B1() :_b1_val(2){} virtual void test1() { cout << "B1::test1()" << endl; } virtual void test2() { cout << "B1::test2()" << endl; } virtual void test3() { cout << "B1::test3()" << endl; } protected: int _b1_val; }; class B2 :public B { public: B2() :_b2_val(3){} virtual void test1() { cout << "B2::test1()" << endl; } virtual void test4() { cout << "B2::test4()" << endl; } virtual void test5() { cout << "B2::test5()" << endl; } protected: int _b2_val; }; class D:public B1,public B2 { public: D() :_d_val(4){} virtual void tets1() { cout << "D::test1()" << endl; } virtual void test2() { cout << "D::test2()" << endl; } virtual void test4() { cout << "D::test4()" << endl; } virtual void test6() { cout << "D::test6()" << endl; } protected: int _d_val; }; void test() { D d; }
代碼說明:定義父類B,包含一個虛函數test1(),同時包含一個成員變量,構造為1;定義類B1,共有繼承類 B ,包含三個虛函數,test1()重寫 B 的虛函數,test2()和test3()為自己的虛函數,同時包含成員變量_b1_val,構造為2;定義類B2,共有繼承類 B ,包含三個虛函數,test1()重寫 B 的虛函數,test4()和test5()為自己的虛函數,同時包含成員變量_b2_val,構造為3;定義類 D,共有繼承類 B1和B2,包含四個虛成員函數,test1()、test2()、test4()為重寫繼承父類的虛函數,test6()為類 D 自己的虛函數,同時包含成員變量_d_val,構造為4。
接下來切換到調試窗口<如下圖>,可以看到類 D 實例化對象 d 這里維護了七個成員,由于直接繼承了兩個類,因此這里有兩個虛表指針"__vfptr"、同時包含兩次繼承自基類的成員變量"_b_val"、"_b1_val",和"_b_val"、"_b2_val",和自己本身的成員變量"_d_val"。在內存窗口中“&d”,得到的是0x00cbdd10,對應到監(jiān)視窗口,即繼承的第一個類的虛表指針 __vfptr ,接下來是0x00000001,0x000000002即成員變量_b_val,_b1_val,接下來依次類推,得到第二個類的虛表指針,和繼承自第二個類的成員變量,最后一項是子類 D 的成員變量。如果在這里去 sizeof(b),得到的結果應該是28。
在這里,依舊沒有能直接找到子類 D 自己的虛成員函數地址。接下來,分別打開兩個虛表指針所指向的內存空間。
多說一點,這里兩個虛表指針指向的內容是沒有相同的函數指針的。
這里可以注意到,第一個繼承的類的虛表中有五個地址,第二個繼承的類中國有三個地址,但我們在定義類 B1 和 B2 時,是完全相同的,也都是直接共有繼承,所以推斷,我們的子類 D 中自己的虛函數指針是保存在了第一個繼承的類的虛表中。還是沿用上一篇提到的用函數指針的方式去調用這些函數,代碼如下:
typedef void(*p_fun)(); void Print_fun(p_fun* ppfun) { int i = 0; for (; ppfun[i] != NULL; i++) { ppfun[i](); } } void test() { D d; Print_fun((p_fun*)(*(int*)&d)); cout << "-------------------------------" << endl; Print_fun((p_fun*)*((int*)&d+3)); }
這里我是直接通過指針的偏移調用到每一個函數,是因為我這里在內存中知道了有多少個函數指針,觀察輸出結果,如下圖:
因此我們可以得到在菱形非虛擬繼承下的內存布局是下圖這樣的關系:
在第一個繼承的類的虛表中,保存了被子類 D 重寫的函數test1()、test2(),未被改寫的函數test3(),和D類自己的虛函數test6()。第二個繼承的類的虛表中,保存了被子類 D 重寫的函數test1()、test4() 和未被改寫的函數test5()。很明顯,test1()函數被保存了兩份,成員變量_b_val也保存了兩份,在實際的開發(fā)過程中,這必然會帶來大量的數據冗余和二義性的問題,同時也會有調用不明確的可能。
我們嘗試用虛繼承的方式去解決
代碼如下:
class B { public: B() :_b_val(1){} virtual void test1() { cout << "B::test1()" <<endl; } protected: int _b_val; }; class B1 : virtual public B { public: B1() :_b1_val(2){} virtual void test1() { cout << "B1::test1()" << endl; } virtual void test2() { cout << "B1::test2()" << endl; } virtual void test3() { cout << "B1::test3()" << endl; } protected: int _b1_val; }; class B2 : virtual public B { public: B2() :_b2_val(3){} virtual void test1() { cout << "B2::test1()" << endl; } virtual void test4() { cout << "B2::test4()" << endl; } virtual void test5() { cout << "B2::test5()" << endl; } protected: int _b2_val; }; class D: public B1, public B2 { public: D() :_d_val(4){} virtual void test1() { cout << "D::test1()" << endl; } virtual void test2() { cout << "D::test2()" << endl; } virtual void test4() { cout << "D::test4()" << endl; } virtual void test6() { cout << "D::test6()" << endl; } protected: int _d_val; }; void test() { D d; cout << sizeof(d) << endl;//----------->40 }
代碼說明:和第一個例子一樣,不同之處在于類 B1 和 B2 虛擬繼承了公共子類 B。
接下來,切換到監(jiān)視窗口,單步調試,如下圖:
說實話,剛開始看到這里的時候,內心是很拒絕的,因為感覺又多出來好多虛表指針,而且更亂了,我們對類 D 實例化出的對象求 sizeof,得到的結果是40 ,比之前未使用虛繼承的時候占用的空間更多。不過按照上面給出的箭頭縷縷,可以發(fā)現,盡管在原來的基礎上多出了三個__vfptr,但慶幸的是三個指向了同一塊空間,而且這一次,我們成員變量并沒有出現出現多次的情況。
仔細觀察的話,會發(fā)現綠色箭頭指向的虛表指針,下面還跟了一個地址,再次走進去看看,如圖:
可以看到,通過偏移,我們的指針確實是可以找到我們監(jiān)視窗口中出現了三次一樣的__vfptr。表明在VS環(huán)境下,我們虛繼承解決菱形繼承帶來問題,實際上是通過指針的偏移實現的。
為了確認,我們使用我們的函數指針對函數進行調用,代碼如下:
typedef void(*p_fun)(); void Print_fun(p_fun* ppfun) { int i = 0; for (; ppfun[i] != NULL; i++) { ppfun[i](); } } void test() { D d; Print_fun((p_fun*)(*(int*)&d)); cout << "-------------------------------" << endl; //Print_fun((p_fun*)*((int*)&d+3));//測試有誤,更改如下 ((p_fun*)*((int*)&d+3))[0](); cout << "-------------------------------" << endl; Print_fun((p_fun*)(*(int*)&d+8));
打印結果如圖:
至此,關于虛繼承對菱形繼承帶來問題的解決已經全部說明完畢,在VS環(huán)境下,通過指針的偏移解決了代碼的冗余和二義性問題,實際開發(fā)過程中,怎么說呢。。能避免盡量避免,這并不是一種很好的做法。引入效率的下降是它的死穴,以空間和時間的代價去解決自己制造的麻煩并不是一個明智之舉。如果關于虛繼承,還是有不理解的地方,建議先看http://11331490.blog.51cto.com/11321490/1841930,多態(tài)的實現是C++一個很出色的地方,但有時候引入的問題還是要值得深究。
---------------------------------------muhuizz-------------------------------------------
http://11331490.blog.51cto.com/11321490/1841930
免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。