深入指针5:回调函数与泛型排序

文章目录

深入指针5:回调函数与泛型排序

本章节主要探讨C语言中两个高级且实用的概念:回调函数泛型排序 。通过理解回调函数,我们可以写出更灵活、更简洁的代码;而通过学习qsort函数,我们将掌握一种强大的、能处理任意数据类型的排序方法。

一、回调函数(以计算器程序为例)

1. 定义

回调函数本质上是一个通过函数指针被调用的函数。具体来说,我们将一个函数的地址作为参数传递给另一个函数,当这个函数指针被用来执行它所指向的函数时,被调用的那个函数就是回调函数。

2. 为什么需要回调函数?------ 消除代码冗余

让我们从一个简单的计算器程序开始。在最初的实现中,每个case分支都包含了大量重复的代码(输入、计算、输出),只有中间调用的计算函数不同。

c 复制代码
// 原始计算器代码片段(存在大量重复)
switch (input) {
    case 1:
        printf("请输入两个整数: "); scanf("%d %d", &x, &y);
        ret = Add(x, y); // 只有这一行不同
        printf("结果是:%d", ret);
        break;
    case 2:
        printf("请输入两个整数: "); scanf("%d %d", &x, &y);
        ret = Sub(x, y); // 只有这一行不同
        printf("结果是:%d", ret);
        break;
    // ... 以此类推
}

这种冗余代码不仅显得臃肿,也不利于维护。回调函数正是解决此类问题的利器。

3. 使用回调函数优化

我们可以将程序中不变的部分 (输入输出)封装成一个通用的函数calu,而将变化的部分 (具体的加减乘除运算)以函数指针的形式传递进去。这个被传入的AddSub等函数,就是回调函数

c 复制代码
// 计算器 - 使用回调函数优化
int Add(int x, int y) { return x + y; }
int Sub(int x, int y) { return x - y; }
int Mul(int x, int y) { return x * y; }
int Div(int x, int y) { return x / y; }

// 通用计算函数,接收一个函数指针作为参数
int calu(int (*pf)(int, int)) {
    int x = 0, y = 0;
    printf("请输入两个数: ");
    scanf("%d %d", &x, &y);
    int ret = pf(x, y); // 通过函数指针调用回调函数
    printf("结果是: %d\n", ret);
    return ret;
}

void menu() { /* ... 菜单打印代码 ... */ }

int main() {
    int input = 0;
    do {
        menu();
        printf("请选择: ");
        scanf("%d", &input);
        switch (input) {
        case 1: calu(Add); break; // 将Add函数的地址传入
        case 2: calu(Sub); break; // 将Sub函数的地址传入
        case 3: calu(Mul); break;
        case 4: calu(Div); break;
        case 0: printf("正在退出...\n"); break;
        default: printf("请重新选择:\n"); break;
        }
    } while (input);
    return 0;
}

总结:

  • 回调机制calu函数像一个"中间商",它接收一个"指令"(函数指针),然后在合适的时机(获取用户输入后)执行这个指令,最后返回结果。
  • 定义 :当函数指针被用来调用其指向的函数时,被调用的函数(Add, Sub等)就是回调函数
  • 优势 :回调函数不是由实现方(main函数)直接调用的,而是在特定事件或条件(calu函数内部)发生时,由另一方(calu函数)调用,以响应事件(执行运算并输出结果)。这种方式极大地提高了代码的复用性和灵活性。

二、qsort 函数的使用

qsort是C语言标准库中提供的一个通用排序函数,它可以对任意数据类型的数组进行排序。

1. 函数原型
qsort函数的声明位于<stdlib.h>头文件中,其原型如下:

c 复制代码
void qsort(void* base, size_t num, size_t size,
           int (*compar)(const void*, const void*));

2. 参数解释

  • base :指向待排序数组的第一个元素的指针。由于是void*类型,它可以接收任何类型的数组。
  • num :数组中元素的个数,由 sizeof(arr) / sizeof(arr[0]) 计算得到。
  • size :数组中每个元素的大小(以字节为单位),由 sizeof(arr[0]) 计算得到。这个参数让qsort知道如何寻址下一个元素。
  • compar :这是一个函数指针 ,指向一个用于比较两个元素的回调函数 。这个函数由用户自己提供,定义了具体的比较规则。

3. 比较函数 compar 的规则

用户自定义的比较函数必须遵循以下格式和规则:

  • 返回值
    • 如果 p1 指向的元素 大于 p2 指向的元素,返回一个 大于0 的整数。
    • 如果 p1 指向的元素 小于 p2 指向的元素,返回一个 小于0 的整数。
    • 如果两个元素 相等 ,返回 0
  • 参数 :两个参数都是 const void* 类型,这是一种泛型指针,可以接收任何类型的地址。

4. void* 的好处

  • 泛型编程void* 指针可以指向任何数据类型(int、double、结构体等),这使得qsort函数本身与数据类型无关,实现了"一次编写,到处使用"的泛型思想。
  • 灵活性 :在用户提供的compar函数内部,我们可以将void*指针强制转换回其原始的数据类型,然后进行具体的比较操作。

5. 使用举例

示例1:排序整型数组

c 复制代码
#include <stdio.h>
#include <stdlib.h> // qsort 的头文件

// 比较整数的回调函数
int cmp_int(const void* p1, const void* p2) {
    // 将 void* 强制转换为 int*,然后解引用并相减
    return *(int*)p1 - *(int*)p2; // 升序排列
    // 如果要降序,可以返回 *(int*)p2 - *(int*)p1;
}

void print_int_arr(int arr[], int sz) {
    for (int i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

void test1() {
    int arr[] = {10, 2, 4, 3, 6, 5, 7, 8, 9, 1};
    int sz = sizeof(arr) / sizeof(arr[0]);
    qsort(arr, sz, sizeof(arr[0]), cmp_int);
    print_int_arr(arr, sz); // 输出:1 2 3 4 5 6 7 8 9 10
}

示例2:排序结构体数组

假设有一个学生结构体,我们希望按照姓名或年龄进行排序。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // strcmp 的头文件

typedef struct Stu {
    char name[20];
    int age;
} Stu;

// 按姓名比较(字符串比较)
int cmp_by_name(const void* p1, const void* p2) {
    // 强制转换为结构体指针,然后使用 -> 访问成员,并用 strcmp 比较
    return strcmp(((Stu*)p1)->name, ((Stu*)p2)->name);
}

// 按年龄比较
int cmp_by_age(const void* p1, const void* p2) {
    return ((Stu*)p1)->age - ((Stu*)p2)->age;
}

void test2() {
    Stu students[] = {{"zhangsan", 18}, {"lisi", 20}, {"wangwu", 19}};
    int sz = sizeof(students) / sizeof(students[0]);

    // 按姓名排序
    qsort(students, sz, sizeof(students[0]), cmp_by_name);
    // 排序结果:lisi, wangwu, zhangsan

    // 按年龄排序
    // qsort(students, sz, sizeof(students[0]), cmp_by_age);
    // 排序结果:zhangsan(18), wangwu(19), lisi(20)
}
  • 注意 :字符串比较必须使用strcmp函数,它返回的值正好符合compar函数的返回值要求(正、负、零)。

三、模拟实现 qsort:改造冒泡排序

为了深入理解qsort的工作原理,我们可以尝试将经典的冒泡排序改造成一个通用的排序函数。这个过程完美地体现了回调函数和泛型编程的思想。

c 复制代码
#include <stdio.h>
#include <string.h>

// 通用的字节交换函数
void swap(char* buf1, char* buf2, size_t width) {
    for (size_t i = 0; i < width; i++) {
        char tmp = *buf1;
        *buf1 = *buf2;
        *buf2 = tmp;
        buf1++;
        buf2++;
    }
}

// 通用的冒泡排序函数 (模拟 qsort)
void bubble_sort(void* base, size_t sz, size_t width,
                 int (*cmp)(const void* p1, const void* p2)) {
    // 外层循环:排序的趟数
    for (size_t i = 0; i < sz - 1; i++) {
        // 内层循环:每趟比较的对数
        for (size_t j = 0; j < sz - 1 - i; j++) {
            // 计算两个要比较的元素的地址
            // 关键:将 base 强制转换为 char*,然后通过 j * width 计算偏移量
            void* elem1 = (char*)base + j * width;
            void* elem2 = (char*)base + (j + 1) * width;

            // 调用回调函数 cmp 进行比较
            if (cmp(elem1, elem2) > 0) {
                // 如果比较结果 >0,说明 elem1 > elem2,需要交换
                swap(elem1, elem2, width);
            }
        }
    }
}

// --- 以下是测试代码,与 qsort 示例中的 cmp 函数和 test 函数类似 ---
// 为了完整性,再次列出 cmp_int 和 print 函数
int cmp_int(const void* p1, const void* p2) { return *(int*)p1 - *(int*)p2; }
void print_int_arr(int arr[], int sz) {
    for (int i = 0; i < sz; i++) printf("%d ", arr[i]);
    printf("\n");
}

typedef struct Stu { char name[20]; int age; } Stu;
int cmp_by_name(const void* p1, const void* p2) { return strcmp(((Stu*)p1)->name, ((Stu*)p2)->name); }
int cmp_by_age(const void* p1, const void* p2) { return ((Stu*)p1)->age - ((Stu*)p2)->age; }

void test_bubble() {
    int arr[] = {9, 10, 3, 4, 5, 1, 2, 6, 7, 8};
    int sz = sizeof(arr) / sizeof(arr[0]);
    bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
    print_int_arr(arr, sz); // 输出:1 2 3 4 5 6 7 8 9 10

    Stu students[] = {{"lisi", 18}, {"zhangsan", 20}, {"wangwu", 19}};
    sz = sizeof(students) / sizeof(students[0]);
    // bubble_sort(students, sz, sizeof(students[0]), cmp_by_name);
    // bubble_sort(students, sz, sizeof(students[0]), cmp_by_age);
}

关键问题解释:

  1. 为什么要强制类型转换为 char*

    因为char类型占1个字节,char*指针进行加减运算时,偏移量以字节为单位。通过 (char*)base + j * width,我们可以精确地计算出第j个元素的起始地址,无论这个元素原本是什么类型。

  2. cmp函数如何控制升序/降序?

    比较函数cmp的返回值决定了排序的方向。bubble_sort的规则是:如果cmp(elem1, elem2) > 0,则交换。因此:

    • 升序cmp返回 *(int*)p1 - *(int*)p2。当elem1 > elem2时,返回值>0,触发交换,大的元素向后移动。
    • 降序 :只需让cmp返回 *(int*)p2 - *(int*)p1 即可。当elem1 < elem2时,返回值>0,触发交换,小的元素向后移动,从而实现降序。
  3. swap函数的核心思想是什么?
    swap函数的核心是逐个字节地交换内存。它将两个元素的内存区域视为一段连续的字节序列,然后通过一个循环,逐一交换它们对应的每一个字节。由于任何类型的数据在内存中最终都是由字节组成的,这种方法可以安全地交换任意类型的数据,而无需关心其具体结构。


四、总结与核心思想
  • 回调函数的本质 :它是一种运行时的"行为注入"机制。通过函数指针,我们将一部分可变的"行为"(如比较规则、运算逻辑)作为参数传递给一个固定的"框架"(如qsortcalu),从而实现代码的通用和灵活。
  • qsort的强大 :它是C语言泛型编程的典范。通过void*指针和回调函数,它成功地将"排序算法 "与"数据类型 "以及"比较方式"解耦。
  • 泛型编程思想 :旨在编写与数据类型无关的通用代码。在C语言中,这种思想主要通过void*指针和回调函数来实现。这允许我们将算法的实现和数据的处理分离开来,提高了代码的复用性和可维护性。
  • 实际应用 :在实际开发中,回调函数广泛应用于事件处理、异步编程、以及各种需要策略模式的场景中。而qsort的思想也被许多其他语言借鉴,用于实现其内置的排序功能。
相关推荐
qq_454245031 小时前
计算机与AI领域中的“上下文”:多维度解析
数据结构·人工智能·分类
今儿敲了吗1 小时前
24| 字符串
数据结构·c++·笔记·学习·算法
Wect1 小时前
LeetCode 105. 从前序与中序遍历序列构造二叉树:题解与思路解析
前端·算法·typescript
小雨中_1 小时前
2.5 动态规划方法
人工智能·python·深度学习·算法·动态规划
小龙报1 小时前
【51单片机】不止是调光!51 单片机 PWM 实战:呼吸灯 + 直流电机正反转 + 转速控制
数据结构·c++·stm32·单片机·嵌入式硬件·物联网·51单片机
智算菩萨2 小时前
【Python小游戏】基于Pygame的递归回溯迷宫生成与BFS寻路实战:从算法原理到完整游戏架构的深度解析
python·算法·pygame
qq_454245032 小时前
Graphkey:使用占位符彻底解耦函数与工作流
数据结构·c#
测绘工程师2 小时前
【排序算法】冒泡排序
数据结构·算法·排序算法
m0_672703312 小时前
上机练习第28天
算法