目录
1. 首先我们需要明确bubble_myqsort函数的参数
一.回调函数?
1.1 什么是回调函数呢?
回调函数就是一个通过函数指针调用的函数
示例如下
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int Add(int x, int y)//回调函数Add
{
return x + y;
}
void text(int (*pf)(int, int))
{
int r = pf(10, 20);
printf("%d", r);
}
int main()
{
text(Add);//text是调用Add的那方
return 0;
}
其中Add被称为回调函数
即如果你把函数的指针(地址)作为参数传递给另一个函数 当这个指针被用来调用其所指向的函数时 被调用的函数就是回调函数 回调函数不是由该函数的实现方调用 而是在特定的事件或条件发生时由另外的一方调用的 用于对该事件或条件进行响应
1.2 回调函数有什么用呢?
在上一节《C 语言指针高级指南:字符、数组、函数指针的进阶攻略》中 我们学习了用函数指针数组来实现了简单计算器
现在让我们通过回调函数来对该代码进行优化
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
// 定义运算函数
double add(double a, double b) { return a + b; }
double sub(double a, double b) { return a - b; }
double mul(double a, double b) { return a * b; }
double div(double a, double b) { return a / b; }
void calc(double(*pf)(double, double))//通过calc函数来调用运算函数
{
printf("请输入两个操作数");
double a,b;
scanf("%lf %lf", &a, &b);
double r = pf(a,b);
printf("%lf\n", r);
}
int main() {
int choice;
double x, y;
do
{
printf("选择运算:\n0. 退出计算器\n1. 加\n2. 减\n3. 乘\n4.除\n");
scanf("%d", &choice);
switch (choice)//使用switch来选择使用的函数
{
case 0:
break;
case 1:
calc(add);
break;
case 2:
calc(sub);
break;
case 3:
calc(mul);
break;
case 4:
calc(div);
break;
default:
printf("输入错误 请重新输入");
break;
}
} while (choice);
return 0;
}
代码调用逻辑如图 示例 调用除法运算
首先通过switch找到需要执行的calc函数 在通过calc函数来调用对应的运算函数
其中运算函数就被称为回调函数 而calc函数则是调用回调函数的一方
其中关于calc函数解释如下
二.qsort使用实例
2.1 什么是qsort函数呢?
qsort
是 C 标准库(<stdlib.h>
)中提供的快速排序函数,用于对任意类型的数组进行排序。它的核心特点是通过回调函数自定义排序规则,因此非常灵活。
那它有什么优势呢? 为什么要去学习它呢?
1. 它是现成的排序算法 学会了直接就能使用 不需要自己去实现具体逻辑
2. 大部分情况下 效率比较高
3. qsort函数可以排序任意类型的数据
首先让我们看一下官方给的解释
估计你可能看了也是一头雾水 让我们来解释一下 首先让我们来了解一下qsort函数的参数
void qsort(
void *base, // 待排序数组的首地址
size_t nmemb, // 数组元素个数
size_t size, // 每个元素的大小(字节数)
int (*compar)(const void *, const void *) // 比较函数
);
函数参数说明:
base
(void* 类型)
- 指向要排序的数组的第一个元素的指针
- 由于是
void*
类型,可以接受任何数据类型的数组
nmemb
(size_t 类型)
- 数组中元素的数量
- 例如:对于
int arr[10]
,这个值就是 10
size
(size_t 类型)
- 数组中每个元素的大小(以字节为单位)
- 通常使用
sizeof
运算符获取,如sizeof(int)
compar
(函数指针)
- 指向比较函数的指针
- 这个函数决定了排序的顺序
其中可能你只会对比较函数有些疑问 那让我们来继续深入了解他
因为qsort并不清楚你需要比较的元素的类型或者说并不知道你比较的规则 因此qsort传入的参数是void*的类型 在你的自定义函数里 你需要自行进行强制类型转换来使用 这是因为void类型的指针解引用是无法进行比较的
2.2 qsort返回值 深度解析
在C语言的qsort
函数中,自定义比较函数的返回值是控制排序行为的核心机制。它的返回值不仅决定了元素的相对顺序,还直接影响排序算法的正确性和效率。下面从底层原理、返回值的数学意义和实际应用三个层次进行深刻讲解:
一、底层原理:qsort
如何利用返回值?
qsort
是一个通用的排序算法(通常是快速排序的实现),它通过反复调用比较函数来决定如何交换和分区元素。具体过程:
算法内部会多次调用比较函数,传入两个元素的指针(
p1
和p2
)。根据返回值决定是否交换元素:
若返回值
< 0
:认为p1
应排在p2
左侧(即p1
“小于”p2
)。若返回值
> 0
:认为p1
应排在p2
右侧(即p1
“大于”p2
)。若返回值
= 0
:认为两者相等,顺序无关紧要。🔍 关键点:
qsort
本身不关心具体数据类型,它只依赖比较函数的返回值来指导排序。
二、返回值的数学意义:三值逻辑
比较函数的返回值本质上是对元素序关系的数学描述。它实现了三值逻辑:
负值
:表示p1 < p2
(p1
应在前)。
零
:表示p1 == p2
(顺序任意)。
正值
:表示p1 > p2
(p1
应在后)。
经典实现模式:
int cmp_int(const void* p1, const void* p2) {
int a = *(const int*)p1;
int b = *(const int*)p2;
return (a > b) - (a < b); // 安全且无溢出的写法
}
-
这种写法避免了直接返回
a - b
可能导致的整数溢出问题(例如a=INT_MAX, b=-1
时溢出)。
三、实际应用:如何通过返回值控制排序?
1. 升序排序(默认)
int cmp_asc(const void* p1, const void* p2) {
return (*(int*)p1 - *(int*)p2); // p1 > p2时返回正,p1排在后面
}
2. 降序排序
int cmp_desc(const void* p1, const void* p2) {
return (*(int*)p2 - *(int*)p1); // 故意用p2减p1,反转顺序
}
2.3 qsort函数中的自定义比较函数
如果你问到 qsort函数中什么最重要 那一定是自定义比较函数最重要了
在C语言的qsort
函数中,最重要的部分是自定义的比较函数(即cmp_int
函数)。这是因为:
1. 比较函数决定排序规则
qsort
本身不知道如何比较数组元素(因为元素可能是整数、结构体、字符串等)。- 比较函数需要明确告诉
qsort
:
- 如何判断两个元素的顺序(升序、降序或自定义规则)。
- 返回值必须符合以下约定:
< 0
:p1
应排在p2
前面(升序时表示p1 < p2
)。> 0
:p1
应排在p2
后面(升序时表示p1 > p2
)。= 0
:两者相等,顺序无关紧要。
2. 为什么比较函数最关键?
- 灵活性:通过修改比较函数,可以轻松实现:
- 升序/降序排序。
- 对结构体按某个字段排序。
- 自定义复杂规则(如字符串按长度排序)。
2.4 qsort函数实现整型数据的排序
qsort中最重要的就是比较函数的实现 那首先就让我们来实现它
int cmp_int(const void* p1, const void* p2)
{
if (*(int*)p1 > *(int*)p2)
return 1;
if (*(int*)p1 < *(int*)p2)
return -1;
else
return 0;
}
我们在这里定义实现了比较函数 com_int 我们将p1和p2强制类型转换为了int类型的指针 再解引用进行比较
- 将指针转换为
int*
后比较值:
- 如果
p1
的值 >p2
的值,返回1
(表示p1
应排在p2
后)。- 如果
p1
的值 <p2
的值,返回-1
(表示p1
应排在p2
前)。- 如果相等,返回
0
。
我们可以对比较函数进行优化 如下
int cmp_int(const void* p1, const void* p2)
{
return (*(int*)p1-*(int*)p2);
}
直接返回p1与p2的差 若p1>p2 则返回大于0的数 表示p1应该排在p2后面
若p1<p2则返回小于0的数 表示p2应该排在p1后面 这样就能完成升序
具体完整代码如下
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include<stdlib.h>
int cmp_int(const void* p1, const void* p2)
{
return (*(int*)p1-*(int*)p2);
}
void printf_arr(const int* p,int sz)
{
for (int i = 0;i < sz;i++)
{
printf("%d ", *(p+i));
}
printf("\n");
}
int main()
{
int arr[] = { 1,5,3,8,6,7,9,2,4,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
printf_arr(arr, sz);
qsort(arr, sz, sizeof(arr[0]), cmp_int);
printf_arr(arr,sz);
}
运行结果如下:
2.5 qsort函数实现结构体数据中整型的排序
首先 我们需要创建出结构体变量
struct Stu
{
char name[50];
int age;
};
其次 我们需要对结构体变量进行赋值
struct Stu arr[] = { { "cat",10 }, { "dog",20 }, { "pig",30 } };
其次我们需要写出比较函数 那我们则需要明确比较规则
int cmp_Stu_age(const void*p1, const void* p2)
{
return (*(struct Stu*)p1).age - (*(struct Stu*)p2).age;
}
//将p1和p2强制类型转换为结构体变量指针 在解引用调用其中的age
//将p1与p2的差作为返回值返回给qsort函数
最后就是调用qsort代码 以下是完整代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include<stdlib.h>
struct Stu//结构体类型创建
{
char name[50];
int age;
};
int cmp_Stu_age(const void*p1, const void* p2)//自定义比较函数实现
{
return (*(struct Stu*)p1).age - (*(struct Stu*)p2).age;
}
void printf_stu(struct Stu arr[], int sz)//打印函数实现
{
for (int i = 0;i < sz;i++)
{
printf("%s:%d\n", arr[i].name, arr[i].age);
}
printf("\n");
}
void text()
{
struct Stu arr[] = { { "cat",100 }, { "dog",50 }, { "pig",80 } };//结构体变量数组创建
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]),cmp_Stu_age);//调用qsort函数
printf_stu(arr, sz);
}//调用打印函数
int main()
{
text();
return 0;
}
下面是该代码的运行结果
可以看到 的确通过age 实现了升序排序
其中我们可以对下面这块代码进行修改
int cmp_Stu_age(const void*p1, const void* p2)//自定义比较函数实现
{
return (*(struct Stu*)p1).age - (*(struct Stu*)p2).age;
}
修改之后如下
int cmp_Stu_age(const void*p1, const void* p2)
{
return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}
这时我们就不得不了解一下结构体成员访问操作符: ->
2.6 结构体访问操作符 ->
在C语言中,访问结构体成员有两种操作符:
.
(点操作符):用于直接访问结构体变量的成员->
(箭头操作符):用于通过指针访问结构体成员的成员
1. 点操作符 .
(直接访问)
语法:
结构体变量名.成员名
使用场景:
-
当你有结构体变量(非指针)时使用
-
直接访问结构体的成员
示例:
#include <stdio.h>
struct Student {
char name[50];
int age;
float score;
};
int main() {
// 创建结构体变量
struct Student stu1;
// 使用点操作符赋值
strcpy(stu1.name, "Alice");
stu1.age = 20;
stu1.score = 89.5;
// 使用点操作符访问
printf("Name: %s\n", stu1.name);
printf("Age: %d\n", stu1.age);
printf("Score: %.1f\n", stu1.score);
return 0;
}
输出:
Name: Alice
Age: 20
Score: 89.5
2. 箭头操作符 ->
(指针访问)
语法:
结构体指针->成员名
等价形式:
(*结构体指针).成员名
使用场景:
-
当你有结构体指针时使用
-
通过指针间接访问结构体成员
示例:
#include <stdio.h>
#include <stdlib.h>
struct Student {
char name[50];
int age;
float score;
};
int main() {
// 创建结构体指针
struct Student *pStu = malloc(sizeof(struct Student));
// 使用箭头操作符赋值
strcpy(pStu->name, "Bob");
pStu->age = 21;
pStu->score = 92.5;
// 使用箭头操作符访问
printf("Name: %s\n", pStu->name);
printf("Age: %d\n", pStu->age);
printf("Score: %.1f\n", pStu->score);
free(pStu);
return 0;
}
输出:
Name: Bob
Age: 21
Score: 92.5
3. 两种操作符的对比
操作符 | 使用场景 | 示例 | 等价形式 |
---|---|---|---|
| 结构体变量 |
| - |
| 结构体指针 |
|
|
重要区别:
-
.
直接用于结构体变量 ->
用于结构体指针,是"解引用+访问"的简写形式
2.7 qsort函数实现结构体数据中字符串的排序
之前 我们以年龄作为比较条件 成功实现了结构体数据中整型的排序 现在让我们以名字作为判断条件 来实现排序
首先 我们需要明确自定义函数中比较函数的判断条件是什么?
名字属于字符串 而字符串怎么排序呢? 那就要使用到strcmp函数了
该函数用于比较两个字符串 并且返回类型解释如下
注: 字符串比较大小 不是比长度 而已比较相应位置上字符的ASCLL码值的大小
比如 abc 与 abcde 比较 abc相等 但abc后还有\0 因此\0与d比较 d大 因此abcde大于abc
下面让我们使用具体函数来测试 代码如下
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include<stdlib.h>
struct Stu
{
char name[50];
int age;
};
int cmp_Stu_name(const void*p1, const void* p2)
{
return strcmp(((struct Stu*)p1)->name,((struct Stu*)p2)->name);
}
void printf_stu(struct Stu arr[], int sz)
{
for (int i = 0;i < sz;i++)
{
printf("%s:%d\n", arr[i].name, arr[i].age);
}
printf("\n");
}
void text()
{
struct Stu arr[] = {{"dog",50}, {"cat",100}, {"pig",80}};
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]),cmp_Stu_name);
printf_stu(arr, sz);
}
int main()
{
text();
return 0;
}
运行结果如下:
与使用年龄进行排序的代码差别 只在于自定义比较函数的实现不同(即比较方法不同) 因此结果也不同
int cmp_Stu_name(const void*p1, const void* p2)
{
return strcmp(((struct Stu*)p1)->name,((struct Stu*)p2)->name);
}
三.qsort函数的模拟实现
下面 让我们使用之前学过的冒泡排序来对qsort函数进行模拟实现 来加深自己的学习
下面是完整代码 我将分段讲解
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int int_cmp(const void* p1, const void* p2)
{
return (*(int*)p1 - *(int*)p2);
}
void swap(char* p1, char* p2, size_t width)
{
for (int i = 0;i < width;i++)
{
char temp = *(p1 + i);
*(p1 + i) = *(p2 + i);
*(p2 + i) = temp;
}
}
void bubble_mysort(void* base, int sz, int width, int(*cmp)(void*, void*))
{
for (int i = 0;i < sz;i++)
{
for (int j = 0;j < sz - 1 - i;j++)
{
if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
{
swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
}
}
}
}
void print(int arr[], int sz)
{
for (int i = 0;i < sz;i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[] = { 1,5,9,6,0,3,8,7,4,2};
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_mysort(arr, sz, sizeof(arr[0]), int_cmp);
print(arr, sz);
}
1. 首先我们需要明确bubble_myqsort函数的参数
void bubble_sort(void* base, int sz, int width, int(*cmp)(void*, void*))
2. 自定义比较函数 int_cmp
int int_cmp(const void* p1, const void* p2)
{
return (*(int*)p1 - *(int*)p2);
}
- 作用:比较两个整数值
- 参数:两个
void*
指针,指向要比较的元素- 实现:
- 将
void*
强制转换为int*
然后解引用- 返回
p1 - p2
的结果:
- 若
p1 > p2
,返回正数- 若
p1 < p2
,返回负数- 若相等,返回0
3. 交换函数 swap
void swap(char* p1, char* p2, size_t width)
{
for (int i = 0; i < width; i++)
{
char temp = *(p1 + i);
*(p1 + i) = *(p2 + i);
*(p2 + i) = temp;
}
}
- 作用:交换两个内存块的内容
- 参数:
p1
,p2
:指向要交换的内存块的指针width
:每个元素的大小(字节数)- 实现:
- 逐字节交换两个内存块的内容
- 使用
char*
因为它是1字节的最小可寻址单元- 优点:通用性强,可以交换任何类型的数据
- 实现原理:
- 外层循环控制排序轮数
- 内层循环比较相邻元素
- 使用
(char*)base + j*width
计算元素地址:
- 将
base
转为char*
以便进行字节级指针运算j*width
计算第j个元素的偏移量- 调用比较函数决定是否交换
- 特点:
- 模仿标准库
qsort
的接口设计- 可以排序任何类型的数据
- 时间复杂度O(n²)
4. 关键点解析
通用排序的实现:
- 使用
void*
和元素宽度width
实现对任意数据类型的支持- 通过函数指针
cmp
实现自定义比较逻辑指针运算技巧:
(char*)base + j * width
char*
指针算术以字节为单位j*width
计算第j个元素的偏移量
本篇内容到此结束 如果对你有所帮助 希望能一键三连 谢谢
往期回顾:
《初探指针世界:揭开内存管理与编程优化的第一篇章》-----指针一
《C 语言指针进阶:const 修饰、断言机制与传址调用深度解析》----指针二
《C 语言指针高级指南:字符、数组、函数指针的进阶攻略》----指针三
《从回调函数到 qsort:C 语言指针高级应用全攻略》----指针四