天天看点

在C++实现回调

来看看怎么在C++中实现回调吧。

Method1:使用全局函数作为回调

在C语言中的回调很方便。当然,我们可以在C++中使用类似于C方式的回调函数,也就是将全局函数定义为回调函数,然后再供我们调用。

typedef void(*pCalledFun)(int *);

void GetCallBack(pCalledFun parafun)

{

}

如果我们想使用GetCallBack函数,那么就要实现一个pCalledFun类型的回调函数:

void funCallback(int *iNum)

{

}

然后,就可以直接把funCallback当作一个变量传递给GetCallBack,

GetCallBack(funCallback);

编译器可能会有几种调用规范。比如在Visual C++中,可以在函数类型前加_cdecl,_stdcall来表示其调用规范(默认为_cdecl)。调用规范影响编译器产生的给定函数名,参数传递的顺序(从右到左或从左到右),堆栈清理责任(调用者或者被调用者)以及参数传递机制(堆栈,CPU寄存器等)。看看下面的例子:

#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指针,我们可以像用全局函数一样使用静态函数。如下:

#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封装到类中,加大内聚。

#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(),这个类就有了类似函数的行为,就是一个仿函数类了。这样的好处就是可以用面向对象的考虑方式来设计、维护和管理你的代码。多的不说,见例子:

#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指针,这就要求回调时不仅需要函数指针,还需要一个指针指向某个实例体。解决方法有几种,使用模板和编译时的实例化及特化就是其中之一,看例子:

#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,然后跳转到期望的成员函数地址就可以了。代码如下:

#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关键字的,如果使用了编译器会报错。