1. 程式人生 > >C++通過虛擬函式表呼叫虛擬函式

C++通過虛擬函式表呼叫虛擬函式

    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);
這樣就不會調用出錯了。