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 核心逻辑 调用者(main) qsort 排序算法 自定义比较函数 返回 -1 / 0 / 1 传入:数组、元素个数、元素大小、比较函数指针 qsort 内部调用比较函数,决定元素顺序 比较函数由用户自定义,实现任意排序规则 回调机制

这张图的核心就一句话:qsort 只负责排序流程,比较逻辑完全由你的回调函数决定。你换一个比较函数,排序行为就完全变了——这就是回调函数的威力。

总结一下:

  • qsort 是标准库提供的通用排序函数,通过回调函数实现类型无关。
  • 自定义比较函数必须遵循固定签名,返回负数、0、正数。
  • 回调函数的核心价值在于解耦——算法框架与具体逻辑分离。
  • 实际项目中,回调函数广泛用于事件处理、遍历、异步操作等场景。

好了,这一章就到这里。回调函数这东西,你多用几次就习惯了。下次遇到需要排序的场景,别自己手写冒泡了——qsort 加一个比较函数,干净利落。


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