(1.)回调函数
一、回调函数的定义与核心思想
1. 定义
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给
另一个函数,当这个指针被用来调用其所指向的函数是,我们就说这是回调函数。回调函数不是由
该函数的实现方式直接调用,而是在特定的事件或条件发生时由另外的一方调用,用于对该事件或
条件进行响应
回调函数是通过函数指针调用的函数:将函数的地址(函数指针)作为参数传递给另一个函数,当满足特定条件 / 事件时,被调用的函数通过该指针执行目标函数(回调函数)。
回调函数的核心特征:
- 不由函数的实现者直接调用,而是由 "外部调用者" 触发;
- 实现解耦:调用方无需关心回调函数的具体逻辑,只需约定函数指针的接口。
2. 核心基础:函数指针
回调函数依赖 C 语言的函数指针(指向函数的指针,存储函数在内存中的入口地址),需先掌握函数指针的声明、赋值和调用。
(1)函数指针的声明格式
cs
返回值类型 (*指针变量名)(参数列表);
- 括号不可省略:
(*fp)表明fp是指针,而非返回值为指针的函数; - 参数列表需与目标函数完全一致(类型、个数、顺序)。
(2)函数指针的赋值与调用
cs
// 普通函数
int add(int a, int b) { return a + b; }
int main() {
// 声明函数指针,匹配add的参数和返回值
int (*fp)(int, int);
// 赋值:函数名本身就是函数的地址(可省略&)
fp = add; // 等价于 fp = &add;
// 调用:两种方式(推荐第一种,更直观)
int res1 = (*fp)(10, 5); // 解引用调用
int res2 = fp(10, 5); // 简化调用(编译器自动解引用)
printf("res1=%d, res2=%d\n", res1, res2); // 输出:15,15
return 0;
}
二、回调函数的基本使用步骤
- 定义回调函数:符合函数指针的接口约定;
- 定义 "注册函数":接收函数指针作为参数,负责触发回调;
- 调用注册函数:传递回调函数的地址,触发回调执行。
示例 1:最简回调(无参数无返回值)
cs
#include <stdio.h>
// 步骤1:定义回调函数
void myCallback() {
printf("回调函数被触发!\n");
}
// 步骤2:定义注册函数(接收函数指针参数)
void triggerCallback(void (*callback)()) {
// 安全检查:避免空指针调用
if (callback == NULL) {
printf("回调函数指针为空!\n");
return;
}
// 触发回调
callback();
}
// 步骤3:调用注册函数,传递回调函数地址
int main() {
triggerCallback(myCallback); // 输出:回调函数被触发!
triggerCallback(NULL); // 输出:回调函数指针为空!
return 0;
}
示例 2:带参数的回调(业务场景)
实现一个通用的 "计算器",通过回调支持加减乘除,无需修改计算器核心逻辑:
cs
#include <stdio.h>
// 步骤1:定义多个回调函数(符合同一函数指针接口)
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) { return b != 0 ? a / b : 0; }
// 步骤2:注册函数(通用计算器逻辑)
int calculate(int a, int b, int (*calcFunc)(int, int)) {
if (calcFunc == NULL) return -1;
return calcFunc(a, b);
}
// 步骤3:调用注册函数,传递不同回调
int main() {
int a = 20, b = 5;
printf("加法:%d\n", calculate(a, b, add)); // 25
printf("减法:%d\n", calculate(a, b, sub)); // 15
printf("乘法:%d\n", calculate(a, b, mul)); // 100
printf("除法:%d\n", calculate(a, b, div)); // 4
return 0;
}
三、标准库中的经典回调:qsort
C 标准库的qsort(快速排序)是回调函数的典型应用,通过回调实现通用排序(支持任意数据类型)。
qsort函数原型
cs
void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void *));
base:待排序数组的首地址;nitems:数组元素个数;size:单个元素的字节大小;compar:回调函数(比较两个元素,返回值规则:- 负数:
*a < *b; - 0:
*a == *b; - 正数:
*a > *b)。
- 负数:
示例:用qsort排序整数数组
cs
#include <stdio.h>
#include <stdlib.h>
// 回调函数:比较两个整数
int compareInt(const void *a, const void *b) {
// void* 转换为具体类型(int*),再取值比较
return *(int *)a - *(int *)b;
}
int main() {
int arr[] = {9, 3, 7, 1, 8, 5};
int len = sizeof(arr) / sizeof(arr[0]);
// 调用qsort,传递回调函数
qsort(arr, len, sizeof(int), compareInt);
// 打印排序结果
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]); // 输出:1 3 5 7 8 9
}
return 0;
}
扩展:排序自定义结构体
cs
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 自定义结构体
typedef struct {
char name[20];
int age;
} Person;
// 回调函数:按年龄升序排序
int comparePerson(const void *a, const void *b) {
return ((Person *)a)->age - ((Person *)b)->age;
}
int main() {
Person people[] = {{"Alice", 25}, {"Bob", 20}, {"Charlie", 30}};
int len = sizeof(people) / sizeof(people[0]);
qsort(people, len, sizeof(Person), comparePerson);
// 打印结果
for (int i = 0; i < len; i++) {
printf("%s: %d\n", people[i].name, people[i].age);
}
return 0;
}
四、回调函数的典型应用场景
- 通用算法 :如
qsort、bsearch(二分查找),通过回调适配不同数据类型; - 事件驱动编程:如按键触发、网络事件、定时器,回调函数响应事件;
- 模块化解耦:底层模块(如驱动、框架)通过回调通知上层业务逻辑,无需耦合上层代码;
- 异步操作:如异步 IO、多线程,回调函数处理异步任务的结果;
- 钩子函数(Hook):在框架的关键节点插入自定义逻辑(如日志、性能统计)。
五、使用回调函数的注意事项
1. 函数指针类型严格匹配
回调函数的返回值、参数类型 / 个数 / 顺序必须与函数指针声明完全一致,否则会导致:
- 编译报错(类型不兼容);
- 运行时未定义行为(如栈溢出、段错误)。
2. 避免空指针调用
调用回调前必须检查函数指针是否为NULL,否则会触发段错误(核心转储):
cs
void triggerCallback(void (*callback)()) {
if (callback != NULL) { // 关键检查
callback();
}
}
3. 回调函数的可重入性(线程安全)
若回调函数在多线程中使用:
- 避免访问全局 / 静态变量(或加互斥锁);
- 不调用非可重入函数(如
strtok、printf需谨慎)。
4. void*的灵活使用
void*是 C 语言实现 "通用回调" 的核心:
- 注册函数通过
void*接收任意类型参数; - 回调函数内部将
void*转换为具体类型(需保证类型匹配)。
5. 回调函数的生命周期
C 语言中函数地址是全局的(静态存储区),无需担心回调函数 "失效";但如果回调函数是动态生成的(如动态库卸载后),需避免悬空指针。
六、总结
回调函数是 C 语言中实现动态行为、模块解耦、通用算法的核心手段,其本质是函数指针的灵活应用。关键要点:
- 函数指针是回调的基础,需严格匹配接口;
- 回调的核心价值是 "约定接口,灵活实现";
- 实际开发中需注意空指针、线程安全、类型转换等问题。
通过回调函数,可写出高扩展性、低耦合的 C 代码,尤其适合框架开发、驱动开发、通用工具类场景。
(2.)实践代码解析
cs
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// ===================== 核心概念说明 =====================
// 回调函数:通过函数指针调用的函数,将函数地址作为参数传递给另一个函数时,被指向的函数即为回调函数
// void* 通用指针特性:可指向任意类型数据,但不能直接解引用/加减运算,需强制转换为具体类型后使用
// 示例:void* p = # // 合法;*p = 10; // 非法;((int*)p) = 10; // 合法
// ===================== 通用交换函数(适配任意类型) =====================
// 功能:逐字节交换两个任意类型的数据(核心:char*按字节操作,width指定元素大小)
// 参数:
// buf1/buf2:待交换数据的起始地址(char*保证按字节访问)
// width:单个元素的字节大小(如int为4,struct Stu为24)
void Swap(char* buf1, char* buf2, int width) {
for (int i = 0; i < width; i++) {
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++; // 移动到下一字节
buf2++;
}
}
// ===================== 比较函数(回调函数,定义排序规则) =====================
// 1. 比较两个整数(适配qsort/自定义冒泡排序)
// 返回值:>0则e1>e2(升序);<0则e1<e2;=0则相等
int cmp_int(const void* e1, const void* e2) {
return *(int*)e1 - *(int*)e2; // 升序;交换e1/e2则为降序
}
// 2. 学生结构体定义(姓名+年龄)
typedef struct {
char name[20]; // 字符串(固定长度)
int age; // 整数
} Stu;
// 3. 按学生姓名排序(字符串比较,依赖strcmp)
int cmp_stu_by_name(const void* e1, const void* e2) {
// 转换为Stu*后,取name成员,用strcmp比较(strcmp返回值符合qsort回调规则)
return strcmp(((Stu*)e1)->name, ((Stu*)e2)->name);
}
// 4. 按学生年龄排序(整数比较)
int cmp_stu_by_age(const void* e1, const void* e2) {
return ((Stu*)e1)->age - ((Stu*)e2)->age;
}
// ===================== 自定义通用冒泡排序(核心:回调函数解耦排序规则) =====================
// 功能:支持任意类型数据的冒泡排序,排序规则由回调函数cmp决定
// 参数:
// base:待排序数据的起始地址(void*适配任意类型)
// sz:数据元素个数
// width:单个元素的字节大小
// cmp:比较函数指针(回调函数,定义"谁大谁小")
void bubble_sort(void* base, int sz, int width, int (*cmp)(const void* e1, const void* e2)) {
// 外层循环:控制排序轮数(n个元素需n-1轮)
for (int i = 0; i < sz - 1; i++) {
int flag = 1; // 优化标记:1表示本轮无交换(已有序),可提前退出
// 内层循环:每轮比较相邻元素,已排序的末尾i个元素无需比较
for (int j = 0; j < sz - i - 1; j++) {
// 核心:计算相邻元素的地址(char*按字节偏移,j*width定位第j个元素)
// base强制转为char*:保证+width时按字节偏移(void*不能直接加减)
if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0) {
// 前一个元素 > 后一个元素,交换(升序)
Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
flag = 0; // 标记本轮有交换
}
}
if (flag == 1) {
break; // 无交换,说明已有序,提前退出
}
}
}
// ===================== 测试函数(验证排序功能) =====================
// 测试1:qsort排序int数组
void test1() {
int arr[] = {3, 1, 4, 1, 5, 9, 2, 6};
int sz = sizeof(arr) / sizeof(arr[0]);
printf("===== 测试1:qsort排序int数组 =====\n");
printf("排序前:");
for (int i = 0; i < sz; i++) {
printf("%d ", arr[i]);
}
// qsort参数:数组地址、元素个数、单个元素大小、比较回调函数
qsort(arr, sz, sizeof(int), cmp_int);
printf("\n排序后:");
for (int i = 0; i < sz; i++) {
printf("%d ", arr[i]);
}
printf("\n\n");
}
// 测试2:qsort排序Stu数组(按姓名)
void test2() {
Stu students[] = {{"ZhangSan", 20}, {"LiSi", 18}, {"WangWu", 22}};
int sz = sizeof(students) / sizeof(students[0]);
printf("===== 测试2:qsort排序Stu数组(按姓名) =====\n");
printf("排序前:\n");
for (int i = 0; i < sz; i++) {
printf("姓名:%s,年龄:%d\n", students[i].name, students[i].age);
}
qsort(students, sz, sizeof(Stu), cmp_stu_by_name);
printf("排序后:\n");
for (int i = 0; i < sz; i++) {
printf("姓名:%s,年龄:%d\n", students[i].name, students[i].age);
}
printf("\n");
}
// 测试3:自定义bubble_sort排序int数组
void test3() {
int arr[] = {9, 8, 7, 6, 5, 4, 3, 2, 1};
int sz = sizeof(arr) / sizeof(arr[0]);
printf("===== 测试3:自定义bubble_sort排序int数组 =====\n");
printf("排序前:");
for (int i = 0; i < sz; i++) {
printf("%d ", arr[i]);
}
bubble_sort(arr, sz, sizeof(int), cmp_int);
printf("\n排序后:");
for (int i = 0; i < sz; i++) {
printf("%d ", arr[i]);
}
printf("\n\n");
}
// 测试4:自定义bubble_sort排序Stu数组(按年龄)
void test4() {
Stu students[] = {{"ZhaoLiu", 25}, {"QianQi", 21}, {"SunBa", 19}};
int sz = sizeof(students) / sizeof(students[0]);
printf("===== 测试4:自定义bubble_sort排序Stu数组(按年龄) =====\n");
printf("排序前:\n");
for (int i = 0; i < sz; i++) {
printf("姓名:%s,年龄:%d\n", students[i].name, students[i].age);
}
bubble_sort(students, sz, sizeof(Stu), cmp_stu_by_age);
printf("排序后:\n");
for (int i = 0; i < sz; i++) {
printf("姓名:%s,年龄:%d\n", students[i].name, students[i].age);
}
}
// ===================== 主函数(调用测试) =====================
int main() {
test1(); // qsort排序int
test2(); // qsort排序Stu(按姓名)
test3(); // 自定义冒泡排序int
test4(); // 自定义冒泡排序Stu(按年龄)
return 0;
}
一、代码详细分析(按模块拆解)
1. 核心设计思想
代码的核心是通过回调函数解耦 "排序逻辑" 和 "数据类型 / 排序规则":
- 排序核心逻辑(冒泡排序 /qsort)只负责 "比较 - 交换" 的框架,不关心具体数据类型;
- 具体的 "比较规则" 由回调函数(
cmp_int/cmp_stu_by_name等)定义,实现 "一套排序逻辑适配所有数据"。
2. 关键模块解析
(1)void* 通用指针(实现跨类型适配)
- 为什么用
void*?C 语言没有泛型,void*可以指向任意类型数据(int、结构体等),是实现 "通用函数" 的核心。 - 注意事项:
void*不能直接解引用(*p)或加减运算(p+1),必须强制转换为具体类型(如char*/int*/Stu*)后操作。
(2)通用 Swap 函数(按字节交换)
- 设计思路:无论数据类型是 int(4 字节)还是结构体(24 字节),都可以按字节逐个交换;
- 核心:
char*指针按 1 字节偏移,width参数指定单个元素的总字节数,保证交换完整。
(3)回调函数(比较函数)
所有比较函数都遵循统一的函数指针接口 :int (*)(const void* e1, const void* e2),这是回调的关键(排序函数只认接口,不认具体实现):
| 比较函数 | 功能 | 实现细节 |
|---|---|---|
cmp_int |
比较整数 | 转换为int*后直接相减 |
cmp_stu_by_name |
比较学生姓名 | 转换为Stu*后调用strcmp |
cmp_stu_by_age |
比较学生年龄 | 转换为Stu*后取 age 成员相减 |
(4)自定义 bubble_sort 函数(回调的核心应用)
base参数:void*类型,接收任意数组的首地址;- 地址计算:
(char*)base + j * width强制转为char*后,j * width能精准定位第 j 个元素(如 int 数组,j=1 时偏移 4 字节); - 回调调用:
cmp(...) > 0时交换元素,排序规则完全由cmp决定(换个回调函数就能改排序规则,无需改 bubble_sort 逻辑); - 优化:
flag标记无交换时提前退出,减少不必要的循环。
(5)标准库 qsort 函数(经典回调应用)
qsort的设计思路和自定义 bubble_sort 完全一致,只是排序算法是快速排序:
- 函数原型:
void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void *)); - 调用方式:只需传递 "数组地址 + 元素个数 + 元素大小 + 比较回调",即可完成任意类型排序。
3. 运行结果(验证功能)
编译运行代码后,输出如下(按测试函数顺序):
cs
===== 测试1:qsort排序int数组 =====
排序前:3 1 4 1 5 9 2 6
排序后:1 1 2 3 4 5 6 9
===== 测试2:qsort排序Stu数组(按姓名) =====
排序前:
姓名:ZhangSan,年龄:20
姓名:LiSi,年龄:18
姓名:WangWu,年龄:22
排序后:
姓名:LiSi,年龄:18
姓名:WangWu,年龄:22
姓名:ZhangSan,年龄:20
===== 测试3:自定义bubble_sort排序int数组 =====
排序前:9 8 7 6 5 4 3 2 1
排序后:1 2 3 4 5 6 7 8 9
===== 测试4:自定义bubble_sort排序Stu数组(按年龄) =====
排序前:
姓名:ZhaoLiu,年龄:25
姓名:QianQi,年龄:21
姓名:SunBa,年龄:19
排序后:
姓名:SunBa,年龄:19
姓名:QianQi,年龄:21
姓名:ZhaoLiu,年龄:25
4. 扩展与优化点
- 降序排序:只需修改比较函数(如
cmp_int改为*(int*)e2 - *(int*)e1); - 线程安全:若在多线程中使用,需保证回调函数无全局 / 静态变量(或加锁);
- 性能优化:自定义 bubble_sort 可替换为快速排序 / 归并排序,只需保留回调接口,核心逻辑不变。
二、总结
该代码完整展示了 C 语言回调函数的核心价值:接口与实现分离。通过函数指针(回调),排序函数无需关心 "排什么数据、按什么规则排",只需遵守统一的比较接口;而具体的比较规则可灵活定制,实现了极高的扩展性。这也是 Linux 内核、标准库等底层代码中回调函数的核心应用模式。