1. 回调函数详解
回调函数就是⼀个通过函数指针调⽤的函数。
如果你把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被⽤来调⽤其所指向的函数时,被调⽤的函数就是回调函数。回调函数不是由该函数的实现⽅直接调⽤,⽽是在特定的事件或条件发⽣时由另外的⼀⽅调⽤的,⽤于对该事件或条件进⾏响应。
回调函数是通过函数指针实现的机制,其核心特点如下:
- 函数指针作为参数传递
- 在特定条件触发时被调用
- 实现调用方与被调用方的解耦
数学表示:
设存在函数 f:R×R→Rf: \mathbb{R} \times \mathbb{R} \rightarrow \mathbb{R}f:R×R→R
回调机制可描述为:
g(f,x,y)=f(x,y) g(f, x, y) = f(x, y) g(f,x,y)=f(x,y)
其中 ggg 是调用函数,fff 是通过指针传递的回调函数
c
源代码
#include <stdio.h>
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;
}
void menu()
{
printf("1.add 2.sub\n");
printf("3.mul 4.div\n");
printf("0.eixt\n");
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int r = 0;
do
{
menu();
printf("请输入:");
scanf("%d", &input);
switch(input)
{
case 1:
printf("请输入2个数字:");
scanf("%d %d", &x, &y);
r = Add(x, y);
printf("结果是:%d\n", r);
break;
case 2:
printf("请输入2个数字:");
scanf("%d %d", &x, &y);
r = Sub(x, y);
printf("结果是:%d\n", r);
break;
case 3:
printf("请输入2个数字:");
scanf("%d %d", &x, &y);
r = Mul(x, y);
printf("结果是:%d\n", r);
break;
case 4:
printf("请输入2个数字:");
scanf("%d %d", &x, &y);
r = Div(x, y);
printf("结果是:%d\n", r);
break;
case 0:
printf("推出计算器\n");
break;
default :
printf("请输入0------4\n");
break;
}
} while (input);
return 0;
}
c
更改后的代码
#include <stdio.h>
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;
}
void calc(int (*pf)(int, int))
{
int x = 0;
int y = 0;
int r = 0;
printf("请输入2个数字:");
scanf("%d %d", &x, &y);
r = pf(x, y);
printf("结果是:%d\n", r);
}
void menu()
{
printf("1.add 2.sub\n");
printf("3.mul 4.div\n");
printf("0.eixt\n");
}
int main()
{
int input = 0;
do
{
menu();
printf("请输入:");
scanf("%d", &input);
switch(input)
{
case 1:
calc(Add);
break;
case 2:
calc(Sub);
break;
case 3:
calc(Mul);
break;
case 4:
calc(Div);
break;
case 0:
printf("推出计算器\n");
break;
default :
printf("请输入0------4\n");
break;
}
} while (input);
return 0;
}
代码对比分析
原始代码结构
c
switch(input) {
case 1:
// 重复输入输出逻辑
r = Add(x, y);
break;
// 类似重复代码...
}
问题:
- 代码冗余:每个分支重复输入/输出逻辑
- 耦合度高:业务逻辑与界面操作强耦合
- 扩展困难:新增功能需修改主流程
改进后代码
c
void calc(int (*pf)(int, int)) {
int x, y, r;
scanf("%d %d", &x, &y);
r = pf(x, y); // 回调点
printf("结果是:%d\n", r);
}
switch(input) {
case 1:
calc(Add); // 传递函数指针
break;
//...
}
优化点:
- 抽象公共逻辑:输入输出封装在
calc()中 - 函数指针应用:
int (*pf)(int,int)声明回调接口 - 解耦实现:业务逻辑与界面操作分离
回调机制图解
主流程 → calc() → 通过函数指针调用
↑
(Add/Sub/Mul/Div)
calc()作为通用框架- 具体运算函数作为回调注入
- 框架控制流程,回调实现具体功能
关键语法解析
c
int (*pf)(int, int) // 函数指针声明
*pf:指针变量(int, int):参数类型int:返回值类型calc(Add):传递函数地址
性能对比
| 指标 | 原始代码 | 改进代码 |
|---|---|---|
| 代码行数 | 48 | 36 |
| 分支复杂度 | 高 | 低 |
| 维护成本 | 高 | 低 |
| 扩展性 | 差 | 优 |
改进代码通过回调机制减少30%重复代码,显著提升可维护性和扩展性,完美演示了回调函数在实际工程中的应用价值。
2. qsort使用举例
2.1 使用qsort函数排序整型数据
qsort 是 C 标准库 <stdlib.h> 中提供的快速排序函数,其函数原型如下:
c
void qsort(void *base, size_t num, size_t size,
int (*compar)(const void *, const void *));
base:待排序数组的首元素地址num:数组元素个数size:每个元素的大小(字节数)compar:比较函数的函数指针(回调函数),用于定义排序规则
示例:排序整型数组
c
#include <stdio.h>
#include <stdlib.h>
// 比较函数:升序排列
int cmp_int(const void *e1, const void *e2)
{
return *(int *)e1 - *(int *)e2;
}
int main()
{
int arr[] = {3, 1, 5, 7, 2, 4, 9, 6, 8};
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_int);
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
输出结果 :1 2 3 4 5 6 7 8 9
关键点 :比较函数接收
const void *指针,需要先强制类型转换为实际类型再解引用。e1 - e2返回正数表示e1 > e2(升序),返回负数表示e1 < e2,返回 0 表示相等。
2.2 使用qsort排序结构数据
当排序结构体数据时,比较函数需要访问结构体的成员。这里就需要用到结构指针 和结构体成员访问操作符 ->。
结构指针与 -> 操作符详解
结构指针是指向结构体变量的指针,其定义方式为:
c
struct Student stu; // 定义结构体变量
struct Student *p = &stu; // 定义结构指针并指向stu
-> 操作符(箭头操作符)用于通过结构指针访问结构体成员:
c
p->name // 等价于 (*p).name
p->age // 等价于 (*p).age
p->score // 等价于 (*p).score
重点 :
p->member是(*p).member的语法糖,两者完全等价。箭头操作符更简洁直观,是 C 语言中通过指针访问结构体成员的标准写法。
示例:按成绩排序学生信息
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 定义学生结构体
typedef struct Student
{
char name[20];
int age;
float score;
} Student;
// 按成绩升序排序的比较函数
int cmp_by_score(const void *e1, const void *e2)
{
// e1、e2 是 void* 指针,先强转为结构指针
Student *s1 = (Student *)e1;
Student *s2 = (Student *)e2;
// 使用 -> 操作符访问结构体成员
if (s1->score > s2->score)
return 1;
else if (s1->score < s2->score)
return -1;
else
return 0;
}
// 按年龄升序排序的比较函数
int cmp_by_age(const void *e1, const void *e2)
{
// 直接通过强制类型转换 + 箭头操作符访问成员
return ((Student *)e1)->age - ((Student *)e2)->age;
}
// 按姓名排序(字典序)
int cmp_by_name(const void *e1, const void *e2)
{
return strcmp(((Student *)e1)->name, ((Student *)e2)->name);
}
int main()
{
Student students[] = {
{"张三", 20, 88.5},
{"李四", 19, 92.0},
{"王五", 21, 76.5},
{"赵六", 20, 95.5}
};
int sz = sizeof(students) / sizeof(students[0]);
// 按成绩排序
qsort(students, sz, sizeof(students[0]), cmp_by_score);
printf("按成绩升序排列:\n");
for (int i = 0; i < sz; i++)
{
// 结构体变量用 . 访问成员,结构指针用 -> 访问成员
printf("姓名:%s 年龄:%d 成绩:%.1f\n",
students[i].name,
students[i].age,
students[i].score);
}
return 0;
}
输出结果:
按成绩升序排列:
姓名:王五 年龄:21 成绩:76.5
姓名:张三 年龄:20 成绩:88.5
姓名:李四 年龄:19 成绩:92.0
姓名:赵六 年龄:20 成绩:95.5
结构指针与 -> 操作符要点总结
| 概念 | 说明 | 示例 |
|---|---|---|
| 结构指针 | 指向结构体变量的指针 | Student *p = &stu; |
-> 操作符 |
通过指针访问成员 | p->score |
. 操作符 |
通过变量直接访问成员 | stu.score |
| 等价写法 | p->member = (*p).member |
两者完全等价 |
| 在 qsort 中的应用 | 将 void* 强转为结构指针后,用 -> 访问待比较的成员 |
((Student *)e1)->score |
重点强调:
qsort的比较函数参数是const void *,必须先强制类型转换为实际的结构指针类型才能访问成员。- 访问结构体成员时:结构变量用
.,结构指针用->。->操作符的优先级高于强制类型转换,因此(Student *)e1->score是错误的写法,必须加括号:((Student *)e1)->score。
2.3 综合对比:排序整型 vs 排序结构
| 对比项 | 排序整型数据 | 排序结构数据 |
|---|---|---|
| 比较函数参数 | const void * |
const void * |
| 类型转换 | *(int *)e1 |
((Student *)e1)->score |
| 访问方式 | 直接解引用 | 通过 -> 访问成员 |
| 比较依据 | 元素本身的值 | 结构体中的某个成员 |
| 典型场景 | 数组排序 | 学生成绩、员工信息等结构化数据排序 |
3. qsort函数的模拟实现
理解了 qsort 的使用方法后,我们来深入其底层原理------使用回调函数 机制,模拟实现一个通用的冒泡排序版本 my_qsort。
3.1 设计思路
qsort 的核心在于通用性 :它不知道要排序的数据类型,只通过 void* 指针操作内存。其设计思想如下:
-
参数通用化 :接收
void* base(起始地址)、size_t num(元素个数)、size_t size(元素大小) -
比较回调化 :通过函数指针
int (*cmp)(const void*, const void*)让用户自定义比较规则 -
交换字节化:由于不知道具体类型,交换元素时按字节逐个交换
流程图:
my_qsort(base, num, size, cmp)
│
├─ 外层循环 i = 0 → num-1
│ └─ 内层循环 j = 0 → num-1-i
│ ├─ 计算第j个元素地址:base + j*size
│ ├─ 计算第j+1个元素地址:base + (j+1)*size
│ ├─ 调用 cmp(elem_j, elem_j+1) 比较
│ └─ 若返回值 > 0,则交换两个元素(按字节交换)
└─ 排序完成
3.2 核心代码实现
c
#include <stdio.h>
#include <string.h>
// 按字节交换两个内存块
// 因为不知道具体类型,所以逐字节交换
void Swap(char *buf1, char *buf2, size_t size)
{
size_t i = 0;
for (i = 0; i < size; i++)
{
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
// 模拟实现qsort(基于冒泡排序)
// base:待排序数组首地址
// num:元素个数
// size:每个元素的大小(字节数)
// cmp:比较函数的函数指针(回调函数)
void my_qsort(void *base, size_t num, size_t size,
int (*cmp)(const void *, const void *))
{
size_t i = 0;
size_t j = 0;
// 外层循环:排序趟数
for (i = 0; i < num - 1; i++)
{
// 内层循环:每趟比较次数
for (j = 0; j < num - 1 - i; j++)
{
// 计算第j个元素的地址
// 将 base 强转为 char*,按字节偏移
char *elem_j = (char *)base + j * size;
// 计算第j+1个元素的地址
char *elem_j1 = (char *)base + (j + 1) * size;
// 调用回调函数比较两个元素
// 如果返回值 > 0,说明 elem_j > elem_j1,需要交换
if (cmp(elem_j, elem_j1) > 0)
{
Swap(elem_j, elem_j1, size);
}
}
}
}
重点解析:
(char *)base + j * size:将void*转为char*,因为char占 1 字节,这样指针加j * size就能精确跳到第 j 个元素的起始地址。Swap函数按字节交换,保证了无论什么类型的数据都能正确交换。cmp是回调函数,由调用者提供,my_qsort只负责调用它来判断大小关系。
3.3 测试:排序整型数据
c
// 比较整型(升序)
int cmp_int(const void *e1, const void *e2)
{
return *(int *)e1 - *(int *)e2;
}
int main()
{
int arr[] = {3, 1, 5, 7, 2, 4, 9, 6, 8};
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
printf("排序前:");
for (i = 0; i < sz; i++)
printf("%d ", arr[i]);
printf("\n");
// 使用我们模拟的 my_qsort 排序
my_qsort(arr, sz, sizeof(arr[0]), cmp_int);
printf("排序后:");
for (i = 0; i < sz; i++)
printf("%d ", arr[i]);
printf("\n");
return 0;
}
输出结果:
排序前:3 1 5 7 2 4 9 6 8
排序后:1 2 3 4 5 6 7 8 9
3.4 测试:排序结构数据
c
#include <string.h>
typedef struct Student
{
char name[20];
int age;
float score;
} Student;
// 按成绩升序
int cmp_by_score(const void *e1, const void *e2)
{
// 先强转为结构指针,再用 -> 访问成员
float s1 = ((Student *)e1)->score;
float s2 = ((Student *)e2)->score;
if (s1 > s2) return 1;
else if (s1 < s2) return -1;
else return 0;
}
int main()
{
Student students[] = {
{"张三", 20, 88.5},
{"李四", 19, 92.0},
{"王五", 21, 76.5},
{"赵六", 20, 95.5}
};
int sz = sizeof(students) / sizeof(students[0]);
int i = 0;
printf("排序前:\n");
for (i = 0; i < sz; i++)
printf("姓名:%s 年龄:%d 成绩:%.1f\n",
students[i].name, students[i].age, students[i].score);
// 使用 my_qsort 按成绩排序
my_qsort(students, sz, sizeof(students[0]), cmp_by_score);
printf("\n按成绩升序排列:\n");
for (i = 0; i < sz; i++)
printf("姓名:%s 年龄:%d 成绩:%.1f\n",
students[i].name, students[i].age, students[i].score);
return 0;
}
输出结果:
排序前:
姓名:张三 年龄:20 成绩:88.5
姓名:李四 年龄:19 成绩:92.0
姓名:王五 年龄:21 成绩:76.5
姓名:赵六 年龄:20 成绩:95.5
按成绩升序排列:
姓名:王五 年龄:21 成绩:76.5
姓名:张三 年龄:20 成绩:88.5
姓名:李四 年龄:19 成绩:92.0
姓名:赵六 年龄:20 成绩:95.5
3.5 关键知识点总结
| 知识点 | 说明 | 代码示例 |
|---|---|---|
void* 指针运算 |
void* 不能直接加减,需强转为 char* 再按字节偏移 |
(char*)base + j * size |
| 按字节交换 | 通过 char* 逐字节交换,实现通用数据交换 |
Swap(char *buf1, char *buf2, size_t size) |
| 回调函数 | 比较函数由调用者提供,my_qsort 通过函数指针调用 |
cmp(elem_j, elem_j1) |
| 函数指针参数 | int (*cmp)(const void*, const void*) 声明回调接口 |
my_qsort(arr, sz, size, cmp_int) |
| 类型无关性 | 同一套排序逻辑可排序任意类型数据 | 整型、结构体均可 |
3.6 与标准库 qsort 的对比
| 对比项 | 标准库 qsort |
模拟实现 my_qsort |
|---|---|---|
| 算法 | 快速排序(平均 O(n log n)) | 冒泡排序(O(n²)) |
| 参数 | 完全相同 | 完全相同 |
| 回调机制 | 使用函数指针 | 使用函数指针 |
| 通用性 | 任意类型 | 任意类型 |
| 性能 | 高效 | 教学演示用 |
核心思想 :虽然算法不同,但回调函数的设计模式 完全一致------框架(排序函数)负责流程控制,回调(比较函数)负责具体规则。这就是
qsort能够排序任意类型数据的根本原因。
3.7 完整代码汇总
c
#include <stdio.h>
#include <string.h>
// 按字节交换
void Swap(char *buf1, char *buf2, size_t size)
{
size_t i = 0;
for (i = 0; i < size; i++)
{
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
// 模拟qsort(冒泡排序版)
void my_qsort(void *base, size_t num, size_t size,
int (*cmp)(const void *, const void *))
{
size_t i = 0, j = 0;
for (i = 0; i < num - 1; i++)
{
for (j = 0; j < num - 1 - i; j++)
{
char *elem_j = (char *)base + j * size;
char *elem_j1 = (char *)base + (j + 1) * size;
if (cmp(elem_j, elem_j1) > 0)
{
Swap(elem_j, elem_j1, size);
}
}
}
}
// 比较整型
int cmp_int(const void *e1, const void *e2)
{
return *(int *)e1 - *(int *)e2;
}
// 测试整型排序
void test_int()
{
int arr[] = {3, 1, 5, 7, 2, 4, 9, 6, 8};
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
printf("排序前:");
for (i = 0; i < sz; i++) printf("%d ", arr[i]);
printf("\n");
my_qsort(arr, sz, sizeof(arr[0]), cmp_int);
printf("排序后:");
for (i = 0; i < sz; i++) printf("%d ", arr[i]);
printf("\n\n");
}
// 学生结构体
typedef struct Student
{
char name[20];
int age;
float score;
} Student;
// 比较成绩
int cmp_by_score(const void *e1, const void *e2)
{
float s1 = ((Student *)e1)->score;
float s2 = ((Student *)e2)->score;
if (s1 > s2) return 1;
else if (s1 < s2) return -1;
else return 0;
}
// 测试结构体排序
void test_struct()
{
Student students[] = {
{"张三", 20, 88.5},
{"李四", 19, 92.0},
{"王五", 21, 76.5},
{"赵六", 20, 95.5}
};
int sz = sizeof(students) / sizeof(students[0]);
int i = 0;
printf("排序前:\n");
for (i = 0; i < sz; i++)
printf("姓名:%s 年龄:%d 成绩:%.1f\n",
students[i].name, students[i].age, students[i].score);
my_qsort(students, sz, sizeof(students[0]), cmp_by_score);
printf("\n按成绩升序排列:\n");
for (i = 0; i < sz; i++)
printf("姓名:%s 年龄:%d 成绩:%.1f\n",
students[i].name, students[i].age, students[i].score);
}
int main()
{
test_int();
test_struct();
return 0;
}
3.8 总结
通过模拟实现 qsort,我们深刻理解了回调函数的强大之处:
void*指针 + 按字节操作 实现了类型无关性- 回调函数(函数指针) 实现了比较规则的解耦
- 同一套排序逻辑 可以排序整型、结构体等任意数据类型
- 这种框架 + 回调的设计模式在 C 语言中广泛应用(如信号处理、线程创建等)