您好,登錄后才能下訂單哦!
C++中虛函數的誕生,就是為了多態(tài)的實現(xiàn)。當子類對父類的虛函數進行了重寫,在父類指針調用重寫的虛函數時,如果父類指針(或引用)指向了父類的對象,則調用父類的虛函數,如果父類指針(或引用)指向了子類對象,則調用子類的虛函數。
要想了解多態(tài)的實現(xiàn),就必須要知道虛函數表的構成。
【注:文章中代碼測試環(huán)境為Win7 64位 VS2013】
首先,我們討論單個含有虛函數的類,即不存在繼承關系。
當我們的類中含有虛函數時,類實例化出來的對象,他的成員除了自己的成員變量外,還會多出一個指針。這個指針我們稱為虛表指針,他所指向的是我們類對象所維護的虛表。虛表中保存的是類中所有虛函數的地址。代碼如下:
class B { public: B() :_val(1){} virtual void fun1() { cout << "void B::fun1()" << endl; } virtual void fun2() { cout << "void B::fun2()" << endl; } void fun3() { cout << "void B::fun3()" << endl; } private: int _val; }; int main() { B b; cout << sizeof(b) << endl;//輸出結果為8 system("pause"); return 0; }
代碼說明:構造函數給類成員變量賦值為1,方便內存查看,成員函數有兩個虛函數,一個非虛函數,斷點打在return 0;處。
然后我們切換到監(jiān)視窗口,<如下>可以發(fā)現(xiàn),對象b實際上維護了兩個成員,"__vfptr"和"_val",在內存窗口中"&b",得到的是 0012cc74 ,即 __vfptr,下一個是 00000001,即我們的成員 _val。這也解釋了為什么 sizeof(b) 的輸出結果是8。不管有多少虛函數,類中只保留了一個虛表指針,添加再多的虛函數,也不會改變sizeof(b)的值
另外,可以看到的是虛表只保存虛函數地址,非虛函數,依舊是屬于整個類而非對象。
我們再次查看 __vfptr 指向的空間
前兩個是我虛函數的地址,也就是說,他們之間存在關系如下
為了驗證,這里我重新封裝一個函數指針,通過偏移,看能不能輸出cout里面的內容。代碼如下:
typedef void(*p_fun)(); void Print_fun(p_fun* ppfun) { for (int i = 0;/* ppfun[i] != NULL*/i<2; i++) { ppfun[i](); } } void test() { B b; Print_fun((p_fun*)(*(int*)&b)); }
測試是可以輸出我們函數內容的。多說一句,虛表中前面保存的都是虛函數的地址,最后結束項在不同編譯器下是不一樣的,在VS2013環(huán)境下,最后一項保存的地址是不可訪問的,VS2008環(huán)境下,最后是以0x00000000結尾,即是NULL。所以打印函數Print_fun()中for循環(huán)條件我做了修改。當然可以更加直接的這樣調用函數。
// ((p_fun*)(*(int*)&b))[0](); // ((p_fun*)(*(int*)&b))[1]();
接下來,我們看看包含虛函數重寫的單繼承中的虛表
這里給出單繼承的測試代碼
class A { public: A() :_a_val(1){} virtual void fun1() { cout << "void A::fun1()" << endl; } virtual void fun2() { cout << "void A::fun2()" << endl; } protected: int _a_val; }; class B:public A { public: B() :_b_val(2){} virtual void fun1() { cout << "virtual B::fun1()" << endl; } virtual void fun3() { cout << "virtual B::fun3()" << endl; } virtual void fun4() { cout << "virtual B::fun4()" << endl; } protected: int _b_val; }; void test() { B b; }
代碼說明:父類 A 包含兩個虛函數 fun1() 、fun2(),一個成員變量 _a_val ,構造成員變量為 1 ;子類 B 共有繼承了 A ,重寫函數 fun1() ,同時添加兩個自己的虛函數 fun3()、fun4(),成員變量 _b_val ,構造為 2 。
接下來切換到調試窗口<如下圖>,可以看到類B實例化對象 b 實際上維護了三個成員,"__vfptr"、"_a_val"、"_b_val",在內存窗口中“&b”,得到的是0x009bcd48,對應到監(jiān)視窗口,即 __vfptr ,接下來是0x00000001,0x00000002,即成員變量_a_val,_b_val。如果在這里去sizeof(b),得到的結果應該是12。
接下來查看 __vfptr 指向的空間
監(jiān)視中看到的是只有兩個函數地址,但內存窗口中可以看到,前四個在內存中是在一起的,或者說很近。為了確認,使用剛剛的打印函數,不過需要改變一下循環(huán)次數,受編譯器的限制,這里只能手動修改循環(huán)次數,看最多打印多少次是正常結束,而非程序崩潰。
typedef void(*p_fun)(); void Print_fun(p_fun* ppfun) { for (int i = 0;/* ppfun[i] != NULL*/i<4; i++) { ppfun[i](); } } void test() { B b; // ((p_fun*)(*(int*)&b))[0](); // ((p_fun*)(*(int*)&b))[1](); // ((p_fun*)(*(int*)&b))[2](); // ((p_fun*)(*(int*)&b))[3](); Print_fun((p_fun*)(*(int*)&b)); }
測試得到,最多可以打印四次,打印結果如下:
*可以看到fun1()函數被子類 B 重寫,fun2()函數繼承自父類。得到結果監(jiān)視窗口未顯示虛函數fun3()和fun4()地址,但實際上子類新創(chuàng)建的虛函數地址也會保存到虛表當中,而且在單繼承過程中,子類的虛函數和父類的虛函數是保存在同一虛表當中,并未對子類的虛函數創(chuàng)建獨立的虛表。
即有下圖關系:
接下來的多繼承中的對象模型
首先給出測試代碼,如下
class A { public: A() :_a_val(1){} virtual void test1() { cout << "A::test1()" << endl; } virtual void test2() { cout << "A::test2()" << endl; } protected: int _a_val; }; class B { public: B() :_b_val(2){} virtual void test1() { cout << "B::test1()" << endl; } virtual void test3() { cout << "B::test3()" << endl; } protected: int _b_val; }; class C { public: C() :_c_val(3){} virtual void test1() { cout << "C::test1()" << endl; } virtual void test4() { cout << "C::test4()" << endl; } protected: int _c_val; }; class D:public A,public B,public C { public: D() :_d_val(4){} virtual void test1() { cout << "D::test1()" << endl; } virtual void test5() { cout << "D::test5()" << endl; } protected: int _d_val; }; void test() { D d; }
代碼說明:首先創(chuàng)造三個基類,類 A 包含兩個虛函數fun1()、fun2(),類包含成員變量 _a_val ,構造為1;類 B 包含兩個虛函數 fun1()、fun3(),類包含成員變量 _b_val,構造為2;類 C 包含兩個虛函數 fun1()、fun4(),類包含成員變量 _c_val,構造為3;創(chuàng)建第四個類,作為派生類 D ,同時共有繼承類A、類B、類C,包含虛函數fun1(),fun2(),fun1()函數對子類中的fun1()進行重寫,同時包含成員變量_d_val。
接下來切換到調試窗口<如下圖>,可以看到類 D 實例化對象 d 這里維護了七個成員,由于繼承了三個類,因此這里有三個虛表指針"__vfptr"、同時包含繼承自三個類的成員變量"_a_val"、"_b_val"、"_c_val"和自己本身的成員變量"_d_val"。在內存窗口中“&d”,得到的是0x013bdd04,對應到監(jiān)視窗口,即繼承的第一個類的虛表指針 __vfptr ,接下來是0x00000001,即成員變量_a_val,接下來依次類推,得到第二個類的虛表指針,和繼承自第二個類的成員變量,第三個類的虛表指針,和繼承自第三個類的成員變量,最后一項是子類 D 的成員變量。如果在這里去 sizeof(b),得到的結果應該是28。
不過這里有個問題,是子類 D 的虛函數地址在哪里。。這里我們打開多個內存窗口,同時把各個虛表指針指向的內容列出來。<如圖>
可以看到,盡管監(jiān)視窗口中,A的虛表指針下只有兩項,但對應到內存中卻有三項,可以推測,子類單獨的虛函數地址是保存在了第一繼承子類的虛函數表中,未覆蓋的虛函數不會單獨創(chuàng)建一塊虛函數表。
除此之外,還應該可以看到,子類每繼承一個含有虛函數的父類,就會多一個虛表指針,可能會同時維護多個虛表。
換句話說,存在如下圖對應關系。
多提一點,子類繼承了多個父類,父類虛表的地址不一定是連續(xù)的
這里依舊使用函數指針的方式去調用我的成員函數來加以驗證。代碼如下
typedef void(*p_fun)(); void Print_fun(p_fun* ppfun) { for (int i = 0; ppfun[i] != NULL; i++) { ppfun[i](); } } void test() { D d; Print_fun((p_fun*)(*(int*)&d)); cout << "-----------------------------------------" <<endl; Print_fun((p_fun*)(*((int*)&d+2))); cout << "-----------------------------------------" << endl; Print_fun((p_fun*)(*((int*)&d+4))); cout << "-----------------------------------------" << endl; }
打印結果如下:
由打印結果可見,子類專有的虛函數test5()的函數地址放在了第一個繼承的虛表中,test1()函數均被子類 D 重寫。
我們通過虛函數表理解C++ 中的對象模型,了解多態(tài)實際上是用虛函數實現(xiàn)覆蓋,但通過上面的測試,可以發(fā)現(xiàn),實現(xiàn)多態(tài)的同時,無疑會帶來效率的下降(通過兩次指針解引用才可以訪問)。
除此之外應該看到的一點是,多態(tài)實現(xiàn)的過程是不安全的,盡管虛函數表的內容我們不能夠隨意修改,但永遠可以被直接訪問,這是不安全的一種直接表現(xiàn)。
關于菱形繼承的對象模型和菱形虛擬繼承的對象模型,會在下一篇中提到。
------------------------------------------muhuizz------------------------------------------
http://11331490.blog.51cto.com
免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。