一、背景
回调在我们开发的过程种使用的频率非常之高。甚至有这样一种说法,开发者会不会使用回调直接就能看出这个人的开发能力,回调的使用几乎会存在任何的项目中。最近在用C++开发项目刚好要用到回调,等到需要使用的时候才发现和C语言的回调使用方式完全不是一回事(以前主要写C),也是查找了一番资料才慢慢搞懂C++回调的一些常用用法。在此做一个简要的总结记录,避免日后忘记后有个可以翻阅的地方,也和各位码友分享,有不足之处欢迎在评论区批评指正,一起成长!废话不多说,直接进入主题吧。
二、为什么要用回调
在回答为什么要用回调函数之前,我们先弄清楚什么是回调函数。官方解释是这么说的:回调函数就是通过一个函数指针调用的函数。函数实现者并不调用,是等待某一个事件触发的时候才会在另外一个模块被调用。经常我们需要将事件或者业务流程和结果分开处理,这时候就要可以实现传一个函数指针到业务模块中去,等到需要结果的时候业务模块就会调用这个函数指针将结果返回到主模块,这样就实现了一个被动通知的效果,而不需要一直去查询结果。很多的notify机制就是通过函数指针实现的。还有一种需要实现兼容操作的时候也可以回调实现,比如linux系统中的驱动代码框架需要兼容各种厂家和型号的芯片,就是通过回调注册的机制来实现高度抽象分离的,回调函数就是实现各自的芯片驱动逻辑,上层框架就是通过调用函数指针来实现业务逻辑,做到解耦分离。
三、回调的实现方式
清楚了回调函数的定义和为什么需要使用回调机制后,现在就来谈谈回调的实现方式。如果是C语言,回调的实现方式就比较简单,只需要声明一个函数指针,然后将这个函数指针当作形参传过去即可,这里不做详细展开讨论了,有兴趣的小伙伴可以去网上搜一下c语言回调函数的实现方式。但是C++中的回调实现就五花八门,各式各样了,这里整理了几种常见的实现方式。如有不全面或者不妥之处欢迎评论区指出,不胜感激,一起进步。
3-1:std::function和std::bind方式
std::function 是C++11标准中的一种类型,可以通过这个类型来实现C++中高阶函数的概念。std::function的功能非常强大而且灵活,这里只是借用它的功能来实现一个C++中的回调机制。定义std::function 的方式:“std::function 返回类型<参数类型列表> 函数名”,例如要定义一个std::function 名为add,接受两个int型参数,返回一个int值的函数变量。可以按这样定义:std::function int <int, int> add ; 下面我们就用一个小例子来说明std::function的用法。
#include <iostream>
#include <functional>
using namespace std;
int add(int a, int b)
{
return (a+b);
}
int main()
{
std::function<int (int, int)> addFun = add; //定义一个addFun函数变量,并将add这个函数实例赋值给这个变量
int sum = addFun(2,3);
std::cout << "sum=" << sum << std::endl;
return 0;
}
上例中通过std::function这个类型定义了一个函数指针变量addFun, 将add这个函数地址赋值给addFun后,就能在main函数中通过addFun这个变量来调用add这个函数。大家可以思考一下,是不是和c语言中的函数指针如出一辙,只是定义的方式不太一样而已。如果我们将这个在一个函数的形参中定义一个这样的函数指针,是不是就能实现基本的回调了,接下来我们就将上面的例子做一个简单的修改,论证下回调函数的流程。代码如下:
#include <iostream>
#include <functional>
using namespace std;
int add(int a, int b)
{
return (a+b);
}
int MyAddCallBackFun(int a, int b, std::function<int (int, int)> addFun)
{
return addFun(a, b);//回调 callback
}
int main()
{
int sum = MyAddCallBackFun(2, 3, add);//将add函数作形参
std::cout << "sum=" << sum << std::endl;
return 0;
}
从上面的例子我们可以很清晰的看到 MyAddCallBackFun是一个普通函数,但是它的形参并不普通,接受了两个int类型的变量a和b, 以及一个std::function声明的函数变量addFun,在实现体中调用addFun来真正执行加法的实现并返回结果。然后这个addFun变量是在调用之前就设置好的,到需要的时候才会返回调用,这就是回调机制。这样我们就通过一个简单的实例通过std::function 类型来实现了回调。细心的人就会发现了,这个和C语言的回调方式不是一摸一样吗,对的,没错,这只是针对普通函数的回调实现,和C语言中传统的回调没什么区别。但是C++中有类,那C++中的类成员函数能否像普通函数一样这么简单直接的实现回调呢,答案当然是否啦。因为啊c语言中每一个函数都是静态分配好空间的,所以函数的地址都是固定不变的,就可简单的通过传递函数地址的方式来实现回调,但是在C++中类的普通函数的地址是不固定的,类普通成员函数的地址是类实例中的一个偏移,简单来说,必须先定义一个类对象,对象有了地址,对象中的成员才会有地址。(但是有个例外,就是类中的静态成员函数也是固定内存地址的,可以像普通函数一样来实现回调)。
接下来我们就通过std::function来实现下c++类普通成员函数的回调机制。
#include <iostream>
#include <functional>
using namespace std;
using CallFunType = std::function <int (int, int)>; //重命名,类似于c中的typedef
class CcallBack{
public:
CcallBack(){};
~CcallBack(){};
void setCalBackFn(const CallFunType &fn){
m_addFn = fn;
};
int Add(int a, int b){
cout << "Call Add Function" << endl;
return m_addFn(a, b);
};
private:
CallFunType m_addFn; //声明一个函数指针
};
class CdoWoker{
public:
CdoWoker(){};
~CdoWoker(){};
public:
int doAddCallBackFun(int a, int b) //加法函数的执行体
{
cout << "doAddCallBackFun do!" << endl;
return (a+b);
};
};
int main(void)
{
CcallBack callObj; //回调对象
CdoWoker doObj; //执行对象
CallFunType fn = std::bind(&CdoWoker::doAddCallBackFun, &doObj, std::placeholders::_1,std::placeholders::_2);
callObj.setCalBackFn(fn);
int sum = callObj.Add(2,3);
cout<<"sum = " << sum <<endl;
return 0;
}
通过上面的实例我们会发现,通过了一个std::bind函数来将类实例和类成员函数进行一个绑定,然后返回一个std::function 类型的函数指针变量,这个变量就是一个执行类实例成员函数的绝对地址,此时就能像C中一样来实现回调了。以上局势通过std::function和std::bind方式来实现回调的基本操作。也是C++中实现回调机制非常常用的一种方式。
3-2:C++虚函数实现回调
这个是最符合C++特性的回调机制,在一个基类中定义个纯虚函数,然后在继承它的派生类中去实现这个纯虚函数,基类的指针可以指向派生类,这样子就能实现在基类中调用派生类的虚函数,从而实现了回调的功能。先来实现一个简单的实例,让我们有一个
直观的认知,代码如下:
#include <stdio.h>
#include <iostream>
using namespace std;
//使用基类的指针或者引用来指向派生类的对象,然后通过该指针或者引用调用派生类的方法
class Shape{ //定义一个基类
public:
virtual float getShapeArea() = 0; //定义一个获取面积的纯虚函数
};
//定义一个圆形派生类
class Circle : public Shape{
public:
float getShapeArea(){
return 3.14*m_r*m_r;
};
private:
int m_r = 2;
};
//定义一个长方形派生类
class Rectangle: public Shape{
public:
float getShapeArea(){
return m_long*m_wide;
};
private:
float m_long = 2;
float m_wide = 5;
};
//定义一个三角形派生类
class Angle : public Shape{
public:
float getShapeArea(){
return (m_button*m_higt)/2;
};
private:
float m_button = 3;
float m_higt = 3;
};
float GetAreaFun(Shape * shapePtr)
{
return shapePtr->getShapeArea();
}
int main()
{
Circle * circeIns = new Circle();
Rectangle * rectIns = new Rectangle();
Angle * angleIns = new Angle();
cout << "Circle Area:" << GetAreaFun(circeIns) << endl; //获取圆的面积
cout << "Rectangle Area:" << GetAreaFun(rectIns) << endl; //获取长方形的面积
cout << "Angle Area:" << GetAreaFun(angleIns) << endl; //获取三角形的面积
return 0;
}
上面就是通过派生实现一个获取图形面积的功能,首先定义一个基类Shape, 基类只有一个获取面积的纯虚函数,然后定义及三个派生类,分别是实现这个函数。然后在主函数中有一个获取面积的函数,这个函数有一个指向Shape实例的指针,根据实际的需求传不同的类实例就能获取到不同实例的面积。因为基类指针可以指向派生类,所以在调用的时候可以传派生类的对象指针。这样就巧妙的实现了回调的机制。这种方式其实在C++里面不能正式叫做回调,因为C++刻意的在隐藏回调的概念,让使用者逻辑变得更加简单。
3-4: 通过全局结构体实现回调
首先声明一个结构体上下文,结构体成员函数中有一个函数指针,一个对象指针,还有一些用户需要传进来的data。首先在一个类中定义好一个这样的结构体变量,并赋值函数指针以及将自己的实例对象(this)赋值给结构体的对象指针,在new调用者类的时候将这样一个结构体指针传进去。这样调用者类中就能通过这样一个结构体指针来回调到用户类的成员函数了,也就是实现了回调机制。
#include <iostream>
using namespace std;
//声明一个上下文结构体
struct Context{
void (*callBackFn)(char * data, int dataLen, void * user_this);
void * obj; //回调函数所在的对象指针
};
static void g_callback_do(char * data, int dataLen, void * ctx);
class Employee{
public:
explicit Employee(Context * pContext){
m_pContext = pContext;
};
void dowork(char * data, int dataLen){
cout<<"dowork=>data:"<< data << ", " << "dataLen=" << dataLen << endl;
if(m_pContext){
m_pContext->callBackFn(data, dataLen, m_pContext);
}
};
private:
Context * m_pContext;
};
class Manage{
public:
explicit Manage();
~Manage();
void startWork(char* data, int dataLen);
void callBackDoFun(char * data, int dataLen){
cout<<"callBackDoFun=>data:"<< data << ", " << "dataLen=" << dataLen << endl;
};
private:
Context * m_pContext;
Employee * m_pEmpoyee;
};
Manage::Manage()
{
m_pContext = new Context;
m_pContext->callBackFn = g_callback_do;
m_pContext->obj = this;
m_pEmpoyee = new Employee(m_pContext);
}
Manage::~Manage()
{
if(m_pContext){
delete m_pContext;
m_pContext = nullptr;
}
if(m_pEmpoyee){
delete m_pEmpoyee;
m_pEmpoyee = nullptr;
}
}
void Manage::startWork(char * data, int dataLen)
{
cout<<"startWork=>data:"<< data << ", " << "dataLen=" << dataLen << endl;
m_pEmpoyee->dowork(data, dataLen);
}
//全局回调执行函数
static void g_callback_do(char * data, int dataLen, void * ctx)
{
if(!ctx)
return;
Context * pCtx = (Context *)ctx;
Manage * p_obj = (Manage *)pCtx->obj;
p_obj->callBackDoFun(data, dataLen);
}
int main()
{
Manage * pMng = new Manage();
pMng->startWork("CALL BACK TEST", 14);
return 0;
}