結合OD學習下虛函數表,加深印象
對C++ 了解的人都應該知道虛函數(Virtual Function)是通過一張虛函數表(Virtual Table)來實現的。簡稱為V-Table。 在這個表中,主是要一個類的虛函數的地址表,這張表解決了繼承、覆蓋的問題,保證其容真實反應實際的函數。這樣,在有虛函數的類的實例中這個表被分配在了 這個實例的內存中,所以,當我們用父類的指針來操作一個子類的時候,這張虛函數表就顯得由為重要了,它就像一個地圖一樣,指明了實際所應該調用的函數。
在C++的標準規格說明書中說到,編譯器必須要保證虛函數表的指針存在于對象實例中最前面的位置(這是為了保證正確取到虛函數的偏移量)。 這意味著我們通過對象實例的地址得到這張虛函數表,然后就可以遍歷其中函數指針,并調用相應的函數。
先貼代碼:
[cpp]?view plaincopy
#include?<iostream>?? using?namespace?std;?? ?? ?? class?Base?? {?? public:?? ????virtual?void?f()?? ????{?? ????????cout<<"Base::f"<<endl;?? ????}?? ?? ????virtual?void?g()?? ????{?? ????????cout<<"Base::g"<<endl;?? ????}?? ?? ????virtual?void?h()?? ????{?? ????????cout<<"Base::h"<<endl;?? ????}?? };?? ?? typedef?void(*Fun)(void);??? ?? int?main()?? {?? ????Base?b;??? ????Fun?pFun?=?NULL;??? ?????? ????cout?<<?"虛函數表地址:"?<<?(int*)*(int*)(&b)?<<?endl;??? ????cout?<<?"虛函數表?—?第一個函數地址:"?<<?(int)*(int*)*(int*)(&b)?<<?endl;??? ?????? ????pFun?=?(Fun)*((int*)*(int*)(&b));??? ????pFun();??? ?? ????return?0;?? }??
運行Release,注意不要把PDB文件刪了,用OD運行,在main函數加個斷點,F9,再F8單步調試
[cpp]?view plaincopy
00401090?>/$??A1?3C204000???mov?????eax,?dword?ptr?[<&MSVCP90.std::endl>]?? 00401095??|???8B0D?5C204000?mov?????ecx,?dword?ptr?[<&MSVCP90.std::cout>]?????????????;??MSVCP90.std::cout?? 0040109B??|???50????????????push????eax?? 0040109C??|.??68?98214000???push????offset?Base::`vftable'?? 004010A1??|.??68?64214000???push????00402164?? 004010A6??|???51????????????push????ecx?? 004010A7??|.??E8?84010000???call????std::operator<<<std::char_traits<char>?>?? 004010AC??|???83C4?08???????add?????esp,?8?? 004010AF??|???8BC8??????????mov?????ecx,?eax?? 004010B1??|???FF15?48204000?call????dword?ptr?[<&MSVCP90.std::basic_ostream<char,std:>;??MSVCP90.std::basic_ostream<char,std::char_traits<char>?>::operator<<?? 004010B7??|???8BC8??????????mov?????ecx,?eax?? 004010B9??|???FF15?40204000?call????dword?ptr?[<&MSVCP90.std::basic_ostream<char,std:>;??MSVCP90.std::basic_ostream<wchar_t,std::char_traits<wchar_t>?>::operator<<?? 004010BF??|???8B15?3C204000?mov?????edx,?dword?ptr?[<&MSVCP90.std::endl>]?????????????;??MSVCP90.std::endl?? 004010C5??|???A1?98214000???mov?????eax,?dword?ptr?[Base::`vftable']?? 004010CA??|???8B0D?5C204000?mov?????ecx,?dword?ptr?[<&MSVCP90.std::cout>]?????????????;??MSVCP90.std::cout??
在0040109C |. 68 98214000 push offset Base::`vftable'發現虛函數表的地址,調試到此:
可以看到information了
00402198=offset Base::`vftable'
test.cpp:31.? cout << "虛函數表地址:" << (int*)*(int*)(&b) << endl;?
之所以能顯示test.cpp:31是因為存在符號文件,繼續運行,直到出現
這里打印是00402198,和前面的information框顯示一致,說明判斷沒錯,看代碼知道,這是類對象的地址中的前四個字節內容,保存的是就是虛函數表地址
OK,我們繼續,在dump框中ctrl+G輸入00402198,跳到如下界面
前面說了,00402198是虛函數表的地址,那么我們現在看到的就是虛函數表的內容,很清楚了吧,三個,
那么這三個地址,我們可以歸納出為00401000/00401030/00401060,那么我們在上面的反匯編窗口找下這三個地址對應的位置吧:
看到用紅色框標出的吧,三者的符號文件顯示分別在test.cpp的9/14/19行,分別對應base::f,base::g,base::h
所以我們得到這樣的結論,對象的地址的內容中前四個字節保存了虛函數表的地址,虛函數表地址的內容中依次保存了虛函數地址
?
下面繼續看看
一般繼承(無虛函數覆蓋)
先貼代碼:
[cpp]?view plaincopy
#include?<iostream>?? using?namespace?std;?? ?? ?? class?Base?? {?? public:?? ????virtual?void?f()?? ????{?? ????????cout<<"Base::f"<<endl;?? ????}?? ?? ????virtual?void?g()?? ????{?? ????????cout<<"Base::g"<<endl;?? ????}?? ?? ????virtual?void?h()?? ????{?? ????????cout<<"Base::h"<<endl;?? ????}?? };?? ?? class?Derive:?public?Base?? {?? public:?? ????virtual?void?f1()?? ????{?? ????????cout<<"Derive::f1"<<endl;?? ????}?? ?? ????virtual?void?g1()?? ????{?? ????????cout<<"Derive::g1"<<endl;?? ????}?? ?? ????virtual?void?h1()?? ????{?? ????????cout<<"Derive::h1"<<endl;?? ????}?? };?? ?? int?main()?? {?? ????Base?b;??? ????cout?<<?"Base虛函數表地址:"?<<?(int*)*(int*)(&b)?<<?endl;??? ????Derive?d;?? ????cout?<<?"Derive虛函數表地址:"?<<?(int*)*(int*)(&d)?<<?endl;??? ?? ????return?0;?? }??
過程不重復了,OD貼個圖:
可以看出Base的虛表還是沒變,而Derive的虛函數表對應于下圖:
我們可以看到下面幾點:
1)虛函數按照其聲明順序放于表中。
2)父類的虛函數在子類的虛函數前面。
一般繼承(有虛函數覆蓋)
覆蓋父類的虛函數是很顯然的事情,不然,虛函數就變得毫無意義。下面,我們來看一下,如果子類中有虛函數重載了父類的虛函數,會是一個什么樣子?假設,我們有下面這樣的一個繼承關系。
為了讓大家看到被繼承過后的效果,在這個類的設計中,我只覆蓋了父類的一個函數:f()
貼代碼:
[cpp]?view plaincopy
#include?<iostream>?? using?namespace?std;?? ?? ?? class?Base?? {?? public:?? ????virtual?void?f()?? ????{?? ????????cout<<"Base::f"<<endl;?? ????}?? ?? ????virtual?void?g()?? ????{?? ????????cout<<"Base::g"<<endl;?? ????}?? ?? ????virtual?void?h()?? ????{?? ????????cout<<"Base::h"<<endl;?? ????}?? };?? ?? class?Derive:?public?Base?? {?? public:?? ????virtual?void?f()?? ????{?? ????????cout<<"Derive::f"<<endl;?? ????}?? ?? ????virtual?void?g1()?? ????{?? ????????cout<<"Derive::g1"<<endl;?? ????}?? ?? ????virtual?void?h1()?? ????{?? ????????cout<<"Derive::h1"<<endl;?? ????}?? };?? ?? int?main()?? {?? ????Base?b;??? ????cout?<<?"Base虛函數表地址:"?<<?(int*)*(int*)(&b)?<<?endl;??? ????Derive?d;?? ????cout?<<?"Derive虛函數表地址:"?<<?(int*)*(int*)(&d)?<<?endl;??? ?? ????return?0;?? }??
過程不重復了,OD結合貼個圖:
可以看出Base的虛表還是沒變,而Derive的虛函數表第一個是00401090,繼續OD看下:
test.cpp第28行對應Derive:f函數,所以
Derive的虛函數表如下:
我們從表中可以看到下面幾點,
1)覆蓋的f()函數被放到了虛表中原來父類虛函數的位置。
2)沒有被覆蓋的函數依舊。
再改寫下main:
[cpp]?view plaincopy
int?main()?? {?? ?????? ?????? ?????? ?????? ????Base?*b?=?new?Derive;?? ????cout?<<?"Derive虛函數表地址:"?<<?(int*)*(int*)(b)?<<?endl;??? ????return?0;?? }??
繼續OD查看:
這樣,我們就可以看到對于下面這樣的程序,
Base *b = new Derive();
b->f();
由b所指的內存中的虛函數表的f()的位置已經被Derive::f()函數地址所取代,于是在實際調用發生時,是Derive::f()被調用了。這就實現了多態。
多重繼承(無虛函數覆蓋)
下面,再讓我們來看看多重繼承中的情況,假設有下面這樣一個類的繼承關系。注意:子類并沒有覆蓋父類的函數。
貼個代碼:
[cpp]?view plaincopy
#include?<iostream>?? using?namespace?std;?? ?? ?? class?Base1?? {?? public:?? ????virtual?void?f()?? ????{?? ????????cout<<"Base1::f"<<endl;?? ????}?? ?? ????virtual?void?g()?? ????{?? ????????cout<<"Base1::g"<<endl;?? ????}?? ?? ????virtual?void?h()?? ????{?? ????????cout<<"Base1::h"<<endl;?? ????}?? };?? ?? class?Base2?? {?? public:?? ????virtual?void?f()?? ????{?? ????????cout<<"Base2::f"<<endl;?? ????}?? ?? ????virtual?void?g()?? ????{?? ????????cout<<"Base2::g"<<endl;?? ????}?? ?? ????virtual?void?h()?? ????{?? ????????cout<<"Base2::h"<<endl;?? ????}?? };?? ?? class?Base3?? {?? public:?? ????virtual?void?f()?? ????{?? ????????cout<<"Base3::f"<<endl;?? ????}?? ?? ????virtual?void?g()?? ????{?? ????????cout<<"Base3::g"<<endl;?? ????}?? ?? ????virtual?void?h()?? ????{?? ????????cout<<"Base3::h"<<endl;?? ????}?? };?? ?? class?Derive:?public?Base1,?? ??????????????public?Base2,?? ??????????????public?Base3?? {?? public:?? ????virtual?void?f1()?? ????{?? ????????cout<<"Derive::f"<<endl;?? ????}?? ?? ????virtual?void?g1()?? ????{?? ????????cout<<"Derive::g1"<<endl;?? ????}?? ?? };?? ?? int?main()?? {?? ????Derive?*p?=?new?Derive;?? ????cout?<<?"Derive對象的指針地址:"?<<p<<endl;?? ????cout?<<?"強制轉換成Base1的指針地址:"<<(Base1*)p<<endl;?? ????cout?<<?"強制轉換成Base2的指針地址:"<<(Base2*)p<<endl;?? ????cout?<<?"強制轉換成Base3的指針地址:"<<(Base3*)p<<endl;?? ?? ????return?0;?? }??
這時會生成三個虛指針表:
用OD查看下:對于子類實例中的虛函數表,是下面這個樣子:
我們可以看到,指針地址的內容依次保存了三個虛函數表的地址,而強制轉換成Base1/Base2/Base3地址和這三個虛函數表剛好相對應
我們再看下這三個虛函數表地址分別對應的內容:
?
可以用下圖來表示:
?
我們可以看到:
1) 每個父類都有自己的虛表。
2) 子類的成員函數被放到了第一個父類的表中。(所謂的第一個父類是按照聲明順序來判斷的)
這樣做就是為了解決不同的父類類型的指針指向同一個子類實例,而能夠調用到實際的函數。
多重繼承(有虛函數覆蓋)
下圖中,我們在子類中覆蓋了父類的f()函數。
?
下面是對于子類實例中的虛函數表的圖:
不具體分析了!
我們可以看見,三個父類虛函數表中的f()的位置被替換成了子類的函數指針。這樣,我們就可以任一靜態類型的父類來指向子類,并調用子類的f()了。如:
Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()
安全性
每次寫C++的文章,總免不了要批判一下C++。這篇文章也不例外。通過上面的講述,相信我們對虛函數表有一個比較細致的了解了。水可載舟,亦可覆舟。下面,讓我們來看看我們可以用虛函數表來干點什么壞事吧。
一、通過父類型的指針訪問子類自己的虛函數
我們知道,子類沒有重載父類的虛函數是一件毫無意義的事情。因為多態也是要基于函數重載的。雖然在上面的圖中我們可以看到Base1的虛表中有Derive的虛函數,但我們根本不可能使用下面的語句來調用子類的自有虛函數:
Base1 *b1 = new Derive();
b1->f1(); //編譯出錯
任何妄圖使用父類指針想調用子類中的未覆蓋父類的成員函數的行為都會被編譯器視為非法,所以,這樣的程序根本無法編譯通過。但在運行時,我們可以通過指針的方式訪問虛函數表來達到違反C++語義的行為。(關于這方面的嘗試,其實我們很容易從上面的OD看到的虛函數表發現,只要我們能確定這個函數在虛函數表中的地址,我們就能很easy的調用到它)
二、訪問non-public的虛函數
另外,如果父類的虛函數是private或是protected的,但這些非public的虛函數同樣會存在于虛函數表中,所以,我們同樣可以使用訪問虛函數表的方式來訪問這些non-public的虛函數,這是很容易做到的。
如:
class Base {
private:
virtual void f() { cout << "Base::f" << endl; }
};
class Derive : public Base{
};
typedef void(*Fun)(void);
void main() {
Derive d;
Fun pFun = (Fun)*((int*)*(int*)(&d)+0);
pFun();
}
總結
以上是生活随笔為你收集整理的2.OD-C++的虚函数表遍历的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。