C++ typeid 和虛函數詳解
typeid 和虛函數
前面咱們講到 typeid 的操作返回值是 type_info 對象的引用,然後輸出返回值的地址是相同的,測試代碼如下:
#include <iostream> #include <functional> using namespace std; class Base{ public: virtual void test(){ cout << "Base::test" << endl; } }; class Derived : public Base{ public: void test(){ cout << "Derived::test" << endl; } virtual ~Derived(){ cout << "Derived::~Derived" << endl; } }; int main() { Base* pBase = new Base(); Base* pBase2 = new Derived(); Derived* pDerive = new Derived(); //typeid(pBase2) 和 typeid(pDerive) 返回地址相同 cout << "typeid(pBase2) = " << &typeid(*pBase2) << " typeid(pDerive) = "<< &typeid(*pDerive) << endl; return 0; }
output信息:
typeid(pBase2) = 0x55dd724c6d48 typeid(pDerive) = 0x55dd724c6d48
也就是說,0x55dd724c6d48 就是 Derived 類編譯之後的類標識(type_info)數據信息!是否真的如此,咱們可以添加一下代碼測試:
int main() { Base* pBase = new Base(); Base* pBase2 = new Derived(); Derived* pDerive = new Derived(); //typeid(pBase2) 和 typeid(pDerive) 返回地址相同 cout << "typeid(pBase2) = " << &typeid(*pBase2) << " typeid(pDerive) = "<< &typeid(*pDerive) << endl; //class Base type_info 地址 cout << "typeid(Base) = " << &typeid(Base) << endl; //class Derive type_info 地址 cout << "typeid(Derived) = " << &typeid(Derived) << endl; //指針類型推導 cout << "point ---- typeid(pBase2) = " << &typeid(pBase2) << " typeid(pDerive) = "<< &typeid(pDerive) << endl; return 0; }
ouput信息:
typeid(pBase2) = 0x562309345d48 typeid(pDerive) = 0x562309345d48
typeid(Base) = 0x562309345d60
typeid(Derived) = 0x562309345d48
point —- typeid(pBase2) = 0x562309345d28 typeid(pDerive) = 0x562309345d08
可以看到,Derived 類的 type_info 信息的地址就是 0x558a4dec7d48 !要註意的一點:直接對指針類型進行操作,並不能返回正確的原始類型。
好瞭嘛,那 typeid 到底是咋從虛函數表找到這個地址的呢?如果大傢看過我之前的 深入理解new[]和delete[]_master-計算機科學專欄-CSDN博客 一文,應該就能夠想到是不是C++編譯器對虛函數表進行構造的過程中是不是也一樣,做瞭地址偏移呢?
咱們看看上面代碼的匯編信息:
通過查看匯編信息,我們得到以下結論:
虛函數表中確實存有typeinfo信息(第一個虛函數的地址偏移 -1 即是)typeinfo信息是區分指針類型是的(指針類型有前綴P,例如 P4Base、P7Derived)
然後,我們仔細觀察四個 typeinfo 類(Derived*、 Base*、Derived、Base),每個typeinfo 類都有一個虛函數表,繼承自 vtable for __cxxabiv1::******* ,後面的信息會不一樣。這裡對該信息做一下簡單說明:
對於啟用瞭 RTTI 的類來說會繼承 __cxxabiv1 裡的某個類所有的基礎類(沒有父類的類)都繼承於_class_type_info所有的基礎類指針都繼承自 __pointer_type_info所有的單一繼承類都繼承自 __si_class_type_info所有的多繼承類都繼承自 __vmi_class_type_info
以typeinfo for Derived為例:
然後是指向存儲類型名字的指針,
如果有繼承關系,則最後是指向父類的 typeinfo 的記錄。
所以,如果是正常調 typeinfo 基類(_class_type_info、__pointer_type_info、__si_class_type_info、__vmi_class_type_info)的方法,應該會動態調到 type_info 的繼承類 (typeinfo for Derived*、typeinfo for Base*、typeinfo for Derived、typeinfo for Base)的方法。
但是,typeid 操作指針類型時並不是這樣,說明C++編譯器底層有特殊處理!
調試以下代碼:
cout << typeid(*pBase2).name(); cout << typeid(*pDerive).name(); cout << typeid(pBase2).name(); cout << typeid(pDerive).name();
通過匯編信息,可以看到這裡並沒有做任何動態調用的邏輯,而是直接返回該指針類型的typeinfo信息,這也就解釋瞭為什麼 typeid 操作指針和操作對象的結果不一樣!
那麼我們在使用typeid時,如果要獲取到真實對象類型,應該要將指針去掉!
為瞭驗證我們前面的結論: 虛函數表中確實存有typeinfo信息(第一個虛函數的地址偏移 -1 即是),咱們可以直接通過指針的方式操作虛函數表!
測試代碼如下:
#include <iostream> #include <functional> using namespace std; class Base{ public: virtual void test(){ cout << "Base::test" << endl; } }; class Derived : public Base{ public: void test(){ cout << "Derived::test" << endl; } virtual ~Derived(){ cout << "Derived::~Derived" << endl; } }; typedef void (*FUNPTR)(); type_info* getTypeInfo(unsigned long ** vtbl){ type_info* typeinfo = (type_info*)((unsigned long)vtbl[-1]); return typeinfo; } void visitVtbl(unsigned long ** vtbl, int count) { cout << vtbl << endl; cout << "\t[-1]: " << (unsigned long)vtbl[-1] << endl; typedef void (*FUNPTR)(); for (int i = 0; vtbl[i] && i < count; ++i) { cout << "\t[" << i << "]: " << vtbl[i] << " -> "; FUNPTR func = (FUNPTR)vtbl[i]; func(); } } int main() { Base* pBase = new Base(); Base* pBase2 = new Derived(); Derived* pDerive = new Derived(); //這裡去遍歷虛函數表 visitVtbl((unsigned long **)*(unsigned long **)pBase2, 2); //獲取虛函數表-1位置的typeinfo地址 cout << "pDerive = " << getTypeInfo((unsigned long **)*(unsigned long **)pDerive) << " " << getTypeInfo((unsigned long **)*(unsigned long **)pDerive)->name() << endl; //獲取虛函數表-1位置的typeinfo地址 cout << "pBase2 = " << getTypeInfo((unsigned long **)*(unsigned long **)pBase2) << " " << getTypeInfo((unsigned long **)*(unsigned long **)pBase2)->name() << endl; return 0; }
這裡要註意的一點是,遍歷虛函數表 visitVtbl 方法的第2個參數,自己控制不要越界,此外,還要註意調用的順序,如果先調用瞭虛析構函數,會導致內存錯誤!
output信息:
0x5620022edd10
[-1]: 94695475567936
[0]: 0x5620022eb5fa -> Derived::test
[1]: 0x5620022eb636 -> Derived::~Derived
pDerive = 0x5620022edd40 7Derived
pBase2 = 0x5620022edd40 7Derived
通過直接訪問虛函數表-1位置,我們可以看到輸出的日志信息與我們前面的結論是一致的!也即是C++編譯器給我們做瞭偏移操作(在-1的位置存儲瞭type_info信息,實例化對象中的虛函數表地址是偏移之後的地址)。
總結
本篇文章就到這裡瞭,希望能夠給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!
推薦閱讀:
- C++ RTTI與4種類型轉換的深入理解
- 關於C++虛函數與靜態、動態綁定的問題
- 一篇文章徹底弄懂C++虛函數的實現機制
- C++強制類型轉換(static_cast、dynamic_cast、const_cast、reinterpret_cast)
- C++ 數據類型強制轉化的實現