1. 程式人生 > >介紹瞭如何取成員函式的地址以及呼叫該地址:C++

介紹瞭如何取成員函式的地址以及呼叫該地址:C++

摘要:介紹瞭如何取成員函式的地址以及呼叫該地址.

關鍵字:C++成員函式 this指標 呼叫約定

一、成員函式指標的用法

  在C++中,成員函式的指標是個比較特殊的東西。對普通的函式指標來說,可以視為一個地址,在需要的時候可以任意轉換並直接呼叫。但對成員函式來說,常規型別轉換是通不過編譯的,呼叫的時候也必須採用特殊的語法。C++專門為成員指標準備了三個運算子: "::*"用於指標的宣告,而"->*"和".*"用來呼叫指標指向的函式。比如:

   class tt
   {
       public: void foo(int x){ printf("\n %d \n",x); }
   };
   typedef   void  ( tt::* FUNCTYPE)(int );
    FUNCTYPE ptr = tt::foo;  //給一個成員函式指標賦值.
    tt a;
    (a.*ptr)(5);   //呼叫成員函式指標.
    tt *b = new tt;
    (b->*ptr)(6);  //呼叫成員函式指標.


  注意呼叫函式指標時括號的用法,因為 .* 和 ->* 的優先順序比較低,必須把它們和兩邊要結合的元素放到一個括號裡面,否則通不過編譯。不僅如此,更重要的是,無法為成員函式指標進行任何的型別轉換,比如你想將一個成員函式的地址儲存到一個整數中(就是取類成員函式的地址),按照一般的型別轉換方法是辦不到的.下面的程式碼:

    DWORD dwFooAddrPtr= 0;
    dwFooAddrPtr = (DWORD) &tt::foo;  /* Error C2440 */
    dwFooAddrPtr = reinterpret_cast (&tt::foo); /* Error C2440 */

  你得到只是兩個c2440錯誤而已。當然你也無法將成員函式型別轉換為其它任何稍有不同的型別,簡單的說,每個成員函式指標都是一個獨有的型別,無法轉換到任何其它型別。即使兩個類的定義完全相同也不能在其對應成員函式指標之間做轉換。這有點類似於結構體的型別,每個結構體都是唯一的型別,但不同的是,結構體指標的型別是可以強制轉換的。有了這些特殊的用法和嚴格的限制之後,類成員函式的指標實際上是變得沒什麼用了。這就是我們平常基本看不到程式碼裡有"::*", ".*" 和 "->*"的原因。

二、取成員函式的地址

  當然,引用某位大師的話:"在windows中,我們總是有辦法的"。同樣,在C++中,我們也總是有辦法的。這個問題,解決辦法已經存在了多年,並且廣為使用(在MFC中就使用了)。一般有兩個方法,一是使用內嵌的組合語言直接取函式地址,二是使用union型別來逃避C++的型別轉換檢測。兩種方法都是利用了某種機制逃避C++的型別轉換檢測,為什麼C++編譯器乾脆不直接放開這個限制,一切讓程式設計師自己作主呢?當然是有原因的,因為類成員函式和普通函式還是有區別的,允許轉換後,很容易出錯,這個在後面會有詳細的說明。現在先看看取類成員函式地址的兩種方法:

第一種方法:

template 
void GetMemberFuncAddr_VC6(ToType& addr,FromType f)
{
    union 
    {
      FromType _f;
      ToType   _t;
    }ut;
    ut._f = f;
    addr = ut._t;
}


這樣使用:

DWORD dwAddrPtr;

GetMemberFuncAddr_VC6(dwAddrPtr,&tt::foo);

  為什麼使用模版? 呵呵,如果不使用模版,第二個引數該怎麼些,寫成函式指標且不說太繁瑣,關鍵是沒有通用性,每個成員函式都要單獨寫一個轉換函式。

第二種方法:

#define GetMemberFuncAddr_VC8(FuncAddr,FuncType)\
{                                               \
    __asm                                       \
    {                                           \
        mov eax,offset FuncType                 \
    };                                          \
    __asm                                       \
    {                                           \
        mov FuncAddr, eax                       \
    };                                          \
}

這樣使用:

DWORD dwAddrPtr;

GetMemberFuncAddr_VC8(dwAddrPtr,&tt::foo);

  本來是想寫成一個模版函式的,可惜雖然通過了編譯,卻不能正確執行。估計在彙編程式碼中使用模版引數不太管用,用offset取偏移量直接就得0。
  上面的巨集是可以正確執行的,並且還有一個額外的好處,就是可以直接取私有成員函式的地址(大概在asm括號中,編譯器不再檢查程式碼的可訪問性)。不過缺點是它在vc6下是無法通過編譯的,只能在VC8下使用。

三、呼叫成員函式地址

  通過上面兩個方法,我們可以取到成員函式的地址。不過,如果不能通過地址來呼叫成員函式的話,那也還是沒有任何用處。當然,這是可行的。不過在這之前,需要了解關於成員函式的一些知識。
  我們知道,成員函式和普通函式最大的區別就是成員函式包含一個隱藏的引數this指標,用來表明成員函式當前作用在那一個物件例項上。根據呼叫約定(Calling Convention)的不同,成員函式實現this指標的方式也不同。如果使用__thiscall呼叫約定,那麼this指標儲存在暫存器ECX中,VC編譯器預設情況下就是這樣的。如果是__stdcall或__cdecl呼叫約定,this指標將通過棧進行傳遞,且this指標是最後一個被壓入棧的引數,相當於編譯器在函式的引數列表中最左邊增加了一個this引數。
  這裡還有件事不得不提,雖然vc將__thiscall型別作為成員函式的預設型別,但是vc6卻沒有定義__thiscall關鍵字!如果你使用__thiscall來定義一個函式,編譯器報錯:'__thiscall' keyword reserved for future use。

知道這些就好辦了,我們只要根據不同的呼叫約定,準備好this指標,然後象普通函式指標一樣的使用成員函式地址就可以了。

  對__thiscall型別的成員函式(注意,這個是VC的預設型別),我們在呼叫之前加一句: mov ecx, this; 然後就可以呼叫成員函式指標。例如:

class tt 
{
 public:
    void foo(int x,char c,char *s)//沒有指定型別,預設是__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);//定義對應的非成員函式指標型別,注意指定__stdcall.
    tt abc;
    abc.m_a = 123;
    DWORD ptr;
    DWORD This = (DWORD)&abc;
    GetMemberFuncAddr_VC6(ptr,tt::foo); //取成員函式地址.
    FUNCTYPE fnFooPtr  = (FUNCTYPE) ptr;//將函式地址轉化為普通函式的指標. 
    __asm //準備this指標.
    {
        mov ecx, This;
    }
    fnFooPtr(5,'a',"7xyz"); //象普通函式一樣呼叫成員函式的地址.


  對其它型別的成員函式,我們只要申明一個與原成員函式定義完全類似的普通函式指標,但在引數中最左邊加一個void * 引數。程式碼如下:

class tt 
{
public:
    void __stdcall foo(int x,char c,char *s)//成員函式指定了__stdcall呼叫約定.
    {
        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);//注意多了一個void *引數.
    tt abc;
    abc.m_a = 123;
    DWORD ptr;
    GetMemberFuncAddr_VC6(ptr,tt::foo); //取成員函式地址.
    FUNCTYPE fnFooPtr = (FUNCTYPE) ptr;//將函式地址轉化為普通函式的指標. 
    fnFooPtr(&abc,5,'a',"7xyz"); //象普通函式一樣呼叫成員函式的地址,注意第一個引數是this指標.

  每次都定義一個函式型別並且進行一次強制轉化,這個事是比較煩的,能不能將這些操作寫成一個函式,然後每次呼叫是指定函式地址和引數就可以了呢?當然是可以的,並且我已經寫了一個這樣的函式。

//呼叫類成員函式
//callflag:成員函式呼叫約定型別,0--thiscall,非0--其它型別.
//funcaddr:成員函式地址.
//This:類物件的地址.
//count:成員函式引數個數.
//...:成員函式的引數列表.
DWORD CallMemberFunc(int callflag,DWORD funcaddr,void *This,int count,...)
{
      DWORD re;
      if(count>0)//有引數,將引數壓入棧.
      {
           __asm
           {
                 mov  ecx,count;//引數個數,ecx,迴圈計數器.
                 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預設的成員函式呼叫型別.
      {
           __asm mov ecx,This;
      }
      else//__stdcall
      {
           __asm push This;
      }
      __asm//呼叫函式
      {
           call funcaddr;
           mov  re,eax;
      }
      return re;
}

使用這個函式,則上面的兩個呼叫可以這樣寫:

CallMemberFunc(0,ptr1,&abc,3,5,'a',"7xyz");//第一個引數0,表示採用__thiscall呼叫.

CallMemberFunc(1,ptr2,&abc,3,5,'a',"7xyz");//第一個引數1,表示採用非__thiscall呼叫. 

  需要說明的是,CallMemberFunc是有很多限制的,它並不能對所有的情況都產生正確的呼叫序列。原因之一是它假定每個引數都使用了4個位元組的棧空間。這在大多數情況下是正確的,比如引數為指標,char,short,int,long以及對應的無符號型別,這些引數確實都是每一個引數使用了4位元組的棧空間。但是還有很多情況下,引數不使用4字棧空間,比如double,自定義的結構或類.float雖然是佔了4位元組,但編譯器還產生了一些浮點指令,而這些無法在CallMemberFunc被模擬出來,因此對float引數也是不行的。
  總結一下,如果成員函式的引數都是整型相容型別,則可以使用CallMemberFunc呼叫函式地址。如果不是,那就只有按前面的方法,先定義對應的普通函式型別,強制轉化,準備this指標,然後呼叫普通函式指標。

四、進一步的討論

  到目前為止,已經討論瞭如何取成員函式的地址,然後如何使用這個地址。但是還有些重要的情況沒有討論,我們知道成員函式可分為三種:普通成員函式,靜態,虛擬。另外更重要的是,在繼承甚至多繼承下情況如何。

首先看看最簡單的單繼承,非虛擬函式的情況。
 

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中沒有定義函式foo1,它的foo1函式是從tt1中繼承過來的。這種情況下,我們直接取tt2::foo1的地址行會發生什麼?

DWORD tt2_foo1;
tt1 x;
GetMemberFuncAddr_VC6(tt2_foo1,&tt2::foo1);
CallMemberFunc(0,tt2_foo1,&x,0); // tt2::foo1 = tt1::foo1

  執行結果表明,一切正常!當我們寫下tt2::foo1的時候,編譯器知道那實際上是tt1::foo1,因此它會暗中作替換。編譯器(VC6)產生的程式碼如下:

GetMemberFuncAddr_VC6(tt2_foo1,&tt2::foo1); //原始碼.
//VC6編譯器產生的彙編程式碼:
push offset @ILT+235(tt1::foo1) (004010f0) //直接用tt1::foo1 替換 tt2::foo1.
...

再看看稍微複雜些的情況,繼承情況下的虛擬函式。

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"); }
};

現在tt1和tt2都定義了虛擬函式foo3,按C++語法,如果通過指標呼叫foo3,應該發生多型行為。下面的程式碼:

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

  請注意第二行輸出,tt2_foo3取的是&tt2::foo3,但由於傳遞的this指標產生是&x,所以實際上呼叫了tt1::foo3。同樣,第三行輸出,取的是基類的函式地址,但由於實際物件是派生類,最後呼叫了派生類的函式。這說明取得的成員函式地址在虛擬函式的情況下仍然保持了正確的行為。
  你若真的理解了上面所說的,一定會覺得奇怪。取函式地址的時候就得到了一個整數(成員函式地址),為何呼叫的時候卻進了不同的函式? 只要看看彙編程式碼就都清楚了,"原始碼之前,了無祕密"。原始碼: GetMemberFuncAddr_VC6(tt1_foo3,&tt1::foo3); 產生的彙編程式碼如下:

push offset @ILT+90(`vcall') (0040105f)
...

  原來取tt1::foo3地址的時候,並不是真的就將tt1::foo3的地址傳給了函式,而是傳了一個vcall函式的地址。顧名思義,vcall當然是虛擬呼叫的意思。我們找到地址0040105f,看看這個函式到底幹了些什麼。

@ILT+90([email protected][email protected]):
0040105F jmp `vcall' (00401380)

該地址只是ILT的一個項,直接跳轉到真正的vcall函式,00401380。找到00401380,就可以看到vcall的程式碼。

`vcall':
00401380 mov eax,dword ptr [ecx] ;//將this指標視為dword型別,並將指向的內容(物件的首個dword)放入eax.
00401382 jmp dword ptr [eax] ;//跳轉到eax所指向的地址.

  程式碼執行的時候,ecx就是this指標,具體說就是上面物件x或y的地址。而eax就是物件x或y的第一個dword的值。我們知道,對於有虛擬函式的類物件,其物件的首地址處總是一個指標,該指標指向一個虛擬函式的地址表。上面的物件由於只有一個虛擬函式,所以虛擬函式表也只有一項。因此,直接跳轉到eax指向的地址就好。如果有多個虛擬函式,則eax還要加上一個偏移量,以定位到不同的虛擬函式。比如,如果有兩個虛擬函式,則會有兩個vcall程式碼,分別對應不同的虛擬函式,其程式碼大概是下面的樣子:

`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]

編譯器根據取的是哪個虛擬函式的地址,則相應的用對應的vcall地址代替。

  總結一下:用前面方法取得的成員函式地址在虛擬函式的情況下仍然保持正確的行為,是因為編譯器實際上傳遞了對應的vcall地址。而vcall程式碼會根據上下文this指標定位到對應的虛擬函式表,進而呼叫正確的虛擬函式。
  最後,我們看一下多繼承情況。很明顯,現在情況要複雜得多。如果實際試一下,會碰到很多困難。首先,指定成員函式的時候可能會碰到衝突。其次,給定this指標的時候需要經過調整。另外,對虛擬繼承可能還要特別處理。解決所有這些問題已經超出了這篇文章的範圍,並且我想要的成員函式指標是一個真正的指標,而在多繼承的情況下,很多時候成員函式指標已經變成了一個結構體(見參考文獻),這時要正確呼叫該指標就變得格外困難。因此結論是,上面討論的方法並不適用於多繼承的情況,要想在多繼承的情況下直接呼叫成員函式地址,必須手工處理各種調整,沒有簡單的統一方法。

相關推薦

介紹如何成員函式地址以及呼叫地址:C++

摘要:介紹瞭如何取成員函式的地址以及呼叫該地址. 關鍵字:C++成員函式 this指標 呼叫約定 一、成員函式指標的用法   在C++中,成員函式的指標是個比較特殊的東西。對普通的函式指標來說,可以視為一個地址,在需要的時候可以任意轉換並直接呼叫。但對成員函式來說,常規型別

帶引數的函式以及呼叫方法

<!DOCTYPE html><html><head><title>帶引數的函式</title></head><body><script>// 1.函式引數:// 在函式的呼叫中,

python同一物件的方法(或函式)沒有權利呼叫物件的其他方法(或函式)

先做個解釋: 1.這裡說的沒有權利呼叫"個人理解"是相對於沒有引入類概念前函式之間可以互相呼叫 2.但是引入類概念之後 1.類物件的函式之間不能互相呼叫 --- 必須通過類物件呼叫 2.例項物件的方法之間也不能互相呼叫 --- 必須通過例項物件即格式self.fun()呼叫

this 指標的地址--呼叫成員函式的所在物件的起始地址

#include<iostream> using namespace std; class Test { int x; public: Test(int a){ x=a; } void get_this(); }; void Test:: get_this() { co

C++】類的六大預設的成員函式 之 解構函式以及建構函式和解構函式呼叫順序

                                              解構函式 一.解構函式定義               解構函式也是特殊的成員函式,他的作用和建構函式相反。 class Box { public: //建構函式 Box(i

瞭解c++成員函式呼叫以及引用

成員函式指標 我一直有點困惑在c++的類中並不包含成員函式,比如 class A{ public: void a(){cout<<"a";} void b(){} void c(){} }; sizeof(A)=

java 建構函式 成員函式初始化順序 以及多型的建構函式呼叫順序

對於JAVA中類的初始化是一個很基礎的問題,其中的一些問題也是易被學習者所忽略。當在編寫程式碼的時候碰到時,常被這些問題引發的錯誤,感覺莫名其妙。 而且現在許多大公司的面試題,對於這方面的考查也是屢試不爽。不管基於什麼原因,我認為,對於java類中的初始化問

C++類成員函式做引數以及轉換呼叫

最近專案過程中由於程式碼重構,想精簡程式碼的時候發現類的成員函式重構出現了無法賦值給封裝函式作為引數的問題。有一定基礎的coder應該都知道其中有隱含this成員的原因,但在用法上網上資料雖多但沒有一個較為具體的程式範例,於是我寫下了下面一段程式碼,供大家分析參考: #in

Android平臺Camera實時濾鏡實現方法探討(十)--代碼地址以及簡單介紹(20160118更新)

div iss 將在 spa 方法 target 用途 net dsm 簡單做了個相機和圖片編輯模塊,時間原因非常多功能還沒有做。尚有BUG,見諒,將在以後抽時間改動 代碼地址 PS:請點個Star^-^ -----------------------

關於“在內聯彙編中不能呼叫類的成員函式“的誤解

        MSDN的"inline assembly"中明確說明:在__asm塊中只能呼叫未過載的全域性C++函式,不能呼叫過載的全域性C++函式或一個類的成員函式。在VC6.0中,的確不能使用內聯彙編呼叫類的成員函式。但是經本人試驗,在VC2003中可

Python的繼承以及呼叫父類成員:super用法

python子類呼叫父類成員有2種方法,分別是普通方法和super方法 假設Base是基類 class Base(object): def __init__(self): print “Base init” 則普通方法如下 class Lea

如果兩個類希望互相呼叫成員變數或成員函式

如果希望在類A中使用類B的成員變數或成員函式。那麼有兩種方法: 1.類A和類B相互引用 典型例子是MVP,在View中建立Presenter,建立時View將自己傳入 class Activity{ Presenter mPresenter; public Activ

反彙編C++ OOP程式碼 分析建構函式如何被呼叫 以及簡單的C++物件記憶體模型

在今天進行C++程式碼的思考時,產生一個疑問,就是C++類的建構函式是如何被呼叫的 於是就做了一個簡單的實驗來驗證自己的想法。 //main.cpp #include &lt;stdio.h&gt; class People{ private: int i; i

面試題-面向物件-靜態成員變數以及靜態成員函式

問題1:問:在類中,靜態資料成員和普通資料成員有何區別? 答: 1.普通資料成員屬於類的物件,物件被建立後,普通資料成員才會分配記憶體。靜態資料成員屬於整個類,即使沒有建立物件,它也存在。 2.只能

JS----直接呼叫函式與call呼叫的區別 (函式的三種呼叫方式介紹)

直接呼叫 直接呼叫函式是最常見 最普通的方式,直接以函式附加的物件作為呼叫者, 在函式後括號內傳入引數來呼叫函式 例如: window.alert("測試程式碼"); 其中呼叫者如果是window可以省略, 即直接alert("測試程式碼"); 以call() 方法

c++中指向物件的指標為NULL時可以呼叫物件成員函式

問題貌似有點奇怪,指標都為NULL了怎麼還可使用?但其實不是的,可以看以下程式碼: #include <iostream> 2 using namespace std; 3 4 class A 5 { 6 public: 7 void

kotlin 複合函式 函式鏈式呼叫 以及函式

package kotlinall.chapter5 import java.io.OutputStream import java.nio.charset.Charset //複合函式 //求f(g(x))的值 val add5={i:Int->i+

[C++]空的物件指標可以呼叫成員函式

include using namespace std; class A{ public: void func() { cout << "hahaha" << endl; } int m_num = 1; }; int main() { A* ptr = NULL; A obj;

this指標是允許依靠返回該類物件的引用值來連續呼叫該類的成員函式

#include<iostream> using namespace std; class date { int year; int month; public: date() { year=0; month=0; } date& setyear(int y

MFC中非類成員函式呼叫成員函式方法

1、定義對話方塊類物件全域性變數指標 CDialog *g_pDlg,同時在初始化對話方塊時用this指標初始化此全域性變數。 2、在非類成員函式中可以使用g_pDlg->成員函式名或變數名進行呼叫訪問。   假如有類A,類B兩個類。如果想在B中呼叫A的成員函式,該怎麼辦