直接調(diào)用類(lèi)成員函數(shù)地址 摘要:介紹了如何取成員函數(shù)的地址以及調(diào)用該地址. 關(guān)鍵字:C++成員函數(shù) this指針 調(diào)用約定 一、成員函數(shù)指針的用法 在C++中,成員函數(shù)的指針是個(gè)比較特殊的東西。對(duì)普通的函數(shù)指針來(lái)說(shuō),可以視為一個(gè)地址,在需要的時(shí)候可以任意轉(zhuǎn)換并直接調(diào)用。但對(duì)成員函數(shù)來(lái)說(shuō),常規(guī)類(lèi)型轉(zhuǎn)換是通不過(guò)編譯的,調(diào)用的時(shí)候也必須采用特殊的語(yǔ)法。C++專(zhuān)門(mén)為成員指針準(zhǔn)備了三個(gè)運(yùn)算符: "::*"用于指針的聲明,而"->*"和".*"用來(lái)調(diào)用指針指向的函數(shù)。比如: class tt { public: void foo(int x){ printf("\n %d \n",x); } }; typedef void ( tt::* FUNCTYPE)(int ); FUNCTYPE ptr = tt::foo; //給一個(gè)成員函數(shù)指針賦值. tt a; (a.*ptr)(5); //調(diào)用成員函數(shù)指針. tt *b = new tt; (b->*ptr)(6); //調(diào)用成員函數(shù)指針. 注意調(diào)用函數(shù)指針時(shí)括號(hào)的用法,因?yàn)?.* 和 ->* 的優(yōu)先級(jí)比較低,必須把它們和兩邊要結(jié)合的元素放到一個(gè)括號(hào)里面,否則通不過(guò)編譯。不僅如此,更重要的是,無(wú)法為成員函數(shù)指針進(jìn)行任何的類(lèi)型轉(zhuǎn)換,比如你想將一個(gè)成員函數(shù)的地址保存到一個(gè)整數(shù)中(就是取類(lèi)成員函數(shù)的地址),按照一般的類(lèi)型轉(zhuǎn)換方法是辦不到的.下面的代碼: DWORD dwFooAddrPtr= 0; dwFooAddrPtr = (DWORD) &tt::foo; /* Error C2440 */ dwFooAddrPtr = reinterpret_cast你得到只是兩個(gè)c2440錯(cuò)誤而已。當(dāng)然你也無(wú)法將成員函數(shù)類(lèi)型轉(zhuǎn)換為其它任何稍有不同的類(lèi)型,簡(jiǎn)單的說(shuō),每個(gè)成員函數(shù)指針都是一個(gè)獨(dú)有的類(lèi)型,無(wú)法轉(zhuǎn)換到任何其它類(lèi)型。即使兩個(gè)類(lèi)的定義完全相同也不能在其對(duì)應(yīng)成員函數(shù)指針之間做轉(zhuǎn)換。這有點(diǎn)類(lèi)似于結(jié)構(gòu)體的類(lèi)型,每個(gè)結(jié)構(gòu)體都是唯一的類(lèi)型,但不同的是,結(jié)構(gòu)體指針的類(lèi)型是可以強(qiáng)制轉(zhuǎn)換的。 有了這些特殊的用法和嚴(yán)格的限制之后,類(lèi)成員函數(shù)的指針實(shí)際上是變得沒(méi)什么用了。這就是我們平常基本看不到代碼里有"::*", ".*" 和 "->*"的原因。
二、取成員函數(shù)的地址 當(dāng)然,引用某位大師的話(huà):"在windows中,我們總是有辦法的"。同樣,在C++中,我們也總是有辦法的。這個(gè)問(wèn)題,解決辦法已經(jīng)存在了多年,并且廣為使用(在MFC中就使用了)。一般有兩個(gè)方法,一是使用內(nèi)嵌的匯編語(yǔ)言直接取函數(shù)地址,二是使用union類(lèi)型來(lái)逃避C++的類(lèi)型轉(zhuǎn)換檢測(cè)。兩種方法都是利用了某種機(jī)制逃避C++的類(lèi)型轉(zhuǎn)換檢測(cè),為什么C++編譯器干脆不直接放開(kāi)這個(gè)限制,一切讓程序員自己作主呢?當(dāng)然是有原因的,因?yàn)轭?lèi)成員函數(shù)和普通函數(shù)還是有區(qū)別的,允許轉(zhuǎn)換后,很容易出錯(cuò),這個(gè)在后面會(huì)有詳細(xì)的說(shuō)明?,F(xiàn)在先看看取類(lèi)成員函數(shù)地址的兩種方法: template 這樣使用: DWORD dwAddrPtr; GetMemberFuncAddr_VC6(dwAddrPtr,&tt::foo); 為什么使用模版? 呵呵,如果不使用模版,第二個(gè)參數(shù)該怎么些,寫(xiě)成函數(shù)指針且不說(shuō)太繁瑣,關(guān)鍵是沒(méi)有通用性,每個(gè)成員函數(shù)都要單獨(dú)寫(xiě)一個(gè)轉(zhuǎn)換函數(shù)。 第二種方法: #define GetMemberFuncAddr_VC8(FuncAddr,FuncType){ __asm { mov eax,offset FuncType }; __asm { mov FuncAddr, eax }; }這樣使用: DWORD dwAddrPtr; GetMemberFuncAddr_VC8(dwAddrPtr,&tt::foo); 本來(lái)是想寫(xiě)成一個(gè)模版函數(shù)的,可惜雖然通過(guò)了編譯,卻不能正確運(yùn)行。估計(jì)在匯編代碼中使用模版參數(shù)不太管用,用offset取偏移量直接就得0。 上面的宏是可以正確運(yùn)行的,并且還有一個(gè)額外的好處,就是可以直接取私有成員函數(shù)的地址(大概在asm括號(hào)中,編譯器不再檢查代碼的可訪(fǎng)問(wèn)性)。不過(guò)缺點(diǎn)是它在vc6下是無(wú)法通過(guò)編譯的,只能在VC8下使用。 三、調(diào)用成員函數(shù)地址
通過(guò)上面兩個(gè)方法,我們可以取到成員函數(shù)的地址。不過(guò),如果不能通過(guò)地址來(lái)調(diào)用成員函數(shù)的話(huà),那也還是沒(méi)有任何用處。當(dāng)然,這是可行的。不過(guò)在這之前,需要了解關(guān)于成員函數(shù)的一些知識(shí)。 知道這些就好辦了,我們只要根據(jù)不同的調(diào)用約定,準(zhǔn)備好this指針,然后象普通函數(shù)指針一樣的使用成員函數(shù)地址就可以了。 class tt { public: void foo(int x,char c,char *s)//沒(méi)有指定類(lèi)型,默認(rèn)是__thiscall. { printf("\n m_a=%d, %d,%c,%s\n",m_a,x,c,s); } int m_a; }; typedef void (__stdcall *FUNCTYPE)(int x,char c,char *s);//定義對(duì)應(yīng)的非成員函數(shù)指針類(lèi)型,注意指定__stdcall. tt abc; abc.m_a = 123; DWORD ptr; DWORD This = (DWORD)&abc; GetMemberFuncAddr_VC6(ptr,tt::foo); //取成員函數(shù)地址. FUNCTYPE fnFooPtr = (FUNCTYPE) ptr;//將函數(shù)地址轉(zhuǎn)化為普通函數(shù)的指針. __asm //準(zhǔn)備this指針. { mov ecx, This; } fnFooPtr(5,'a',"7xyz"); //象普通函數(shù)一樣調(diào)用成員函數(shù)的地址. 對(duì)其它類(lèi)型的成員函數(shù),我們只要申明一個(gè)與原成員函數(shù)定義完全類(lèi)似的普通函數(shù)指針,但在參數(shù)中最左邊加一個(gè)void * 參數(shù)。代碼如下: class tt { public: void __stdcall foo(int x,char c,char *s)//成員函數(shù)指定了__stdcall調(diào)用約定. { printf("\n m_a=%d, %d,%c,%s\n",m_a,x,c,s); } int m_a; }; typedef void (__stdcall *FUNCTYPE)(void *This,int x,char c,char *s);//注意多了一個(gè)void *參數(shù). tt abc; abc.m_a = 123; DWORD ptr; GetMemberFuncAddr_VC6(ptr,tt::foo); //取成員函數(shù)地址. FUNCTYPE fnFooPtr = (FUNCTYPE) ptr;//將函數(shù)地址轉(zhuǎn)化為普通函數(shù)的指針. fnFooPtr(&abc,5,'a',"7xyz"); //象普通函數(shù)一樣調(diào)用成員函數(shù)的地址,注意第一個(gè)參數(shù)是this指針.每次都定義一個(gè)函數(shù)類(lèi)型并且進(jìn)行一次強(qiáng)制轉(zhuǎn)化,這個(gè)事是比較煩的,能不能將這些操作寫(xiě)成一個(gè)函數(shù),然后每次調(diào)用是指定函數(shù)地址和參數(shù)就可以了呢?當(dāng)然是可以的,并且我已經(jīng)寫(xiě)了一個(gè)這樣的函數(shù)。 //調(diào)用類(lèi)成員函數(shù) //callflag:成員函數(shù)調(diào)用約定類(lèi)型,0--thiscall,非0--其它類(lèi)型. //funcaddr:成員函數(shù)地址. //This:類(lèi)對(duì)象的地址. //count:成員函數(shù)參數(shù)個(gè)數(shù). //...:成員函數(shù)的參數(shù)列表. DWORD CallMemberFunc(int callflag,DWORD funcaddr,void *This,int count,...) { DWORD re; if(count>0)//有參數(shù),將參數(shù)壓入棧. { __asm { mov ecx,count;//參數(shù)個(gè)數(shù),ecx,循環(huán)計(jì)數(shù)器. mov edx,ecx; shl edx,2; add edx,0x14; edx = count*4+0x14; next: push dword ptr[ebp+edx]; sub edx,0x4; dec ecx jnz next; } } //處理this指針. if(callflag==0) //__thiscall,vc默認(rèn)的成員函數(shù)調(diào)用類(lèi)型. { __asm mov ecx,This; } else//__stdcall { __asm push This; } __asm//調(diào)用函數(shù) { call funcaddr; mov re,eax; } return re; }使用這個(gè)函數(shù),則上面的兩個(gè)調(diào)用可以這樣寫(xiě): CallMemberFunc(0,ptr1,&abc,3,5,'a',"7xyz");//第一個(gè)參數(shù)0,表示采用__thiscall調(diào)用. CallMemberFunc(1,ptr2,&abc,3,5,'a',"7xyz");//第一個(gè)參數(shù)1,表示采用非__thiscall調(diào)用. 需要說(shuō)明的是,CallMemberFunc是有很多限制的,它并不能對(duì)所有的情況都產(chǎn)生正確的調(diào)用序列。原因之一是它假定每個(gè)參數(shù)都使用了4個(gè)字節(jié)的棧空間。這在大多數(shù)情況下是正確的,比如參數(shù)為指針,char,short,int,long以及對(duì)應(yīng)的無(wú)符號(hào)類(lèi)型,這些參數(shù)確實(shí)都是每一個(gè)參數(shù)使用了4字節(jié)的棧空間。但是還有很多情況下,參數(shù)不使用4字??臻g,比如double,自定義的結(jié)構(gòu)或類(lèi).float雖然是占了4字節(jié),但編譯器還產(chǎn)生了一些浮點(diǎn)指令,而這些無(wú)法在CallMemberFunc被模擬出來(lái),因此對(duì)float參數(shù)也是不行的。 總結(jié)一下,如果成員函數(shù)的參數(shù)都是整型兼容類(lèi)型,則可以使用CallMemberFunc調(diào)用函數(shù)地址。如果不是,那就只有按前面的方法,先定義對(duì)應(yīng)的普通函數(shù)類(lèi)型,強(qiáng)制轉(zhuǎn)化,準(zhǔn)備this指針,然后調(diào)用普通函數(shù)指針。
四、進(jìn)一步的討論 到目前為止,已經(jīng)討論了如何取成員函數(shù)的地址,然后如何使用這個(gè)地址。但是還有些重要的情況沒(méi)有討論,我們知道成員函數(shù)可分為三種:普通成員函數(shù),靜態(tài),虛擬。另外更重要的是,在繼承甚至多繼承下情況如何。 class tt1 { public: void foo1(){ printf("\n hi, i am in tt1::foo1\n"); } }; class tt2 : public tt1 { public: void foo2(){ printf("\n hi, i am in tt2::foo2\n"); } }; 注意,tt2中沒(méi)有定義函數(shù)foo1,它的foo1函數(shù)是從tt1中繼承過(guò)來(lái)的。這種情況下,我們直接取tt2::foo1的地址行會(huì)發(fā)生什么? DWORD tt2_foo1; tt1 x; GetMemberFuncAddr_VC6(tt2_foo1,&tt2::foo1); CallMemberFunc(0,tt2_foo1,&x,0); // tt2::foo1 = tt1::foo1 運(yùn)行結(jié)果表明,一切正常!當(dāng)我們寫(xiě)下tt2::foo1的時(shí)候,編譯器知道那實(shí)際上是tt1::foo1,因此它會(huì)暗中作替換。編譯器(VC6)產(chǎn)生的代碼如下: GetMemberFuncAddr_VC6(tt2_foo1,&tt2::foo1); //源代碼. //VC6編譯器產(chǎn)生的匯編代碼: push offset @ILT+235(tt1::foo1) (004010f0) //直接用tt1::foo1 替換 tt2::foo1. ... 再看看稍微復(fù)雜些的情況,繼承情況下的虛擬函數(shù)。 class tt1 { public: void foo1(){ printf("\n hi, i am in tt1::foo1\n"); } virtual void foo3(){ printf("\n hi, i am in tt1::foo3\n"); } }; class tt2 : public tt1 { public: void foo2(){ printf("\n hi, i am in tt2::foo2\n"); } virtual void foo3(){ printf("\n hi, i am in tt2::foo3\n"); } }; 現(xiàn)在tt1和tt2都定義了虛函數(shù)foo3,按C++語(yǔ)法,如果通過(guò)指針調(diào)用foo3,應(yīng)該發(fā)生多態(tài)行為。下面的代碼: DWORD tt1_foo3,tt2_foo3; GetMemberFuncAddr_VC6(tt1_foo3,&tt1::foo3); GetMemberFuncAddr_VC6(tt2_foo3,&tt2::foo3); tt1 x; tt2 y; CallMemberFunc(0,tt1_foo3,&x,0); // tt1::foo3 CallMemberFunc(0,tt2_foo3,&x,0); // tt2::foo3 CallMemberFunc(0,tt1_foo3,&y,0); // tt1::foo3 CallMemberFunc(0,tt2_foo3,&y,0); // tt2::foo3 輸出如下: hi, i am in tt1::foo3 hi, i am in tt1::foo3 hi, i am in tt2::foo3 hi, i am in tt2::foo3 請(qǐng)注意第二行輸出,tt2_foo3取的是&tt2::foo3,但由于傳遞的this指針產(chǎn)生是&x,所以實(shí)際上調(diào)用了tt1::foo3。同樣,第三行輸出,取的是基類(lèi)的函數(shù)地址,但由于實(shí)際對(duì)象是派生類(lèi),最后調(diào)用了派生類(lèi)的函數(shù)。 這說(shuō)明取得的成員函數(shù)地址在虛擬函數(shù)的情況下仍然保持了正確的行為。 push offset @ILT+90(`vcall') (0040105f) ... 原來(lái)取tt1::foo3地址的時(shí)候,并不是真的就將tt1::foo3的地址傳給了函數(shù),而是傳了一個(gè)vcall函數(shù)的地址。顧名思義,vcall當(dāng)然是虛擬調(diào)用的意思。我們找到地址0040105f,看看這個(gè)函數(shù)到底干了些什么。 @ILT+90(??_9@$BA@AE): 0040105F jmp `vcall' (00401380) 該地址只是ILT的一個(gè)項(xiàng),直接跳轉(zhuǎn)到真正的vcall函數(shù),00401380。找到00401380,就可以看到vcall的代碼。 `vcall': 00401380 mov eax,dword ptr [ecx] ;//將this指針視為dword類(lèi)型,并將指向的內(nèi)容(對(duì)象的首個(gè)dword)放入eax. 00401382 jmp dword ptr [eax] ;//跳轉(zhuǎn)到eax所指向的地址. 代碼執(zhí)行的時(shí)候,ecx就是this指針,具體說(shuō)就是上面對(duì)象x或y的地址。而eax就是對(duì)象x或y的第一個(gè)dword的值。我們知道,對(duì)于有虛擬函數(shù)的類(lèi)對(duì)象,其對(duì)象的首地址處總是一個(gè)指針,該指針指向一個(gè)虛函數(shù)的地址表。上面的對(duì)象由于只有一個(gè)虛函數(shù),所以虛函數(shù)表也只有一項(xiàng)。因此,直接跳轉(zhuǎn)到eax指向的地址就好。如果有多個(gè)虛函數(shù),則eax還要加上一個(gè)偏移量,以定位到不同的虛函數(shù)。比如,如果有兩個(gè)虛函數(shù),則會(huì)有兩個(gè)vcall代碼,分別對(duì)應(yīng)不同的虛函數(shù),其代碼大概是下面的樣子: `vcall': 00401BE0 mov eax,dword ptr [ecx] 00401BE2 jmp dword ptr [eax] `vcall': 00401190 mov eax,dword ptr [ecx] 00401192 jmp dword ptr [eax+4] 編譯器根據(jù)取的是哪個(gè)虛函數(shù)的地址,則相應(yīng)的用對(duì)應(yīng)的vcall地址代替。 總結(jié)一下:用前面方法取得的成員函數(shù)地址在虛擬函數(shù)的情況下仍然保持正確的行為,是因?yàn)榫幾g器實(shí)際上傳遞了對(duì)應(yīng)的vcall地址。而vcall代碼會(huì)根據(jù)上下文this指針定位到對(duì)應(yīng)的虛函數(shù)表,進(jìn)而調(diào)用正確的虛函數(shù)。 |
|