C++通過虛擬函式表呼叫虛擬函式
阿新 • • 發佈:2018-11-22
C++的類如果有虛擬函式,則該類的第一個成員的數值,是一個地址,指向其虛擬函式表。例如
class CTest
{
public:
virtual void Test1(void)
{
cout<< "CTest Test1\n";
}
virtual void Test2(void)
{
cout<< "CTest Test2\n";
}
private:
int m_nData;
};
在VC裡CTest的大小為8,m_nData的偏移地址為4。第一個成員為VTable。
通過虛擬函式表,把虛擬函式強制轉換為普通函式,可以呼叫之。例如:
#include<iostream> using namespace std; class CTest { public: virtual void Test1(void) { cout<< "CTest Test1\n"; } virtual void Test2(void) { cout<< "CTest Test2\n"; } private: int m_nData; }; typedef void (*pFun)(void); int main(void) { CTest cc; int* pVtblAddr = (int*)*(int*)(&cc); //這是一個地址,指向CTest的虛擬函式表 int* pFirstVf = (int*)(pVtblAddr[0]); //pVtblAddr相當於一個數組,其中存放的是各個虛擬函式的地址 int* pSecondVf = (int*)(pVtblAddr[1]); pFun pf = (pFun)pSecondVf; pf(); }
執行結果是輸出CTest Test2。
現在給Test2加個引數。
#include<iostream> using namespace std; class CTest { public: virtual void Test1(void) { cout<< "CTest Test1\n"; } virtual void Test2(int i) { cout<< "CTest Test2: i = " << i << "\n"; } private: int m_nData; }; typedef void (*pFun)(int); int main(void) { CTest cc; int* pVtblAddr = (int*)*(int*)(&cc); //這是一個地址,指向CTest的虛擬函式表 int* pFirstVf = (int*)(pVtblAddr[0]); //pVtblAddr相當於一個數組,其中存放的是各個虛擬函式的地址 int* pSecondVf = (int*)(pVtblAddr[1]); pFun pf = (pFun)pSecondVf; pf(100); }
執行時可以正確列印,但是卻報錯。
原因如下:
呼叫pf前後,反彙編結果為
pf(100);
004015D9 mov esi,esp
004015DB push 64h
004015DD call dword ptr [ebp-20h]
004015E0 add esp,4
如果在call裡F11單步除錯,可以看到最終正確呼叫了CTest::Test2。最後一條反彙編指令為:
virtual void Test2(int i)
{
004016C0 push ebp
004016C1 mov ebp,esp
……………………
004016CB mov dword ptr [ebp-4],ecx
cout<< "CTest Test2: i = " << i << "\n";
……………………
}
……………………
00401703 mov esp,ebp
00401705 pop ebp
00401706 ret 4
在pf(100)那裡,呼叫結束後有個add dsp, 4。而實際呼叫Test2的時候,最後又有一個ret 4。
所以相當於,壓棧時只壓入了一個引數100,但卻有兩次出棧操作,最後造成了棧不平衡了。
根本原因在於,Test2的呼叫約定是__thiscall,這樣的函式在本身被呼叫結束後退出時會自動
清除棧;而pf的呼叫約定是_cdecl,main裡呼叫了pf之後,會又有一次清除棧的操作。
解決辦法就是修改pf的呼叫約定。但普通函式是不能用_thiscall宣告的,所以只能退而求其次
,把它宣告為__stdcall的。
typedef void (__stdcall *pFun)(int);
這樣就不會調用出錯了。