14、函数指针作为参数:回调函数
各位同学,今天我们来聊一个让很多人又爱又恨的话题——回调函数。
说实话,我刚开始学C语言那会儿,看到“回调”这个词就头大。什么叫回调?谁调谁?为什么非要绕一圈?后来在项目中真正用上了,才明白这东西有多香。
嗯,咱们今天就把这个事彻底讲透。
14.1 什么是回调函数?
先别急着看定义。咱们想一个场景:
你写了一个排序函数,可以对整数数组排序。但有一天,老板说:“小王啊,现在要排浮点数了。”你改一改,OK。又过两天:“要排字符串了。”你又改。再过一周:“要按学生成绩排结构体了。”
你是不是想骂人?
回调函数就是来解决这个问题的。说白了,回调函数就是一个通过函数指针调用的函数。你把一个函数的地址传给另一个函数,让它在合适的时候去调用你传过去的那个函数。
核心概念:回调函数 = 函数指针作为参数 + 延迟执行
我在项目中遇到过最典型的场景:写一个通用的遍历函数,对链表每个节点做不同操作。打印、删除、修改……操作各不相同,但遍历逻辑完全一样。这时候回调函数就是最佳选择。
14.2 qsort函数深度分析
C标准库里的qsort,就是回调函数最经典的例子。咱们来看看它的真面目:
#include <stdlib.h>
void qsort(void *base, // 待排序数组首地址
size_t nmemb, // 元素个数
size_t size, // 每个元素的大小(字节)
int (*compar)(const void *, const void *) // 比较函数的指针
);
注意最后一个参数——int (*compar)(const void *, const void *)。这就是一个函数指针,指向一个比较函数。
为什么qsort要这么设计?你想想看,qsort不知道你要排什么类型的数据。整数、浮点数、字符串、结构体……它统统不知道。它只知道:你给我一个比较两个元素的方法,我就能把它们排好。
这就是回调的威力——把“比较”这个行为抽象出来,交给调用者去实现。
14.2.1 比较函数的写法
比较函数必须返回:
- 负数:第一个参数小于第二个
- 零:两个相等
- 正数:第一个大于第二个
来看几个实际例子:
// 比较整数
int cmp_int(const void *a, const void *b) {
int ia = *(const int *)a;
int ib = *(const int *)b;
return ia - ib; // 升序
// return ib - ia; // 降序
}
// 比较浮点数(注意:不要直接返回差值,可能有精度问题)
int cmp_double(const void *a, const void *b) {
double da = *(const double *)a;
double db = *(const double *)b;
if (da < db) return -1;
if (da > db) return 1;
return 0;
}
// 比较字符串(按字典序)
int cmp_str(const void *a, const void *b) {
// a和b是指向字符串指针的指针
const char **sa = (const char **)a;
const char **sb = (const char **)b;
return strcmp(*sa, *sb);
}
我曾经踩过的坑:比较整数时直接返回 *(int*)a - *(int*)b。如果两个数相差很大(比如INT_MAX和INT_MIN),减法会溢出!安全写法是用 if-else 判断大小。
14.2.2 使用qsort排序结构体
假设我们有一个学生结构体,按成绩排序:
typedef struct {
char name[32];
int score;
} Student;
int cmp_student(const void *a, const void *b) {
const Student *sa = (const Student *)a;
const Student *sb = (const Student *)b;
return sa->score - sb->score; // 按成绩升序
}
int main() {
Student students[] = {
{"Alice", 85},
{"Bob", 92},
{"Charlie", 78}
};
int n = sizeof(students) / sizeof(students[0]);
qsort(students, n, sizeof(Student), cmp_student);
// 输出排序结果...
return 0;
}
你看,qsort根本不需要知道Student是什么。它只知道每个元素占sizeof(Student)个字节,比较的时候调用你给的cmp_student函数。这就是回调的妙处。
14.3 自定义回调函数示例
光说不练假把式。咱们自己写一个带回调的函数,感受一下。
14.3.1 场景:遍历数组并执行操作
// 回调函数类型:接收一个int,返回void
typedef void (*operate_func)(int);
// 遍历数组,对每个元素执行回调
void traverse(int arr[], int len, operate_func op) {
for (int i = 0; i < len; i++) {
op(arr[i]); // 调用回调函数
}
}
// 具体操作:打印
void print_int(int x) {
printf("%d ", x);
}
// 具体操作:翻倍
void double_int(int x) {
printf("%d ", x * 2);
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
int len = sizeof(arr) / sizeof(arr[0]);
printf("原始数据: ");
traverse(arr, len, print_int);
printf("\n翻倍后: ");
traverse(arr, len, double_int);
return 0;
}
输出:
原始数据: 1 2 3 4 5
翻倍后: 2 4 6 8 10
你看,traverse函数只负责遍历,具体做什么由回调函数决定。这就是控制反转——你不再控制“做什么”,而是把“做什么”交给调用者。
14.3.2 更复杂的例子:带上下文的回调
有时候回调函数需要额外的数据。比如我们要过滤数组中的元素:
// 回调函数类型:接收一个int和一个void*上下文
typedef int (*filter_func)(int, void *);
// 过滤数组,将符合条件的元素存入结果数组
int filter(int arr[], int len, int result[], filter_func fn, void *ctx) {
int count = 0;
for (int i = 0; i < len; i++) {
if (fn(arr[i], ctx)) {
result[count++] = arr[i];
}
}
return count;
}
// 过滤条件:大于某个阈值
int greater_than(int x, void *ctx) {
int threshold = *(int *)ctx;
return x > threshold;
}
int main() {
int arr[] = {10, 25, 8, 33, 17, 42};
int result[6];
int threshold = 20;
int count = filter(arr, 6, result, greater_than, &threshold);
printf("大于%d的元素: ", threshold);
for (int i = 0; i < count; i++) {
printf("%d ", result[i]);
}
return 0;
}
这里void *ctx就是上下文指针。它让回调函数可以访问外部数据,而不需要全局变量。我在项目中经常用这个技巧,比如传递一个配置结构体给回调函数。
14.4 回调函数的知识体系
咱们用一张图来总结回调函数的核心逻辑:
14.5 回调函数的注意事项
用了这么多年回调,我总结了几条经验:
- 函数签名必须匹配:回调函数的参数和返回值必须和中间函数期望的完全一致。否则编译器会警告,运行时可能崩溃。
- 注意线程安全:如果回调函数在多线程环境下被调用,里面不能有全局变量的非原子操作。我吃过这个亏,调试了两天才发现是回调里用了全局计数器。
- 避免过深的嵌套:回调里再调回调,三层以上代码就很难读了。这时候考虑用状态机或者消息队列。
- 善用void*上下文:需要给回调传额外数据时,用void*指针。这是C语言里最灵活(也最危险)的特性之一,用的时候要小心类型转换。
我的个人习惯:定义回调函数类型时,总是用typedef。这样代码更清晰,调用者一眼就能看出需要传什么类型的函数。比如:typedef int (*comparator)(const void*, const void*);
14.6 总结
回调函数说白了就是:你写一个函数,把它的地址告诉别人,让别人在需要的时候调用它。
qsort是教科书级的例子。你给它一个比较函数,它就能排任何类型的数据。这就是回调的威力——把算法和具体数据类型解耦。
我在项目中用回调最多的场景是事件处理、定时器回调、以及各种“插件式”设计。比如一个通信协议栈,收到不同消息类型时调用不同的处理函数——这就是回调函数的典型应用。
嗯,回调函数这个概念,理解了就很简单。关键是多写、多用。下次你写一个需要“通用化”的函数时,不妨想想:能不能用回调把变化的部分抽出来?
公众号:蓝海资料掘金营,微信deep3321