目录
前言
回调函数预备知识
函数指针
什么是函数指针
函数指针的语法
如何用函数指针调用函数
函数指针作为函数的参数
函数指针作为函数返回类型
函数指针数组
回调函数
什么是回调函数
为什么要用回调函数
怎么使用回调函数
总结
前言
在写项目的时候,对于回调函数一知半解,这次将重新学习一下,重新理解一下 回调函数 的魅力所在
回调函数预备知识
在讲回调函数 回调函数 回调函数之前,我们需要了解函数指针。
我们都知道,C语言的灵魂是指针,我们经常使用整型指针,字符串指针,结构体指针等
函数指针
int *p1; // p1是一个指向整数(int)类型的指针变量,可以存储一个int类型数据的地址
char *p2; // p2是一个指向字符(char)类型的指针变量,可以存储一个char类型数据的地址
STRUCT *p3; // p3是一个指向结构体类型STRUCT的指针变量,STRUCT是我们定义的结构体类型
但是好像我们一般很少使用函数指针,我们一般使用函数都是直接使用函数调用。
下面我们来了解一下函数指针的概念和使用方法。
什么是函数指针
函数指针也是个指针,但是和通常的指针不一样,通常的指针指向的是整型、字符型或数组等变量
而函数指针,指向的是函数
函数指针的语法
返回类型 (*指针变量名)(参数类型列表);
-
返回类型: 函数返回的数据类型(如 int double void 等)。
-
指针变量名: 你给这个函数指针起的名字。
-
参数类型列表: 函数接受的参数类型(如果没有参数,可以留空或写 void )。
这里需要注意的是:(*指针变量名)两端的括号不能省略,括号改变了运算符的优先级。如果省略了括号,就不是定义函数指针。而是一个函数声明了,即声明了一个返回值类型为指针型的函数。
那么怎么判断一个指针变量是指向变量的还是指向函数呢?
- 首先看变量名前面有没有 “*”,如果有 “*” 说明是指针变量;
- 其次看变量名有没有带 (),如果有就是指向函数的指针变量,即函数指针,如果没有就是指向变量的指针变量。
最后需要注意的是:指向函数的指针变量没有 ++ 和 – 运算。
一般为了方便使用,我们会选择用typedef进行函数指针类型的别名定义
// 定义一个函数指针类型 别名为:Fun1,它指向返回 int 类型、接受一个 int 参数的函数
typedef int (*Fun1)(int);
// 定义一个函数指针类型 别名为:Fun2,它指向返回 int 类型、接受两个参数(int 和 int)的函数
typedef int (*Fun2)(int, int);
// 定义一个函数指针类型 别名为:Fun3,它指向返回 void 类型、无参数的函数
typedef void (*Fun3)(void);
// 定义一个函数指针类型 别名为:Fun4,它指向返回 void* 类型、接受一个 void* 参数的函数
typedef void* (*Fun4)(void*);
为什么要使用typedef呢?
如何用函数指针调用函数
举个例子
int Func(int x); /*声明一个函数*/
int (*p) (int x); /*定义一个函数指针*/
p = Func; /*将Func函数的首地址赋给指针变量p*/
p = &Func; /*将Func函数的首地址赋给指针变量p*/
赋值时函数 Func 不带括号,也不带参数。由于函数名 Func 代表函数的首地址,因此经过赋值以后,指针变量 p 就指向函数 Func() 代码的首地址了。
下面来写一个程序,看了这个程序你们就明白函数指针怎么使用了:
特别注意的是,因为函数名本身就可以表示该函数地址(指针),因此在获取函数指针时,可以直接用函数名,也可以用&取函数的地址。
函数指针作为函数的参数
我们见过,函数的形参是指针的
// 函数接受一个整数指针作为参数,并修改该值
void modifyValue(int *ptr) {
*ptr = 20; // 修改指针指向的值
}
那么函数指针作为指针,肯定也能放到函数的形参中的
#include <stdio.h>
// 定义一个函数类型:别名为operation,返回类型是int,参数类型是int和int
typedef int (*operation)(int, int);
// 一个加法函数
int add(int a, int b) {
return a + b;
}
// 一个减法函数
int sub(int a, int b) {
return a - b;
}
// 函数 modifyValue 接受一个函数指针作为参数,并调用它
void modifyValue(int *ptr, operation op) {
*ptr = op(*ptr, 5); // 使用传入的函数指针 op 来修改 ptr 指向的值
}
int main() {
int num = 10;
printf("Before: %d\n", num); // 输出修改前的值
// 传递加法函数的指针
modifyValue(&num, add);
printf("After add: %d\n", num); // 输出加法操作后的值
// 传递减法函数的指针
modifyValue(&num, sub);
printf("After sub: %d\n", num); // 输出减法操作后的值
return 0;
}
运行结果
为什么函数指针我们用的是别名operation,在下面调用modifyValue的时候,直接传入add,sub就能识别了呢?
在C语言中,函数名(如add或sub)在没有括号时会自动转换为指向该函数的指针。这与operation类型 的期望使用一个函数指针相匹配。
函数指针作为函数返回类型
有了上面的基础,要写出返回类型为函数指针的函数应该不难了,下面这个例子就是返回类型为函数指针的函数:
void (* func5(int, int, float ))(int, int)
{
...
}
在这里, func5 以 (int, int, float) 为参数,其返回类型为 void (\*)(int, int) 。
函数指针数组
在开始讲解回调函数前,最后介绍一下函数指针数组。既然函数指针也是指针,那我们就可以用数组来存放函数指针。下面我们看一个函数指针数组的例子:
/* 方法1 */
void (*func_array_1[5])(int, int, float);
/* 方法2 */
typedef void (*p_func_array)(int, int, float);
p_func_array func_array_2[5];
回调函数
什么是回调函数
我们先来看看百度百科是如何定义回调函数的:
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
这段话比较长,也比较绕口。下面我通过一幅图来说明什么是回调:
假设我们要使用一个排序函数来对数组进行排序,那么在 主程序(Main program) 中,我们先通过库,选择一个 库排序函数(Library function)。但排序算法有很多,有冒泡排序,选择排序,快速排序,归并排序。同时,我们也可能需要对特殊的对象进行排序,比如特定的结构体等。库函数会根据我们的需要选择一种排序算法,然后调用实现该算法的函数来完成排序工作。这个被调用的排序函数就是回调函数(Callback function)。
结合这幅图和上面对回调函数的解释,我们可以发现,要实现回调函数,最关键的一点就是要将函数的指针传递给一个函数(上图中是库函数),然后这个函数就可以通过这个指针来调用回调函数了。注意,回调函数并不是C语言特有的,几乎任何语言都有回调函数。在C语言中,我们通过使用函数指针来实现回调函数。
总结:把一段可执行的代码(函数)像参数传递那样传给其他代码,而这段代码会在某个时刻被调用执行,这就叫 回调 。如果代码立即被执行就称为同步回调,如果过后再执行,则称之为异步回调
回调函数 就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是 回调函数 。
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
为什么要用回调函数
因为可以把调用者与被调用者分开,所以调用者不关心谁是被调用者。它只需知道存在一个具有特定原型和限制条件的被调用函数。
简而言之,回调函数就是允许用户把需要调用的方法的指针作为参数传递给一个函数,以便该函数在处理相似事件的时候可以灵活的使用不同的方法。
int Callback() ///< 回调函数
{
// TODO
return 0;
}
int main() ///< 主函数
{
// TODO
Library(Callback); ///< 库函数通过函数指针进行回调
// TODO
return 0;
}
回调似乎只是函数间的调用,和普通函数调用没啥区别。
但仔细看,可以发现两者之间的一个关键的不同:在回调中,主程序把回调函数像参数一样传入库函数。
这样一来,只要我们改变传进库函数的参数,就可以实现不同的功能,这样有没有觉得很灵活?并且当库函数很复杂或者不可见的时候利用回调函数就显得十分优秀。如果还不明白。看一下下面的例子就会恍然大悟了
怎么使用回调函数
int Callback_1(int a) //回调函数1
{
cout << "Hello, this is Callback_1。" << "a=" << a << endl;
return 0;
}
int Callback_2(int b) //回调函数2
{
cout << "Hello, this is Callback_2。" << "b=" << b << endl;
return 0;
}
int Callback_3(int c) //回调函数3
{
cout << "Hello, this is Callback_3。" << "c=" << c << endl;
return 0;
}
int Handle(int x, int (*Callback)(int x))
{
Callback(x);
return 0;
}
int main()
{
Handle(10, Callback_1);
Handle(20, Callback_2);
Handle(30, Callback_3);
return 0;
}
可能同学会有一个疑问:Callback(x); 这里就一个函数是怎么做到识别Callback_1 2 3的呢?
Handle(10, Callback_1); Handle(20, Callback_2); Handle(30, Callback_3);本质上是把Callback_1 2 3函数的地址传给Handle函数。Handle函数里的Callback(x); 它存储的是具体回调函数的内存地址,比如说你传入的是 Handle(10, Callback_1);,那Callback存储的是Callback_1的地址,存储的x变量是10,它就会找到Callback_1并且把10传给它
下面是一个四则运算的简单回调函数例子:
typedef float (*Operation)(float, float);
float ADD(float a, float b)
{
cout << "a+b=" << a + b << endl;
return a + b;
}
float SUB(float a, float b)
{
cout << "a-b=" << a - b << endl;
return a - b;
}
float MUL(float a, float b)
{
cout << "a*b=" << a * b << endl;
return a * b;
}
float DIV(float a, float b)
{
if (b == 0) {
printf("Error: Division by zero!\n");
return 0;
}
cout << "a/b=" << a / b << endl;
return a / b;
}
float add_sub_mul_div(float a, float b, Operation op)
{
op(a, b);
return 0;
}
int main()
{
add_sub_mul_div(1.1, 2.2, ADD);
add_sub_mul_div(1.1, 2.2, SUB);
add_sub_mul_div(1.1, 2.2, MUL);
add_sub_mul_div(1.1, 2.2, DIV);
return 0;
}
总结
这下对于回调函数是更加理解了,希望各位在今后的学习中能够看见回调函数不再迷惑!