文章目录
-
-
- **深入指针5:回调函数与泛型排序**
-
- **一、回调函数(以计算器程序为例)**
- [**二、`qsort` 函数的使用**](#二、
qsort函数的使用) - [**三、模拟实现 `qsort`:改造冒泡排序**](#三、模拟实现
qsort:改造冒泡排序) - **四、总结与核心思想**
-
深入指针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,而将变化的部分 (具体的加减乘除运算)以函数指针的形式传递进去。这个被传入的Add、Sub等函数,就是回调函数。
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);
}
关键问题解释:
-
为什么要强制类型转换为
char*?因为
char类型占1个字节,char*指针进行加减运算时,偏移量以字节为单位。通过(char*)base + j * width,我们可以精确地计算出第j个元素的起始地址,无论这个元素原本是什么类型。 -
cmp函数如何控制升序/降序?比较函数
cmp的返回值决定了排序的方向。bubble_sort的规则是:如果cmp(elem1, elem2) > 0,则交换。因此:- 升序 :
cmp返回*(int*)p1 - *(int*)p2。当elem1 > elem2时,返回值>0,触发交换,大的元素向后移动。 - 降序 :只需让
cmp返回*(int*)p2 - *(int*)p1即可。当elem1 < elem2时,返回值>0,触发交换,小的元素向后移动,从而实现降序。
- 升序 :
-
swap函数的核心思想是什么?
swap函数的核心是逐个字节地交换内存。它将两个元素的内存区域视为一段连续的字节序列,然后通过一个循环,逐一交换它们对应的每一个字节。由于任何类型的数据在内存中最终都是由字节组成的,这种方法可以安全地交换任意类型的数据,而无需关心其具体结构。
四、总结与核心思想
- 回调函数的本质 :它是一种运行时的"行为注入"机制。通过函数指针,我们将一部分可变的"行为"(如比较规则、运算逻辑)作为参数传递给一个固定的"框架"(如
qsort、calu),从而实现代码的通用和灵活。 qsort的强大 :它是C语言泛型编程的典范。通过void*指针和回调函数,它成功地将"排序算法 "与"数据类型 "以及"比较方式"解耦。- 泛型编程思想 :旨在编写与数据类型无关的通用代码。在C语言中,这种思想主要通过
void*指针和回调函数来实现。这允许我们将算法的实现和数据的处理分离开来,提高了代码的复用性和可维护性。 - 实际应用 :在实际开发中,回调函数广泛应用于事件处理、异步编程、以及各种需要策略模式的场景中。而
qsort的思想也被许多其他语言借鉴,用于实现其内置的排序功能。