加载中…
个人资料
silex
silex
  • 博客等级:
  • 博客积分:0
  • 博客访问:5,711
  • 关注人气:5
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

C++实现回调

(2023-06-18 09:57:05)
分类: c 编程
参考:https://blog.csdn.net/fly_on_grass/article/details/8102591,研究了其中的方法一到方法五,代码实际进行了测试通过。

# Method1:使用全局函数作为回调(调试通过)

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

```c++
#include
using namespace std;
typedef void(*pCalledFun)(void);      //定义一个函数类型
void GetCallBack(pCalledFun parafun)   //需要调用回调函数的主体,需要将一个函数作为参数传入
{
        parafun();
}
//如果我们想使用 GetCallBack 函数,那么就要实现一个 pCalledFun 类型的回调函数:
void funCallback(void)  //具体的回调函数,参数应该和之前typedef定义的保持一致
{
cout << "funCallback" << endl;
}
//然后,就可以直接把 funCallback 当作一个变量传递给 GetCallBack
int main(void){
        GetCallBack(funCallback); //调用??如何传参数??
```

# Method2:使用类的静态函数作为回调(调试通过)

既然使用了 C++,就不能总是生活在 C 的阴影中,我们要使用类,类,类!!!下面我们来使用类的静态函数作为回调,为啥先说静态函数,因为静态函数跟全局函数很类似,函数调用时不会使用 this 指针,我们可以像用全局函数一样使用静态函数。如下:

```c++
#include
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();
}

int  main(void)
{
 ForText(CCallBack::TextPrint); //调用
 return 0;
}
```

当然,我们可以把 typedef 封装到类中,加大内聚。

总结:以上两个方法就是定义一个函数类型,再定义一个函数把函数原型作为参数传进来,在函数体中调用参数化的函数。一个是全局函数,一个是静态成员函数,本质是一样的。但是不知道怎么才能传参数。

# Method3:使用仿函数作为回调(调试通过)

上面两种方法用来用去感觉还是在用 C 的方式,既然是 C++,要面向对象,要有对象!那么就来看看仿函数吧。所谓仿函数,就是使一个类的使用看上去象一个函数,实质就是在类中重载操作符 operator(),这个类就有了类似函数的行为,就是一个仿函数类了。这样的好处就是可以用面向对象的考虑方式来设计、维护和管理你的代码。多的不说,见例子:

```c++
#include
using namespace std;

typedef void(*Fun)(void);

inline void TextFun(void)
{
 cout << "Callback Function" << endl;
}

class TextFunor
{
public:
 void operator()(void) const //仿函数类,重载(),可以向函数一样使用,const是什么含义?答:我们最好将重载()的函数定义为常函数,这表明我们并不会改变传入的参数,避免一些麻烦
 {
 cout << "Callback Functionor" << endl;
 }
};

void ForText(Fun pFun, TextFunor cFun)
{
 pFun();
 cFun();
}

int main(void)
{
 TextFunor cFunor;
 ForText(TextFun, cFunor); //执行了两个回调函数,TextFun是直接定义了函数,cFunor重载了(),是一个仿函数
 return 0;
}
```

仿函数进一步的介绍见:https://blog.csdn.net/weixin_43723269/article/details/121595303

总结:一般在执行某些步骤时,其中的某一步骤可以灵活的自定义,这时候可以用仿函数做回调函数实现,也就是定义一个重载()的类在形式上当作函数使用。类可以有状态,并且其进一步的好处是传进来的类,可以根据类的不同,实现相应的模板话,这是函数所做不到的(函数没有类型)

援引一点关于仿函数的介绍吧:

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

```c++
#include
using namespace std;

template <<span style="color: #569cd6;">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 tCallBack)
{
 tCallBack(1);
}

int main(void)
{
 CCallBack callback;
 SingularCallBackint, int> Test(&callback, &CCallBack::TextPrint);
 Test.execute(1);
 Test(1);
 funTest(Test);
 return 0;
}
```

总结:更加清晰的写法见https://blog.csdn.net/qq_26973095/article/details/76491751。
基本思想 SingularCallBack 是一个代理类,同时也是模板。Test 是一个根据模板生成的类,在初始化时输入一个带有回调成员函数的类(CCallBack),保存了类的实例和回调成员函数,这相当于既记录了类实例(this),又记录了类本身。然后代理类重载了(),当用仿函数执行或者直接调用执行函数(execute)就会执行回调函数,但上述程序 funTest 好像没有必要

# Method5:使用类的非静态函数作为回调(采用 thunk 的方法 1)

所谓 thunk,就是替换,改变系统本来的调用意图,也有的说是用机器码代替系统调用。
替换原来意图,转调我们需要的地址。 网上有段解释是这样“巧妙的将数据段的几个字节的数据设为特殊的值,然后告诉系统,这几个字节的数据是代码(即将一个函数指针指向这几个字节的第一个字节)”。
为什么不能直接使用类的非静态函数作为回调函数呢,通俗点的解释就是类的非静态函数都要默认传入一个 this 指针参数,这就跟我们平时的回调不同了,所以无法使用。

上面提到过,一般的回调函数都是\_stdcall 或者\_cdecl 的调用方式,但是成员函数是\_\_thiscall 的调用方式。这种调用方式的差别导致不能直接使用类的非静态成员函数作为回调函数。看看区别吧:

关键字

堆栈清除

参数传递

\_\_stdcall

被调用者

将参数倒序压入堆栈(自右向左)

\_\_thiscall

被调用者

压入堆栈,this 指针保存在 ECX 寄存器中

可见两者的不同之处就是\_thiscall 把 this 指针保存到了 ECX 的寄存器中,其他都是一样的。所以我们只需要在调用过程中首先把 this 指针保存到 ECX,然后跳转到期望的成员函数地址就可以了。代码如下:

[cpp:nogutter] view plain copy
#include <</span>tchar.h>  
#include <</span>wtypes.h>  
#include <</span>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<</span>typename dst_type, typename src_type>  
dst_type pointer_cast(src_type src)  
{  
 return *static_cast<</span>dst_type*>(static_cast<</span>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(&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<</span>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 关键字的,如果使用了编译器会报错。

---

# Method6:使用类的非静态函数作为回调(采用 thunk 的方法 2)

在上面的实现过程中,可以看出来主要的部分就是这里:

        bMovEcx = 0xB9;

        dwThis  = (DWORD)pThis;

        bJmp    = 0xE9;

        dwRealProc = DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(THUNK)));

意思就是 Move Ecx pThis; 即把 this 指针保存到了 ECX 的寄存器,JMP dwRealProc;就是跳转到成员函数的地址并进行调用。

对于调用类的非静态方法时中需要传入 this 指针的问题,我们再找一种方法进行解决,上面的类方法使用的是默认的 thiscall 的调用规则,下面我们使用 stdcall 的方式,也就是不使用 ecx 传递 this 指针,而直接使用栈进行 this 指针的传递。见代码:

[cpp:nogutter] view plain copy
#include <</span>iostream>  
#include <</span>windows.h>  
using namespace std;

template<</span>typename ToType, typename FromType>  
void GetMemberFuncAddr_VC6(ToType& addr,FromType f)  
{  
 union  
 {  
 FromType \_f;  
 ToType \_t;  
 }ut;  
 ut.\_f = f;  
 addr = ut.\_t;  
}

class Test  
{  
public:  
 void \_\_stdcall Print(int x,char c,char \*s)  
 {  
 cout << "m_a=" << m_a << "," << x << c << s << endl;  
 }  
 int m_a;  
};

void main(void)  
{  
 typedef void (\_\_stdcall *FUNCTYPE)(void *This,int x,char c,char \*s);

    Test test;
    test.m_a = 111;
    DWORD ptr;
    GetMemberFuncAddr_VC6(ptr,Test::Print);
    FUNCTYPE fnPrintPtr = (FUNCTYPE)ptr;
    fnPrintPtr(&test,3,'Q',"abcde");

}  
这里我们使用 stdcall 的约定定义函数方法,在调用的过程中在函数的参数列表的最左端添入一个 this 指针参数,也就是在栈的最下面,函数返回值之前加入 this 指针,达到可以调用的目的。

Method7:使用类的非静态函数作为回调(采用直接调用虚函数的方法)

其实这种方法跟上面的也类似,规避 this 指针的方法也是采用嵌入汇编的方式,将 this 指针赋值到 ecx 中。不过就是获取函数地址的方法有点不同了,上面都是使用 pointer_cast<</span>int>(&Test::Test2)这种方法,直接转化类的方法,这次我们将类函数的写成虚函数,通过虚函数表获取函数地址,进行调用。

[cpp:nogutter] view plain copy
#include <</span>windows.h>  
#include <</span>iostream>

using namespace std;

class Test {  
public:  
 Test()  
 {  
 m_nFlag = 10;  
 }  
 virtual void f(int nNum)  
 {  
 cout<<"The number is "<<span style="color: #808080;"><</span>nNum<< " ,Flag is " <<m_nFlag << endl;  
 }  
 int m_nFlag;  
};

void main(void)  
{  
 typedef void(\_\_stdcall \*Fun)(int);

    Test test;
    DWORD  pThis = (DWORD)&test;
    int i = 1250;

    //-----------------------------------------
    int** pVtbl = (int**)&test;
    Fun pFun = (Fun) pVtbl[0][0];
    _asm
    {
        mov ecx,pThis;
    }
    pFun(i);
    //-----------------------------------------

    //-----------------------------------------
    pFun = (Fun)*((DWORD*)*(DWORD*)(&test));
    _asm
    {
        mov ecx,pThis;
    }
    pFun(i);
    //-----------------------------------------

}

PS:上面的代码中使用了两种方法获取函数的地址,好好琢磨一下吧~

# Method8:使用类的非静态函数作为回调(采用成员函数指针的方法)

使用成员函数指针,不过这个弄的有点不像回调了,仅供参考吧:

[cpp:nogutter] view plain copy
#include <</span>iostream>  
using namespace std;

class CTest{  
public:  
 CTest(int nNum) : m_nNum(nNum)  
 {  
 }

    int m_nNum;
    void func(int x, char *p) {
        cout << m_nNum << x << p << endl;;
    };

};

typedef void (CTest::_Func)(int, char _);

void CallFun(CTest* pCls, Func pFunc,int x, char* p)  
{  
 (pCls->\*pFunc)(x, p);  
}

int main() {

    Func pFunc;
    pFunc = CTest::func;

    CTest test(5);
    CTest* pClass = &test;

    (pClass->*pFunc)(1, "234");
    CallFun(pClass, pFunc, 2, "abc");

    return 0;

}

# Method9:使用类的非静态函数作为回调(采用 FastDelegate)

很多大牛们早就开始研究这个问题了,解决方法也有很多,FastDelegate 就是一个,可以参看下面的网址:

http://www.codeproject.com/KB/cpp/FastDelegate.aspx

当然中译本也有了,搜一下“成员函数指针与高性能的 C++委托”就可以了,不多说了。

# Method10:使用类的非静态函数作为回调(采用 Tr1::function + bind)

C++ Technical Report 1 (TR1)是 ISO/IEC TR 19768, C++ Library Extensions(函式库扩充)的一般名称。TR1 是一份文件,内容提出了对 C++标准函式库的追加项目。这些追加项目包括了正则表达式、智能指针、哈希表、随机数生成器等。TR1 自己并非标准,他是一份草稿文件。然而他所提出的项目很有可能成为下次的官方标准。这份文件的目标在于「为扩充的 C++标准函式库建立更为广泛的现成实作品」。

C++ tr1 是针对 C++标准库的第一次扩展。即将到来的下一个版本的 C++标准 c++0x 会包括它,以及一些语言本身的扩充。tr1 包括大家期待已久的 smart pointer,正则表达式以及其他一些支持范型编程的东东。草案阶段,新增的类和模板的名字空间是 std::tr1。

这个没怎么研究过,先列到这里,以后再慢慢研究~~

# Method11:使用类的非静态函数作为回调(采用 Boost::Function + bind)

BOOST 库接触的比较少,也不敢多说啥,看到网上写的供给回调的方案有下面几种吧

Boost::Function + bind

Boost::Functor + Signal/Slot

Boost::lamda

大家可以研究一下,这个也以后再细细的说吧~

PS:大家可以看看下面的这个博客,讨论了一下各种方法的速度,没有具体考察过,大家权作参考吧~

http://www.cppblog.com/oldworm/archive/2011/01/30/139610.html

方法太多了,用的场合也多种多样,还要细细的研究啊~

回调,To be, or not to be...

0

阅读 收藏 喜欢 打印举报/Report
  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有