C及C++的回调函数概念及应用

本文介绍了C/C++中的回调函数,详细阐述了回调函数的概念,函数指针的使用,以及在C++中如何处理成员函数作为回调。回调函数可以是普通函数或成员函数,可以通过函数指针、std::function和std::bind来实现。文中给出了各种场景下的回调函数应用,讨论了何时使用回调函数及其优缺点。

1.C语言中回调函数的概念

1.1回调函数

在计算机程序设计中,回调函数,或简称回调(Callback 即call then back 被主函数调用运算后会返回主函数),是指通过参数将函数传递到其它代码的,某一块可执行代码的引用。这一设计允许了底层代码调用在高层定义的子程序。以sort()排序函数为例。
回调函数
要实现回调函数,最关键的一点是要将函数的指针传递给一个函数(上图中是库函数),然后这个函数就可以通过这个指针来调用回调函数了。

1.2 函数指针

函数指针也是一种指针,它指向的是函数。在C中,每个函数在编译后都是存储在内存中,并且每个函数都要一个入口地址,根据这个地址,我们就可以访问并使用这个函数。函数指针就是通过指向这个函数的入口,从而调用这个函数。

1.2.1 函数指针的使用

  • 函数指针的定义
//method 1
void (*p_func)(int , int ,float) = NULL;
//method 2
typedef void(*tp_func)(int,int,float);
tp_func p_func = NULL;

这两种方式都是定义了一个指向返回值为void类型,参数为(int,int,float)的函数指针。

  • 函数指针的赋值
void (*p_func)(int,int,float) = NULL;
p_func=&func1;
p_func=func2;  //编译器会隐式地将func_2由void()(int,int,float)类型转换成
               //void(*)(int,int,float)

详见StackOverflow

  • 使用函数指针调用函数
    因为函数指针也是指针,因此可以使用常规的带*的方法来调用函数,和函数指针的赋值一样,我们也可以使用两种方法:
int val1 = p_func(1,2,3.0);
int val1 = (*p_func)(1,2,3.0);
  • 将函数指针作为参数传给函数
    函数指针和普通指针一样,我们可以将它作为函数的参数传递给函数,下面我们看看如何实现:
//func_3 将函数指针p_func 作为其形参
void func_3(int a,int b,float c,void(*p_func)(int ,int,float ))
{
	(*p_func)(a,b,c);
}
//func_4 调用func_3
void func_4()
{
	func_3(1,2,3.0,func_1);
	//or func_3(1,2,3.0,&func_1);
}
  • 函数指针作为函数返回类型
    返回类型为函数指针的函数:
void (* func_5(int,int,float))(int,int)
{
	...
}

func_5 以(int,int,float)为参数,其返回类型为void(*)(int,int)。

  • 函数指针数组
void (*func_array_1[5])(int,int,float);

typedef void (*P_func_array)(int,int,float);
P_func_array func_array[5];

2.函数指针及回调函数示例

#include <stdio.h>
#include <stdlib.h>

typedef struct _OP{
	float (*p_add) (float,float);
	float (*p_sub) (float,float);
	float (*p_mul) (float,float);
	float (*p_div) (float,float);
} OP;
float ADD(float a,float b)
{
	return a+b;
}
float SUB(float a,float b)
{
	return a-b;
}
float MUL(float a,float b)
{
	return a*b;
}
float DIV(float a,float b)
{
	return a/b;
}

void init_op(OP *op)
{
	op->p_add = ADD;
	op->p_sub = SUB;
	op->p_mul = MUL;
	op->p_div = DIV;
}

float add_sub_mul_div(float a,float b, (*op_func)(float ,float))
{
	return (*op_func)(a,b);
}
int main(int argc ,char ** argv)
{
	OP *op =(OP *)malloc(sizeof(OP));
	init_op(op);
	//直接使用函数指针调用函数
	printf("ADD = %f,SUB = %f , MUL = %f , DIV =%f \n",(op->p_add)(1.3,2.2),(op->p_sub )
	(1.3,2.2),(op->p_mul )(1.3,2.2),(op->p_div )(1.3,2.2));
	//使用回调
	printf("ADD = %f,SUB = %f , MUL = %f , DIV =%f \n",add_sub_mul_div(1.3,2.2,ADD),
	add_sub_mul_div(1.3,2.2,SUB),
	add_sub_mul_div(1.3,2.2,MUL),
	add_sub_mul_div(1.3,2.2,DIV));
	return 0;
}
	

2. C++回调函数理解

C++中的回调函数是继承自C语言的,因此,只有在与C代码建立接口,或与已有的回调接口打交道时,才使用回调函数。除此情况,C++中应使用虚拟方法或函数对象(functor),而不是回调函数。

如果你不想封装一个C接口来进行回调,那必须通过C++的方式实现回调。

c++可通过函数指针(c的语法)和std::function(c++11)来实现的。
c++ 回调函数的使用场景有下面几种:

2.1 回调函数是普通函数

回调函数的使用场景一般是,一个函数中最后产生一个结果,该函数不再去管这个结果后续的使用,而使用回调函数进行处理。

可以先定义好回调函数的函数指针,一般格式:

返回值 (*指针名) (参数列表)
typedef void (*CaptureCallback)(string);

void capturePic(CaptureCallback callback){
    string t = "a pic";
    callback(t);
}
void renderPic(string t){
    print(t);
}
capturePic(renderPic);

上面就是一个简单的例子,在捕获图片的函数里面使用渲染图片的回调函数。

2.2 回调函数是成员函数

2.2.0 关于C++类成员函数

在c++中,一般需要使用类进行函数的封装。但是在调用一般的类的成员函数时,使用类的成员调用时,需要传递this指针.。
当成员函数不是静态的,虚函数,那么我们有以下结论:

  1. &类名::函数名 获取的是成员函数的实际地址;
  2. 对于函数x来讲obj.x()编译器转化后表现为x(&obj),&obj作为this指针传入;
  3. 无法通过强制类型转换在类成员函数指针与其外形几乎一样的普通函数指针之间进行有效的转换。

所以,要在回调函数中传入一个类的普通成员函数时,this指针无处安放,使得回调函数比较复杂。
一般通过std::function 与 std::bind 来实现

2.2.1 回调函数是普通成员函数

有两种解决办法 :1. 使用函数指针的强制类型转换; 2 .利用c++11的std::function()

2.2.1.1 使用函数指针的强制类型转换
#include <iostream>
#include <string>
#include "Windows.h"
#include "windef.h"
using namespace std;
class MyClass
{
	HANDLE hThread;
	DWORD  threadId;

	void test(void * p)
	{
		cout << "this is func " << endl;
	}
public:
	struct S { int a; int b; } m_s;

	bool startThread()
	{//启动子线程  
	 //typedef LPVOID (*FUNC)(LPVOID);//定义FUNC类型是一个指向函数的指针,该函数参数为void*,返回值为void* 
		union {
			void(__stdcall *FUNC)(void *);
			void(MyClass::*func)(void *);
		} _proc;
		_proc.func = &MyClass::test;//强制转换func()的类型

		hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)_proc.FUNC, &m_s, 0, &threadId); // 创建线程
		std::cout << "hthread : " << hThread << endl;
		return true;
	}
};

int main()
{
	MyClass my_class;
	my_class.startThread();
	//my_class.startThread();

	return 1;

}
2.2.1.2 利用c++11的std::function()std::bind()

在c++面向对象里面,回调函数是成员函数的情况更常见,这样的好处是,一个类A的一个函数生成一个结果之后,可以调用另一个类B的成员函数。而不必类A拥有B的实例。
尤其是在B中有了A的实例的情况下,A中如果再包含B的实例会出现循环引用的问题,这也是可以解决的,但是这种耦合还是容易让逻辑变得混乱。

#include <iostream>
#include <functional>
#include <string>

using namespace std;
typedef std::function<void(string)> CaptureCallback;

class CaptureController {
public:
	CaptureCallback callback;
	CaptureController(CaptureCallback callback) :callback(callback) {};
	void capturePic(CaptureCallback callback) {
		string t = "a pic";
		callback(t);
	}
};

class UI {
public:
	//开始捕捉图像
	void startCapture() {
		CaptureController c(std::bind(&UI::renderPic, this, std::placeholders::_1)); //使用bind返回的函数对象构造CaptureCallback类
		//capturePic中的回调函数实际上是UI类的renderPic()
		c.capturePic(c.callback);
	}

	//渲染图片作为回调函数
	void renderPic(string t) {
		std::cout << "t:" << t;
	}
};

//main.cpp
int main()
{
	UI ui;
	ui.startCapture();
}

上面的例子就很好的说明了为什么需要回调函数,以及回调函数是如何使用的。

在UI的类中已经引用了CaptureController的头文件了,如果不使用回调函数,就必须在CaptureController.h中也引用UI.h 的头文件,这样才能访问到UI里面的 renderPic。这样就会造成循环引用头文件的问题。

2.2.2 std::function 与 std::bind

类的成员函数作为回调函数与普通函数不同,使用的不是函数指针,而是 std::function<返回类型(参数类型...)> 函数名称

typedef std::function<void (string)> CaptureCallback;  

std::bind
我们在传入一个函数作为参数的时候,需要使用 std::bind(oldFunName,arg_list)bind()函数会返回一个新的函数对象,arg_list指的是旧的函数对象 oldFunName的参数列表。
而 arg_list中的 _1、_2才是新的函数对象的参数列表。_1这种被称为占位符。在std::placeholders的命名空间下。

CaptureCallback callback = std::bind(&UI::renderPic,this,_1);
callback("test");

当调用 callback("test"),实际上调用的是 UI对象的成员函数 this.renderPic("test"),所以这里面还需要多一个 this的对象指针。
特别的,std::bind函数返回的新的函数对象的参数数目可以与oldFunName的参数数目不同。

void renderPic(string t,int a,char b);
CaptureCallback callback = std::bind(&UI::renderPic,this,_1,2,'b');
callback("test");

这里调用 callback("test"),实际上是调用了 renderPic("test",2,'b')这个函数。
当然这种回调函数的参数数目与传入的函数参数数目不同的应用场景很少见,可以忽略。

需要注意的是,std::function和 std::bind都是c++11标准才有的语法。
也是从boost这个c++扩展库中拿过来的。

2.2.3 回调函数是静态成员函数

  1. 静态成员函数与普通成员函数的区别

    静态成员函数普通成员函数
    静态成员函数实际上是一个全局函数,不依赖一个类的对象. 而属于类,不创建对象也可调用。由类名(::)(或对象名.)调用普通成员函数依赖一个类的对象,也就是它有一个隐藏的调用参数(this)指针,必须指向一个类的对象。由类对象(加.或指针加->)调用
    静态成员函数不接受隐含的this自变量 ,只能访问类中的静态成员变量;
  2. 示例

class MyClass
{
	static MyClass* CurMy;//存储回调函数调用的对象
	static void* callback(void*);//回调函数
	pthread_t TID;
	void func()
	{
		//子线程执行代码
	}
	
	void setCurMy()
	{//设置当前对象为回调函数调用的对象
		CurMy = this;
	}
public:
	bool startThread()
	{//启动子线程
		setCurMy();
		int ret = pthread_create( &TID , NULL , MyClass::callback , NULL );
		if( ret != 0 )
			return false;
		else
			return true;
	}
};
MyClass* MyClass::CurMy = NULL;
void* MyClass::callback(void*)
{
	CurMy->func();
	return NULL;
}
 
int main()
{
	MyClass a;
	a.startThread();
}

MyClass有了1个静态数据成员CurMy和1个静态成员函数callbackCurMy用来存储一个对象的指针。callback当作回调函数,执行CurMy->func()的代码。每次建立线程前先要调用setCurMy()来让CurMy指向当前自己。
这个方法的好处是封装性得到了很好的保护,MyClass对外只公开一个接口startThread(),子线程代码和回调函数都被设为私有,外界不可见。另外没有占用callback的参数,可以从外界传递参数进来。但每个对象启动子线程前一定要注意先调用setCurMy()让CurMy正确的指向自身,否则将为其它对象开启线程,这样很引发很严重的后果。

为什么如果callback函数是一个类成员函数时,最好令其成为静态成员函数?

  • 原因在于必须舍弃掉类成员函数的隐藏参数this指针
    比如一个callback函数被要求声明为以下形式:
    void CALLBACK function();
    如果这个函数在类ObjClass里面,编译器会为其添加一个this指针,用于指向调用该函数的对象。所以编译出来的代码是这种形式:
    void CALLBACK ObjClass::function(ObjClass* this);
    显然有个this指针,函数参数列表与被要求声明的形式不一致。但是加上static,就表示该类成员函数属于类所有,舍弃掉this指针。
  • 当然,也可像2.2.1 中一样使用std::functionstd::bind

2.3 回调函数既可以是普通函数也可以是成员函数

其实就是第二种方式,使用 std::function可以取代c语言中的函数指针,同时需要注意的是,如果是普通函数传入,则不需要传入 this的对象指针。

2.4 回调函数并不是一定要用

以上面的摄像头捕获图片->渲染图片的例子为例。

有两种方法也可以实现,先捕获图片在渲染界面的顺序:

  • 方法一
void startCapture(){
        string t;
        CaptureController c(); 
        c.capturePic(t);//参数是string的引用
        renderPic(t);
}
void renderPic(string t){
    print(t);
}

通过在 startCapture内部先调用 capturePic的函数,再调renderPic函数处理这个结果也可以。但是大多应用场景是不用线程的。

  • 如果是不同线程还可以用方法二
    renderPic 开一个定时器,定时去取capturePic函数产生的结果,也可以,但是此时renderPic的节奏就和capturePic的节奏不一致。

所以,是否需要使用回调函数,需要根据当前的应用场景去选择。

3

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值