文章目录

  • 1.回调函数的定义
  • 2.回调函数的eg1
  • 3.回调函数的eg2
  • 4.同步调用和异步调用(注册回调函数)
  • 5.回调函数eg3:简单版
  • 5.回调函数eg4:简单版

1.回调函数的定义

  • 如果参数是一个函数指针,则调用者可以传递给一个函数的地址给实现者,实现者去调用它,这称之为回调函数Callback Function。eg:C语言中qsort(3)和bsearch(3)。
  • 一个回调函数eg:void func(void (*f)(void *), void *p);

2.回调函数的eg1

  • 下面的代码实现了一个repeat_three_times函数,可以把调用者传来的任何回调函数连续执行三次(未编译)
/*para_callback.h*/
#ifndef PARA_CALLBACK_H
#define PARA_CALLBACK_H
/*
1.callback_t就是声明一个函数指针,入参类型是void*,callback_t的函数类型也是void*
2.上面这句话不理解的话,就去看c primer plus有关函数指针的章节
*/
typedef void (*callback_t)(void *);
extern void repeat_three_times(callback_t, void *);
/*para_callback.c*/
#include "para_callback.h"
/*
1.实现者是repeat_three_times,调用者是say_hello和count_numbers
2.参数类型都是由实现者规定,对于实现者来说,f就是一个void*的指针,实现者
只负责将这个指针转交给回调函数,而不关心它到底指向什么数据类型
*/
void repeat_three_times(callback_t f, void *para)
{
	f(para);
	f(para);
	f(para);
}
/*main.c*/
#include <stdio.h>
#include "para_callback.h"

/*
1.回调函数的参数按什么类型解释由调用者规定
2.调用者知道自己传的参数是char *型的,所以在回调函数中,就应该知道参数要转换成char *型来解释
*/
void say_hello(void *str)/**/
{
	printf("Hello %s\n",(const char *)str);/* “Gays”字符串的类型是char*的,参数要转换成char*型 */
}

void count_numbers(void *num)
{
	int i;
	for (i=1;i<=(int)num;i++)
		printf("%d", i);
	putchar('\n');
}

int main(void)
{
	repeat_three_times(say_hello,"Guys");
	repeat_three_times(count_numbers, (void *)4);
	return 0;
}

3.回调函数的eg2

  • 回调函数的一个典型应用就是实现类似C++的泛型算法(Generics Algorithm)
  • 下面实现的max函数可以在任意一组对象中找出最大值,可以是一组int、一组char或者一组结构体,但是实现者并不知道怎样去比较两个对象的大小,调用者需要提供一个做比较操作的回调函数。
/*generics.h*/
#ifndef GENERICS_H
#define GENERICS_H

typedef int (*cmp_t)(void*, void*);
extern void *max(void *data[], int num, cmp_t cmp);
/**generics.c/
#include "generics.h"
/*
1.max函数之所以能对一组任意类型的对象进行操作,关键在于传给max的是指向对象的指针所构成的
数组,而不是对象本身所构成的数组,这样max不必关心对象到底是什么类型,只需转给比较函数cmp,
然后根据比较结果做相应操作即可

2.cmp是调用者提供的回调函数,调用者当然知道对象是什么类型以及如何比较。
*/


void *max(void *data[],int num, cmp_t cmp)
{
	int i;
	void *temp=data[0];
	for (i=1;i<num;i++)
	{
		if (cmp(temp,data[i])<0)
			temp=data[i];
	}
	return temp;
}
/*main.c*/
#include <stdio.h>
#include "generics.h"

typedef struct 
{
	const char *name;
	int score;
}student_t;

/*
1.cmp_student是做比较的回调函数
*/
int cmp_student(void *a, void *b)
{
	if (((student_t *)a->score)>((student_t *)b->score))
		return 1;
	else if (((student_t *)a->score)==((student_t *)b->score))
		return 0;
	else 
		return -1;	
}

int main(void)
{
	student_t list[4]={{"Tom",68},{"Jerry",72},{"Moby",60},{"Kirby",89}};
	student_t *plist[4]={&list[0],&list[1],&list[2],&list[3]};
	student_t *pmax=max((void**)plist,4,cmp_student);
	printf("%s gets the highest score %d\n",pmax->name,pmax->score);
	return 0;
}

4.同步调用和异步调用(注册回调函数)

  • 以上举例eg1,eg2的回调函数是被同步调用的,调用者调用max函数, max函数则调用cmp函数,相当于调用者间接调了自己提供的回调函数。
  • 在实际系统中,异步调用也是回调函数的一种典型用法,调用者首先将回调函数传给实现者,实现者记住这个函数,这称为注册一个回调函数,然后当某个事件发生时实现者再调用先前注册的函数。 比如sigaction(2)注册一个信号处理函数,当信号产生时由系统调用该函数进行处理,再比如pthread_create(3)注册一个线程函数,当发生调度时系统切换到新注册的线程函数中运行,在GUI编程中异步回调函数更是有普遍的应用,例如为某个按钮注册一个回调函数,当用户点击按钮时调用它。
  • 以下是一个异步调用的代码框架。
/* registry.h */
#ifndef REGISTRY_H
#define REGISTRY_H
typedef void (*registry_t)(void);
extern void register_func(registry_t);
/* registry.c */
#include <unistd.h>
#include "registry.h"
/*
1.注意注册的回调函数func是全局的,存储在全局存储区,你当作他已经实现了
2.别的.c文件include这个registry.h头文件,可以在外面使用 register_func函数,而不需要知道
注册函数func是如何实现的
*/
static registry_t func;

void register_func(registry_t f)
{
	func = f;
}
static void on_some_event(void)
{
	...
	func();
	...
}
  • 既然参数可以是函数指针,返回值同样也可以是函数指针,因此可以有func()();这样的调用。
  • 返回函数的函数在C语言中很少见,在一些函数式编程语言(例如LISP)中则很常见,基本思想是把函数也当作一种数据来操作,输入、输出参与运算,操作函数的函数称为高阶函数(High-order Function) 。

5.回调函数eg3:简单版

#include <stdio.h>  
  
int add_ret() ;  
  
int add(int a , int b , int (*add_value)())  
{  
    return (*add_value)(a,b);  
}  
  
int main(void)  
{  
    int sum = add(3,4,add_ret);  
    printf("sum:%d\n",sum);  
    return 0 ;  
}   
  
int add_ret(int a , int b)  
{  
    return a+b

回调函数_回调函数

  • 我们把函数的指针(地址),这里也就是add_ret,作为参数int add(int a , int b , int (*add_value)()) , 这里的参数就是int(*add_value)() , 这个名字可以随便取,但是要符合C语言的命名规范。当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。我们看到add函数内部,return (*add_value)(a,b) ; 这个(*add_value)(a,b)相当于对指针进行了简引用,我们在main函数中,传入具体要实现功能的函数,add_ret,这个函数很简单,就是实现两数相加并返回,这里刚刚好,简引用,相当于取出指针返回地址里的值,这个值就是return a+b,也就是我们传入a和b两数相加的结果。
  • 回调函数究竟有什么作用呢?
    说到这里,就有了用户和开发者之间的概念,比方说,刚刚说的add()这个函数,假设一下,用户是实现add_value这个函数,而开发者是实现add_value这个函数,用户做的工作不多,就是想要通过开发者实现的这么一个接口,然后在函数中通过调用开发者实现的这个接口的返回值,然后来实现我们的功能。这个开发者角色就很多了,可以是自己公司的核心开发人物,也可以是别的工作的外包商的人物,这时候,他作为一个开发者的角色完完全全可以将add_value实现的add_ret这个函数封装起来并且加密,然后扔一个.so或者.a给用户,那么用户就看不到具体add_ret的实现内容,用户只需要开发者给他提供一个.h和.so即可,这样,作为开发者,他就将他实现的函数功能给保密了。

5.回调函数eg4:简单版

接下来,我们用Linux来演示下这个结果:

我们在目录下创建三个文件,main.c,vendor.c,vendor.h

     Main.c是用户开发的

     Vendor.c和vendor.h是开发者实现的。
  • 在main.c中,代码如下:
#include <stdio.h>  
#include "vendor.h"  
  
int add(int a , int b , int (*add_value)())  
{  
    return (*add_value)(a,b);  
}  
  
int main(void)  
{  
    int sum = add(3,4,add_ret);  
    printf("sum:%d\n",sum);  
    return 0 ;  
}
  • vendor.c,代码如下:
#include "vendor.h"  
int add_ret(int a , int b)  
{  
    return a+b ;  
}
  • vendor.h,代码如下:
#ifndef __VENDOR_H  
#define __VENDOR_H  
  
int add_ret(int a, int b) ;  
  
#endif
  • 接下来,我们制作一个动态链接库,最终开发者把vendor.c的内容封起来,把vendor.h提供给用户使用。
在linux下制作动态链接库,将vendor.c和vendor.h打包成一个动态链接库

先明白以下几个命令是什么意思:

生成动态库:

gcc -shared -fPIC dvendor.c -o libvendor.so    

-shared : 生成动态库;

-fPIC  : 生成与位置无关代码;

-o               :指定生成的目标文件;

 

使用动态库:

gcc main.c -L . –lvendor -o main

-L : 指定库的路径(编译时); 不指定就使用默认路径(/usr/lib/lib)

-lvendor : 指定需要动态链接的库是谁;

代码运行时需要加载动态库:

./main 加载动态库 (默认加载路径:/usr/lib /lib ./ ...)

./main

我们将编译动态库生成的libvendor.so拷贝到/usr/lib后,现在就不需要vendor.c了,此时我们将vendor.c移除,也可以正常的编译并且执行main函数的结果,这就是回调函数的作用之一。
  • 回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
  • 回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。