在C++實現回調

在C++實現回調
 來看看怎麼在C++中實現回調吧。
Method1:使用全局函數作爲回調
在C語言中的回調很方便。當然,我們可以在C++中使用類似於C方式的回調函數,也就是將全局函數定義爲回調函數,然後再供我們調用。

typedef void(*pCalledFun)(int *);
void GetCallBack(pCalledFun parafun)
{
/*do something*/
}
如果我們想使用GetCallBack函數,那麼就要實現一個pCalledFun類型的回調函數:
void funCallback(int *iNum)
{
/* do something */
}
然後,就可以直接把funCallback當作一個變量傳遞給GetCallBack,
GetCallBack(funCallback);
 
編譯器可能會有幾種調用規範。比如在Visual C++中,可以在函數類型前加_cdecl,_stdcall來表示其調用規範(默認爲_cdecl)。調用規範影響編譯器產生的給定函數名,參數傳遞的順序(從右到左或從左到右),堆棧清理責任(調用者或者被調用者)以及參數傳遞機制(堆棧,CPU寄存器等)。看看下面的例子:
 

[cpp] view plaincopyprint?
01.#include <iostream>   
02.using namespace std;  
03.  
04.typedef void (__stdcall *pFun)(void);  
05.typedef void (__cdecl *pFunc)(void);  
06.  
07.void __stdcall TextPrint(void)  
08.{  
09.    cout << "Call Back Like Pascal" << endl;  
10.}  
11.  
12.void __cdecl TextPrintc(void)  
13.{  
14.    cout << "Call Back Like C" << endl;  
15.}  
16.  
17.void ForText(pFun pFun1, pFunc pFun2)  
18.{  
19.    pFun1();  
20.    pFun2();  
21.}  
22.  
23.void main(void)  
24.{  
25.    //pFun pP = TextPrint;   
26.    //pFunc pPC = TextPrintc;   
27.    //pP();   
28.    //pPC();   
29.    ForText(TextPrint, TextPrintc);  
30.}  
#include <iostream>
using namespace std;
typedef void (__stdcall *pFun)(void);
typedef void (__cdecl *pFunc)(void);
void __stdcall TextPrint(void)
{
 cout << "Call Back Like Pascal" << endl;
}
void __cdecl TextPrintc(void)
{
 cout << "Call Back Like C" << endl;
}
void ForText(pFun pFun1, pFunc pFun2)
{
 pFun1();
 pFun2();
}
void main(void)
{
 //pFun pP = TextPrint;
 //pFunc pPC = TextPrintc;
 //pP();
 //pPC();
 ForText(TextPrint, TextPrintc);
}
 
 
Method2:使用類的靜態函數作爲回調
既然使用了C++,就不能總是生活在C的陰影中,我們要使用類,類,類!!!
下面我們來使用類的靜態函數作爲回調,爲啥先說靜態函數,因爲靜態函數跟全局函數很類似,函數調用時不會使用this指針,我們可以像用全局函數一樣使用靜態函數。如下:
[cpp] view plaincopyprint?
01.#include <iostream>   
02.using namespace std;  
03.  
04.typedef void  (*pFun)(void);  
05.  
06.class CCallBack  
07.{  
08.public:  
09.    static void TextPrint(void)  
10.    {  
11.        cout << "Static Callback Function of a Class" << endl;  
12.    }  
13.};  
14.  
15.void ForText(pFun pFun1)  
16.{  
17.    pFun1();  
18.}  
19.  
20.void main(void)  
21.{  
22.    ForText(CCallBack::TextPrint);  
23.}  
#include <iostream>
using namespace std;
typedef void  (*pFun)(void);
class CCallBack
{
public:
 static void TextPrint(void)
 {
  cout << "Static Callback Function of a Class" << endl;
 }
};
void ForText(pFun pFun1)
{
 pFun1();
}
void main(void)
{
 ForText(CCallBack::TextPrint);
}
 
當然,我們可以把typedef封裝到類中,加大內聚。
 
[cpp] view plaincopyprint?
01.#include <iostream>   
02.using namespace std;  
03.  
04.class CCallBack  
05.{  
06.public:  
07.    typedef void  (*pFun)(void);  
08.    static void TextPrint(void)  
09.    {  
10.        cout << "Static Callback Function of a Class with funtype" << endl;  
11.    }  
12.};  
13.  
14.void ForText(CCallBack::pFun pFun1)  
15.{  
16.    pFun1();  
17.}  
18.  
19.void main(void)  
20.{  
21.    ForText(CCallBack::TextPrint);  
22.}  
#include <iostream>
using namespace std;
class CCallBack
{
public:
 typedef void  (*pFun)(void);
 static void TextPrint(void)
 {
  cout << "Static Callback Function of a Class with funtype" << endl;
 }
};
void ForText(CCallBack::pFun pFun1)
{
 pFun1();
}
void main(void)
{
 ForText(CCallBack::TextPrint);
}
 
 
 

Method3:使用仿函數作爲回調
上面兩種方法用來用去感覺還是在用C的方式,既然是C++,要面向對象,要有對象!那麼就來看看仿函數吧。所謂仿函數,就是使一個類的使用看上去象一個函數,實質就是在類中重載操作符operator(),這個類就有了類似函數的行爲,就是一個仿函數類了。這樣的好處就是可以用面向對象的考慮方式來設計、維護和管理你的代碼。多的不說,見例子:
 
 
[cpp] view plaincopyprint?
01.#include <iostream>   
02.using namespace std;  
03.  
04.typedef void(*Fun)(void);  
05.  
06.inline void TextFun(void)  
07.{  
08.    cout << "Callback Function" << endl;  
09.}   
10.  
11.class TextFunor  
12.{  
13.public:  
14.    void operator()(void) const  
15.    {  
16.        cout << "Callback Functionor" << endl;  
17.    }   
18.};  
19.  
20.void ForText(Fun pFun, TextFunor cFun)  
21.{  
22.    pFun();  
23.    cFun();  
24.}  
25.  
26.void main(void)  
27.{  
28.    TextFunor cFunor;  
29.    ForText(TextFun, cFunor);  
30.}  
#include <iostream>
using namespace std;
typedef void(*Fun)(void);
inline void TextFun(void)
{
 cout << "Callback Function" << endl;
}
class TextFunor
{
public:
 void operator()(void) const
 {
  cout << "Callback Functionor" << endl;
 } 
};
void ForText(Fun pFun, TextFunor cFun)
{
 pFun();
 cFun();
}
void main(void)
{
 TextFunor cFunor;
 ForText(TextFun, cFunor);
}

援引一點關於仿函數的介紹吧:
仿函數(functor)的優點
我的建議是,如果可以用仿函數實現,那麼你應該用仿函數,而不要用回調。
原因在於:
仿函數可以不帶痕跡地傳遞上下文參數。 而回調技術通常 使用一個額外的 void*參數傳遞。這也是多數人認爲回 調技術醜陋的原因。
更好的性能。 仿函數技術可以獲得更好的性能, 這點直觀來講比較難以理解。 你可能說,回調函數申明爲 inline了,怎麼會性能比仿函數差?我們這裏來分析下。我們假設某個函數 func(例如上面的 std::sort)調用中傳遞了一個回調函數(如上面的 compare),那麼可以分爲兩種情況:
func 是內聯函數,並且比較簡單,func 調用最終被展開了,那麼其中對回調函數的調用也成爲一普通函數調用 (而不是通過函數指針的間接調用),並且如果這個回調函數如果簡單,那麼也可能同時被展開。在這種情形 下,回調函數與仿函數性能相同。
func 是非內聯函數,或者比較複雜而無法展開(例如上面的 std::sort,我們知道它是快速排序,函數因爲存在遞歸而無法展開)。此時回調函數作爲一個函數指針傳 入,其代碼亦無法展開。而仿函數則不同。雖然 func 本 身複雜不能展開,但是 func 函數中對仿函數的調用是編 譯器編譯期間就可以確定並進行 inline 展開的。因此在 這種情形下,仿函數比之於回調函數,有着更好的性能。 並且,這種性能優勢有時是一種無可比擬的優勢(對於 std::sort 就是如此,因爲元素比較的次數非常巨大,是 否可以進行內聯展開導致了一種雪崩效應)。
仿函數(functor)不能做的
話又說回來了,仿函數並不能完全取代回調函數所有的應用場合。例如,我在 std::AutoFreeAlloc 中使用了回調函數,而不是仿函數, 這是因爲 AutoFreeAlloc 要容納異質 的析構函數,而不是隻支持某一種類的析構。這和模板(template)不能處理在同一個容器中 支持異質類型,是一個道理。
 
Method4:使用類的非靜態函數作爲回調(採用模板的方法)
現在纔開始說使用類的非靜態方法作爲回調是這樣的,C++本身並不提供將類的方法作爲回調函數的方案,而C++類的非靜態方法包含一個默認的參數:this指針,這就要求回調時不僅需要函數指針,還需要一個指針指向某個實例體。解決方法有幾種,使用模板和編譯時的實例化及特化就是其中之一,看例子:
[cpp] view plaincopyprint?
01.#include <iostream>   
02.using namespace std;  
03.  
04.template < class Class, typename ReturnType, typename Parameter >  
05.class SingularCallBack  
06.{  
07.public:   
08.    typedef ReturnType (Class::*Method)(Parameter);  
09.  
10.    SingularCallBack(Class* _class_instance, Method _method)  
11.    {  
12.        class_instance = _class_instance;   
13.        method         = _method;  
14.    };  
15.  
16.    ReturnType operator()(Parameter parameter)  
17.    {  
18.        return (class_instance->*method)(parameter);  
19.    };  
20.  
21.    ReturnType execute(Parameter parameter)  
22.    {  
23.        return operator()(parameter);  
24.    };  
25.private:  
26.    Class*  class_instance;  
27.    Method  method;  
28.};  
29.  
30.class CCallBack  
31.{  
32.public:  
33.    int TextPrint(int iNum)  
34.    {  
35.        cout << "Class CallBack Function" << endl;  
36.        return 0;  
37.    };  
38.};  
39.  
40.template < class Class, typename ReturnType, typename Parameter >  
41.void funTest(SingularCallBack<Class, ReturnType, Parameter> tCallBack)  
42.{  
43.    tCallBack(1);  
44.}  
45.  
46.void main(void)  
47.{  
48.    CCallBack callback;  
49.    SingularCallBack<CCallBack, int, int> Test(&callback, callback.TextPrint);  
50.    Test.execute(1);  
51.    Test(1);  
52.    funTest(Test);  
53.}  
#include <iostream>
using namespace std;
template < class Class, typename ReturnType, typename Parameter >
class SingularCallBack
{
public: 
 typedef ReturnType (Class::*Method)(Parameter);
 SingularCallBack(Class* _class_instance, Method _method)
 {
        class_instance = _class_instance; 
  method         = _method;
 };
 ReturnType operator()(Parameter parameter)
 {
        return (class_instance->*method)(parameter);
 };
 ReturnType execute(Parameter parameter)
 {
        return operator()(parameter);
 };
private:
 Class*  class_instance;
 Method  method;
};
class CCallBack
{
public:
 int TextPrint(int iNum)
 {
  cout << "Class CallBack Function" << endl;
  return 0;
 };
};
template < class Class, typename ReturnType, typename Parameter >
void funTest(SingularCallBack<Class, ReturnType, Parameter> tCallBack)
{
 tCallBack(1);
}
void main(void)
{
 CCallBack callback;
 SingularCallBack<CCallBack, int, int> Test(&callback, callback.TextPrint);
 Test.execute(1);
 Test(1);
 funTest(Test);
}
 
 
Method5:使用類的非靜態函數作爲回調(採用thunk的方法1)
所謂thunk,就是替換,改變系統本來的調用意圖,也有的說是用機器碼代替系統調用。
替換原來意圖,轉調我們需要的地址。 網上有段解釋是這樣“巧妙的將數據段的幾個字節的數據設爲特殊的值,然後告訴系統,這幾個字節的數據是代碼(即將一個函數指針指向這幾個字節的第一個字節)”。
爲什麼不能直接使用類的非靜態函數作爲回調函數呢,通俗點的解釋就是類的非靜態函數都要默認傳入一個this指針參數,這就跟我們平時的回調不同了,所以無法使用。
上面提到過,一般的回調函數都是_stdcall或者_cdecl的調用方式,但是成員函數是__thiscall的調用方式。這種調用方式的差別導致不能直接使用類的非靜態成員函數作爲回調函數。看看區別吧:

關鍵字
 堆棧清除
 參數傳遞
 
__stdcall
 被調用者
 將參數倒序壓入堆棧(自右向左)
 
__thiscall
 被調用者
 壓入堆棧,this指針保存在 ECX 寄存器中
 

可見兩者的不同之處就是_thiscall把this指針保存到了ECX的寄存器中,其他都是一樣的。所以我們只需要在調用過程中首先把this指針保存到ECX,然後跳轉到期望的成員函數地址就可以了。代碼如下:

[cpp] view plaincopyprint?
01.#include <tchar.h>   
02.#include <wtypes.h>   
03.#include <iostream>   
04.  
05.using namespace std;   
06.typedef void (*FUNC)(DWORD dwThis);  
07.typedef int (_stdcall *FUNC1)(int a, int b);  
08.  
09.  
10.#pragma pack(push,1)   
11.//先將當前字節對齊值壓入編譯棧棧頂, 然後再將 n 設爲當前值   
12.typedef struct tagTHUNK  
13.{  
14.    BYTE    bMovEcx;     //MOVE ECX Move this point to ECX   
15.    DWORD   dwThis;      // address of this pointer   
16.    BYTE    bJmp;        //jmp   
17.    DWORD   dwRealProc; //proc offset Jump Offset   
18.    void Init(DWORD proc,void* pThis)  
19.    {  
20.        bMovEcx = 0xB9;          
21.        dwThis  = (DWORD)pThis;  
22.        bJmp    = 0xE9;  
23.        dwRealProc = DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(THUNK)));  
24.        //jmp跳轉的是當前指令地址的偏移,也就是成員函數地址與當前指令的地址偏移   
25.        FlushInstructionCache(GetCurrentProcess(),this,sizeof(THUNK));  
26.        // 因爲修改了數據,所以調用FlushInstructionCache,刷新緩存   
27.    }  
28.}THUNK;  
29.// BYTE bMovEcx; DWORD   dwThis; 這兩句連起來就是把this指針保存到了ECX的寄存器   
30.// BYTE bJmp; DWORD   dwRealProc;就是跳轉到成員函數的地址   
31.#pragma pack(pop)    
32.//將編譯棧棧頂的字節對齊值彈出並設爲當前值.   
33.template<typename dst_type, typename src_type>  
34.dst_type pointer_cast(src_type src)  
35.{  
36.    return *static_cast<dst_type*>(static_cast<void*>(&src));  
37.}  
38.  
39.class Test  
40.{  
41.public:  
42.    int      m_nFirst;  
43.    THUNK    m_thunk;  
44.      int      m_nTest;   
45.      
46.    Test() : m_nTest(3),m_nFirst(4)  
47.    {}   
48.    void TestThunk()  
49.    {  
50.        m_thunk.Init(pointer_cast<int>(&Test::Test2),this);  
51.        FUNC1 f = (FUNC1)&m_thunk;  
52.        f(1,2);  
53.        cout << "Test::TestThunk()" << endl;  
54.    }   
55.      
56.    int Test2(int a, int b)  
57.    {  
58.        cout << a << " " << b << " " << m_nFirst << " " << m_nTest << " <<I am in Test2" << endl;  
59.        return 0;  
60.    }  
61.};   
62.  
63.  
64.int main(int argc, _TCHAR* argv[])  
65.{  
66.    Test t;  
67.    t.TestThunk();  
68.    //system("pause");   
69.    return 0;  
70.}  
#include <tchar.h>
#include <wtypes.h>
#include <iostream>
using namespace std; 
typedef void (*FUNC)(DWORD dwThis);
typedef int (_stdcall *FUNC1)(int a, int b);

#pragma pack(push,1)
//先將當前字節對齊值壓入編譯棧棧頂, 然後再將 n 設爲當前值
typedef struct tagTHUNK
{
    BYTE    bMovEcx;     //MOVE ECX Move this point to ECX
    DWORD   dwThis;      // address of this pointer
    BYTE    bJmp;        //jmp
    DWORD   dwRealProc; //proc offset Jump Offset
    void Init(DWORD proc,void* pThis)
    {
        bMovEcx = 0xB9;        
        dwThis  = (DWORD)pThis;
        bJmp    = 0xE9;
        dwRealProc = DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(THUNK)));
        //jmp跳轉的是當前指令地址的偏移,也就是成員函數地址與當前指令的地址偏移
        FlushInstructionCache(GetCurrentProcess(),this,sizeof(THUNK));
        // 因爲修改了數據,所以調用FlushInstructionCache,刷新緩存
    }
}THUNK;
// BYTE bMovEcx; DWORD   dwThis; 這兩句連起來就是把this指針保存到了ECX的寄存器
// BYTE bJmp; DWORD   dwRealProc;就是跳轉到成員函數的地址
#pragma pack(pop) 
//將編譯棧棧頂的字節對齊值彈出並設爲當前值.
template<typename dst_type, typename src_type>
dst_type pointer_cast(src_type src)
{
 return *static_cast<dst_type*>(static_cast<void*>(&src));
}
class Test
{
public:
 int      m_nFirst;
 THUNK    m_thunk;
      int      m_nTest; 
 
    Test() : m_nTest(3),m_nFirst(4)
    {} 
    void TestThunk()
    {
  m_thunk.Init(pointer_cast<int>(&Test::Test2),this);
        FUNC1 f = (FUNC1)&m_thunk;
        f(1,2);
        cout << "Test::TestThunk()" << endl;
    } 
 
 int Test2(int a, int b)
 {
  cout << a << " " << b << " " << m_nFirst << " " << m_nTest << " <<I am in Test2" << endl;
  return 0;
 }
};

int main(int argc, _TCHAR* argv[])
{
    Test t;
    t.TestThunk();
    //system("pause");
    return 0;
}
 
PS:可以看出上面的方法是將代碼寫入數據段,達到了強制跳轉的目的,在這個過程中一定要弄清楚函數調用規則和堆棧的平衡。
在指針轉化中使用了pointer_cast函數,也可以這樣進行:
template<class ToType,  class FromType>
void GetMemberFuncAddr_VC6(ToType& addr,FromType f)
{
    union
    {
        FromType _f;
        ToType   _t;
    }ut;
    ut._f = f;
    addr = ut._t;
}
使用的時候:
    DWORD dPtr;
    GetMemberFuncAddr_VC6(dPtr,Class::Function); //取成員函數地址.
    FUNCTYPE pFunPtr  = (FUNCTYPE) dPtr;//將函數地址轉化爲普通函數的指針
因爲在類的方法默認的調用規則是thiscall,所以上面在進行回調的過程中採用了在ecx中傳入this指針的方法,也就是this指針通過ecx寄存器進行傳遞。注意,在VC6中是沒有__thiscall關鍵字的,如果使用了編譯器會報錯。
發佈了24 篇原創文章 · 獲贊 12 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章