指针(4)
目录
2.2 qsort 实战 1:排序整型数据(避坑 void*)
2.3 qsort 实战 2:排序结构体(按名字 / 年龄)
✨引言:
普通排序只能排整型?太局限了!C 语言的回调函数 + 通用指针能让排序 “通吃” 任意数据 —— 整型、字符串、结构体全搞定!今天从 qsort 库函数拆解到手写通用冒泡排序,用 “调料包 + 搬家公司” 的通俗比喻,把 “万能排序” 的底层逻辑扒得明明白白~
1. 🎯回调函数:代码复用的 “魔法调料包”
回调函数不是 “直接调用”,而是 “通过指针间接调用”—— 主逻辑像 “餐厅后厨”,回调函数像 “不同口味的调料包”,后厨不变,换调料包就能做出不同菜!
1.1 回调函数的定义(原码 + 通俗解读)
回调函数 = 由函数指针调用的函数(如calc里的add)
主调函数 = 负责框架的函数(如calc)
💡举例:
- 排序函数(主调)负责 “两两比较 + 交换” 的框架;
- 比较函数(回调)负责 “怎么比”(整型比大小 / 字符串比 ASCII / 结构体比年龄)。
1.2 为什么需要回调函数?
如果没有回调函数,排序整型要写一个冒泡,排序结构体又要写一个冒泡 —— 代码冗余到爆炸!有了回调函数,一个排序框架适配所有数据类型,这就是 “抽象” 的魅力~
2. 🛠️qsort 库函数:C 语言的 “万能排序神器”
qsort 是 C 标准库的 “瑞士军刀”,底层用快速排序实现,能排序任意类型数据 —— 核心是靠你提供的 “比较回调函数”!
2.1 qsort 函数原型:4 个参数的 “分工协作”
void qsort(
void* base, // 待排序数组首地址(万能指针:能接int/char/结构体地址)
size_t num, // 数组元素个数
size_t size, // 单个元素的字节大小(int占4,结构体占24...)
int(*compar)(const void*, const void*) // 比较回调函数指针
);
📌compar 函数规则(必须记!):
- 返回 > 0 → p1 元素 > p2 元素
- 返回 < 0 → p1 元素 < p2 元素
- 返回 0 → 两元素相等
2.2 qsort 实战 1:排序整型数据(避坑 void*)
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
// 整型比较回调函数
int cmp_int(const void* p1, const void* p2)
{
// ❌易错点:void*是“无类型指针”,不能直接解引用/±整数!
// return *p1 - *p2;
return *(int*)p1 - *(int*)p2; // ✅先转成int*再解引用
}
void test1()
{
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_int); // 传入回调函数
print_arr(arr, sz); // 输出:0 1 2 3 4 5 6 7 8 9
}
💡void * 指针的 “万能插座” 特性:能接任意类型的地址,但必须 “转换插头”(强制类型转换)才能用 —— 就像万能插座能插国标 / 美标插头,但要适配才能通电!
2.3 qsort 实战 2:排序结构体(按名字 / 年龄)
结构体比较需要 “指定维度”,比如按名字(字符串)或年龄(整型):
struct Stu
{
char name[20];
int age;
};
// 按名字比较(字符串用strcmp)
int cmp_stu_by_name(const void* p1, const void* p2)
{
// ❌易错点:原代码漏写return!strcmp的返回值要返回才生效!
return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}
// 按年龄比较(整型直接减)
int cmp_stu_by_age(const void* p1, const void* p2)
{
return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}
// 打印结构体数组
void print_stu(struct Stu arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("姓名:%s 年龄:%d\n", arr[i].name, arr[i].age);
}
}
void test2()
{
struct Stu arr[3] = { {"zhangsan",20},{"lisi",35},{"wangwu",18} };
int sz = sizeof(arr) / sizeof(arr[0]);
// 按年龄排序(结果:wangwu(18)→zhangsan(20)→lisi(35))
qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
print_stu(arr, sz);
// 按名字排序(注释上面,打开下面)
// qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
// print_stu(arr, sz); // 结果:lisi→wangwu→zhangsan(按ASCII)
}
📌结构体访问小贴士:
- 直接访问:
结构体变量.成员名(如arr[i].name) - 间接访问:
结构体指针->成员名(如((struct Stu*)p1)->name)👉 字符串比较必须用strcmp,不能用>/<—— 因为字符串名是地址,比的是地址不是内容!
3. ✍️手写通用冒泡排序:复刻 qsort 的核心逻辑
普通冒泡排序只能排整型?我们改造它,让它变成 “万能排序”—— 思路和 qsort 一致:框架通用 + 细节回调!
3.1 void * 指针:“万能地址接收器”
用void* base接收任意数组的首地址,不管是 int 数组还是结构体数组,都能接下~
3.2 Swap 函数:逐字节 “搬家” 的交换技巧
不同类型的元素大小不同(int 占 4 字节,结构体占 24 字节),交换要像 “搬家公司”—— 不管房子多大,按 “单个箱子(字节)” 搬:
void Swap(char* buf1, char* buf2, size_t width)
{
int i = 0;
for (i = 0; i < width; i++) // width是元素的字节大小
{
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++; // 逐个字节交换,搬完width个字节就搞定
buf2++;
}
}
💡为什么用 char*?因为 char 是 1 字节,是 C 语言最小的内存单位,能精准操作每一个字节!
3.3 偏移计算:找到任意元素的 “精准地址”
要访问arr[j],需要用(char*)base + width*j计算地址:
(char*)base:把 void转成 char,才能计算偏移(void * 不能 ± 整数);width*j:j 个元素的总字节数(比如 int 占 4 字节,j=2 就是 8 字节偏移)。
完整通用冒泡排序实现(原码 + 标注)
// 通用冒泡排序框架
void bubble_sort(void* base, size_t sz, size_t width, int (*cmp)(const void* p1, const void* p2))
{
int i = 0;
for (i = 0; i < sz - 1; i++) // 趟数:n个元素n-1趟
{
int j = 0;
for (j = 0; j < sz - 1 - i; j++) // 每趟比较次数递减
{
// 计算arr[j]和arr[j+1]的地址
char* elem1 = (char*)base + width * j;
char* elem2 = (char*)base + width * (j + 1);
// 调用回调函数比较,需要交换则调用Swap
if (cmp(elem1, elem2) > 0)
{
Swap(elem1, elem2, width);
}
}
}
}
// 测试:排序整型
void test3()
{
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
print_arr(arr, sz);
}
// 测试:排序结构体(按年龄)
void test4()
{
struct Stu arr[3] = { {"zhangsan",20},{"lisi",35},{"wangwu",18} };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
print_stu(arr, sz);
}
👉 测试结果和 qsort 完全一致!这就是 “通用排序” 的魔力 —— 框架不变,换个比较回调函数就能排任意数据~
4. 📝核心知识点总结:通用排序的 3 个关键
- void * 指针:万能地址接收器,适配任意数据类型(但要转成 char * 才能计算偏移);
- 回调函数:把 “怎么比较” 的细节交给用户,实现框架通用;
- 逐字节操作:用 char*+width 实现任意大小元素的交换(Swap 函数)。
💡一句话记住:框架通用靠 void,细节定制靠回调,内存操作靠 char*!
🎉最后:从 qsort 到手写排序,我们学会了什么?
C 语言的 “通用” 思想 —— 不是写死逻辑,而是把变化的部分(比如比较规则)抽象成回调函数,固定的部分(比如排序框架)写成通用代码。这种思想不仅用在排序,还能用在遍历、查找等场景,是 C 语言进阶的关键!如果这篇内容帮你打通了 “通用编程” 的任督二脉,欢迎点赞收藏🌟~
1615

被折叠的 条评论
为什么被折叠?



