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 回调函数的知识体系

咱们用一张图来总结回调函数的核心逻辑:

回调函数核心逻辑 调用者 (你的代码) 中间函数 (如qsort、traverse) 回调函数 (你实现的比较/操作) 传入函数指针 调用回调 返回比较结果 关键点: 1. 调用者定义回调函数,并将函数指针传给中间函数 2. 中间函数在合适的时机调用回调函数(这就是“回调”的含义) 3. 回调函数将结果返回给中间函数,中间函数据此完成工作 4. 整个过程实现了“控制反转”——中间函数不关心具体逻辑

14.5 回调函数的注意事项

用了这么多年回调,我总结了几条经验:

  1. 函数签名必须匹配:回调函数的参数和返回值必须和中间函数期望的完全一致。否则编译器会警告,运行时可能崩溃。
  2. 注意线程安全:如果回调函数在多线程环境下被调用,里面不能有全局变量的非原子操作。我吃过这个亏,调试了两天才发现是回调里用了全局计数器。
  3. 避免过深的嵌套:回调里再调回调,三层以上代码就很难读了。这时候考虑用状态机或者消息队列。
  4. 善用void*上下文:需要给回调传额外数据时,用void*指针。这是C语言里最灵活(也最危险)的特性之一,用的时候要小心类型转换。

我的个人习惯:定义回调函数类型时,总是用typedef。这样代码更清晰,调用者一眼就能看出需要传什么类型的函数。比如:typedef int (*comparator)(const void*, const void*);

14.6 总结

回调函数说白了就是:你写一个函数,把它的地址告诉别人,让别人在需要的时候调用它

qsort是教科书级的例子。你给它一个比较函数,它就能排任何类型的数据。这就是回调的威力——把算法和具体数据类型解耦。

我在项目中用回调最多的场景是事件处理、定时器回调、以及各种“插件式”设计。比如一个通信协议栈,收到不同消息类型时调用不同的处理函数——这就是回调函数的典型应用。

嗯,回调函数这个概念,理解了就很简单。关键是多写、多用。下次你写一个需要“通用化”的函数时,不妨想想:能不能用回调把变化的部分抽出来?


公众号:蓝海资料掘金营,微信deep3321