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 流程图:回调排序的执行过程

为了让你看得更清楚,我画了一张图,展示回调在排序中的调用链路。

main() bubble_sort() asc_cmp() desc_cmp() 调用排序 回调 cmp(a,b) 传入函数指针 流程说明: 1. main() 决定排序规则,传入 asc_cmp 或 desc_cmp 的函数指针 2. bubble_sort() 在每次比较时回调传入的比较函数,根据返回值决定是否交换

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 那样,能排任意类型的数据。

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