4. 回调机制实战(排序):使用回调函数实现通用的冒泡排序,支持升序/降序
好,咱们今天来点真格的。
前面讲了函数指针的定义、赋值、调用,也聊了回调机制的原理。但说实话,光看概念是记不住的。我自己的经验是——必须亲手写一个能跑起来的例子,才能真正理解回调的精髓。
所以这一章,咱们就用回调函数来实现一个通用的冒泡排序。你传一个升序的比较函数进去,它就升序排;传一个降序的比较函数进去,它就降序排。同一个排序函数,两种行为。这就是回调的威力。
4.1 为什么排序需要回调?
你想想看,传统的冒泡排序长什么样?
void bubble_sort_asc(int arr[], int n) {
for (int i = 0; i < n-1; i++)
for (int j = 0; j < n-1-i; j++)
if (arr[j] > arr[j+1]) {
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
这代码只能升序排。想降序?再写一个 bubble_sort_desc,把 > 改成 <。那如果以后还要支持按绝对值排序、按奇偶排序呢?每来一种需求就复制粘贴改一行?
嗯,这显然不是好办法。
我在项目中遇到过类似的情况——一个数据采集系统需要对不同传感器的数据做不同规则的排序。如果每种规则都写一个排序函数,代码量会爆炸,而且维护起来特别痛苦。后来我用回调函数重构了,整个排序模块就剩一个函数,外加几个比较器。清爽多了。
4.2 核心思路:把比较逻辑抽出来
说白了,冒泡排序的核心就是比较两个元素,然后决定是否交换。那我们把「比较」这个动作抽象成一个函数,用函数指针传进去,排序函数只负责循环和交换,不关心具体的比较规则。
这样,排序函数就变成了一个框架,而比较函数是插件。你换一个插件,行为就变了。
排序函数:负责遍历、比较、交换(比较用回调)
比较函数:负责定义「什么叫做大」
调用者:决定传哪个比较函数
4.3 代码实现
先定义比较函数的类型。我个人习惯用 typedef,这样代码读起来更自然。
// 定义一个比较器类型
// 返回正数:a > b;返回负数:a < b;返回0:相等
typedef int (*Comparator)(int a, int b);
然后写通用的冒泡排序:
void bubble_sort(int arr[], int n, Comparator cmp) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - 1 - i; j++) {
// 用回调函数来判断是否需要交换
if (cmp(arr[j], arr[j+1]) > 0) {
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
}
注意看,这里没有写死 > 或 <,而是调用了 cmp(arr[j], arr[j+1])。如果 cmp 返回正数,说明 arr[j] 应该排在 arr[j+1] 后面,就交换。
接下来写两个比较器:
int asc_cmp(int a, int b) {
return a - b; // 升序:a > b 时返回正数
}
int desc_cmp(int a, int b) {
return b - a; // 降序:a < b 时返回正数
}
最后是调用示例:
#include <stdio.h>
void print_array(int arr[], int n) {
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
printf("\n");
}
int main() {
int arr1[] = {5, 3, 8, 1, 9, 2};
int arr2[] = {5, 3, 8, 1, 9, 2};
int n = 6;
printf("原始数组: ");
print_array(arr1, n);
bubble_sort(arr1, n, asc_cmp);
printf("升序排序: ");
print_array(arr1, n);
bubble_sort(arr2, n, desc_cmp);
printf("降序排序: ");
print_array(arr2, n);
return 0;
}
输出结果:
原始数组: 5 3 8 1 9 2
升序排序: 1 2 3 5 8 9
降序排序: 9 8 5 3 2 1
4.4 流程图:回调排序的执行过程
为了让你看得更清楚,我画了一张图,展示回调在排序中的调用链路。
4.5 避坑指南
我曾经在项目里犯过一个低级错误,分享出来给你提个醒。
比较函数返回
a - b 在大多数情况下没问题,但如果 a 和 b 都是很大的正数或负数,a - b 可能整数溢出。比如 a = 2147483647, b = -1,a - b 直接溢出变成负数,排序结果就错了。安全写法:
return (a > b) ? 1 : (a < b) ? -1 : 0;虽然多写几行,但绝对安全。
如果你想让排序更灵活,可以把比较器的参数改成
const void*,配合强制类型转换,就能排序任意类型的数据(int、float、结构体等)。这就是标准库 qsort 的做法。咱们下一章会展开讲。
4.6 扩展:支持更多排序规则
回调的好处是——你不需要改排序函数,只需要加新的比较器。
比如按绝对值排序:
#include <stdlib.h>
int abs_cmp(int a, int b) {
int abs_a = abs(a);
int abs_b = abs(b);
return (abs_a > abs_b) ? 1 : (abs_a < abs_b) ? -1 : 0;
}
按奇偶排序(偶数在前,奇数在后):
int odd_even_cmp(int a, int b) {
int a_is_even = (a % 2 == 0);
int b_is_even = (b % 2 == 0);
if (a_is_even != b_is_even)
return a_is_even ? -1 : 1; // 偶数排前面
return (a > b) ? 1 : -1; // 同为偶数或奇数时按大小排
}
调用方式完全一样:
bubble_sort(arr, n, abs_cmp);
bubble_sort(arr, n, odd_even_cmp);
你看,bubble_sort 一行代码都没改,只是换了比较器,行为就完全变了。这就是回调的魅力——把变化的部分和不变的部分解耦。
4.7 小结
这一章我们通过一个实际的排序例子,把回调机制落地了。核心就三点:
- 定义比较器类型:用
typedef让函数指针更易读 - 排序函数接收回调:在比较点调用回调,不关心具体规则
- 调用者传入规则:传升序比较器就升序,传降序比较器就降序
嗯,这个模式在 C 语言里非常常见。你以后写代码时,如果发现某个逻辑只是「比较规则」不同,其他都一样,那就可以考虑用回调来抽象。
下一章咱们会把这个思路再推进一步——用 void* 实现真正通用的排序,像标准库的 qsort 那样,能排任意类型的数据。