17、回调函数与qsort:qsort函数的使用、自定义比较函数、回调函数的实际应用
说到C语言的灵魂,指针排第一,那回调函数绝对能排第二。很多初学者觉得回调函数是个玄学,其实说白了——就是把一个函数的地址当作参数,传给另一个函数。今天我就拿标准库里的qsort来给你拆解清楚。
17.1 qsort 函数:标准库里的排序利器
先看原型,在 <stdlib.h> 里:
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
参数不多,但每个都有讲究:
- base:待排序数组的首地址。注意是
void*,意味着它能排任何类型的数据。 - nmemb:数组元素个数。
- size:每个元素的大小(字节数)。
- compar:比较函数的指针。这就是回调函数的核心。
我刚开始用 qsort 时犯过一个低级错误——传错了 size 参数。排 int 数组传了 sizeof(int*),结果排序完全乱套。嗯,这种坑踩一次就记住了。
17.2 自定义比较函数:回调的灵魂
qsort 不知道你要排什么类型的数据,也不知道你想按什么规则排。所以它把「比较」这件事交给你——你写一个比较函数,它来调用。
比较函数的签名是固定的:
int compar(const void *a, const void *b);
返回值规则:
- 返回 负数:a 排在 b 前面
- 返回 0:a 和 b 相等
- 返回 正数:a 排在 b 后面
来个最经典的例子——排整型数组:
#include <stdio.h>
#include <stdlib.h>
int cmp_int(const void *a, const void *b) {
int ia = *(const int *)a;
int ib = *(const int *)b;
return ia - ib;
}
int main() {
int arr[] = {34, 7, 23, 32, 5, 62};
int n = sizeof(arr) / sizeof(arr[0]);
qsort(arr, n, sizeof(int), cmp_int);
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
注意看 cmp_int 里那两个强制类型转换。因为 qsort 传进来的是 void*,你必须先转成 int* 再解引用。这一步很多人会忘,结果就是拿 void* 直接解引用——编译直接报错。
17.3 排结构体:回调函数的真正威力
排基本类型只是开胃菜。真正体现回调函数价值的,是排结构体。比如你有一个学生结构体,想按成绩排序:
typedef struct {
char name[32];
int score;
} Student;
int cmp_student_by_score(const void *a, const void *b) {
const Student *sa = (const Student *)a;
const Student *sb = (const Student *)b;
return sa->score - sb->score;
}
// 调用
Student students[100];
// ... 填充数据 ...
qsort(students, 100, sizeof(Student), cmp_student_by_score);
你看,同样的 qsort,换个比较函数就能排完全不同的数据。这就是回调函数的精髓——把算法和比较逻辑解耦。
我曾经在一个嵌入式项目里,需要按多个字段排序:先按优先级,再按时间戳。我写了三个比较函数,运行时根据用户选择动态传入不同的函数指针。代码复用率极高,维护起来也轻松。
17.4 回调函数的实际应用场景
回调函数不止用在排序里。我列几个常见的:
| 场景 | 说明 |
|---|---|
| 事件驱动 | 按键按下、定时器超时,通过回调通知上层 |
| 遍历/查找 | 如 bsearch 二分查找,也需要比较回调 |
| 异步操作 | 数据接收完成、DMA传输结束,回调处理结果 |
| 自定义排序 | 按不同字段、不同顺序排序 |
说白了,回调函数就是「你告诉我怎么做,我来做」——你把逻辑封装成函数,框架在合适的时机调用它。
17.5 避坑指南:回调函数常见问题
- 类型转换错误:
void*转成错误的类型,解引用后数据全乱。一定要确认你传进去的数组类型和比较函数里转换的类型一致。 - 比较函数返回值溢出:两个大整数相减可能溢出。比如
INT_MIN - INT_MAX会变成正数,排序结果就错了。安全写法是用if-else判断。 - 回调函数里修改了全局状态:如果回调函数依赖外部变量,要确保线程安全或可重入。
关于溢出问题,我给你一个更安全的写法:
int cmp_int_safe(const void *a, const void *b) {
int ia = *(const int *)a;
int ib = *(const int *)b;
if (ia < ib) return -1;
if (ia > ib) return 1;
return 0;
}
虽然多写了几行,但绝对安全。我在做金融交易系统时,数据量上亿,排序结果不能出任何差错,这种写法是标配。
17.6 知识体系:回调函数与qsort的核心逻辑
下面这张图帮你理清整个流程:
这张图的核心就一句话:qsort 只负责排序流程,比较逻辑完全由你的回调函数决定。你换一个比较函数,排序行为就完全变了——这就是回调函数的威力。
总结一下:
qsort是标准库提供的通用排序函数,通过回调函数实现类型无关。- 自定义比较函数必须遵循固定签名,返回负数、0、正数。
- 回调函数的核心价值在于解耦——算法框架与具体逻辑分离。
- 实际项目中,回调函数广泛用于事件处理、遍历、异步操作等场景。
好了,这一章就到这里。回调函数这东西,你多用几次就习惯了。下次遇到需要排序的场景,别自己手写冒泡了——qsort 加一个比较函数,干净利落。