【C语言库函数】回调函数的应用qsort函数的模拟实现
一、库函数qsort的简介
1、什么是qsort函数
qsort函数是C语言的一个库函数,它的头文件是stdio.h,这个函数的底层是用的是快速排序的思路,这个函数可以用来排序任何数据。
2、qsort函数的用法
如果我们在cplusplus这个网站上搜索qsort,就会看到它的如下定义:
如果第一次见,可能会被它的一系列参数搞得眼花缭乱。
我来给大家解释一下这些参数
第一个参数是一个指向待排序数组的第一个元素的voidl类型的指针;
第二个参数是待排序数组中元素的个数;
第三个参数是数组中每个元素的大小,单位为字节;
第四个参数是一个指向一个“用于比较两个元素的函数”的函数指针,这个函数的参数为两个void类型的指针,返回值类型为int。
且对于第四个参数指向的函数的返回值,还有以下规定:
意思是:
当第一个元素大于第二个元素时,返回一个大于0的整型数字;
当第一个元素等于第二个元素时,返回0;
当第一个元素小于第二个元素时,返回一个小于0的整型数字。
而且为了实现通用,着第四个参数执行的函数是由我们使用者自己提供的,也就是说我们使用者想要排序什么数据,就自己定义好比较这种数据的一个函数传给qsort。
下面我将通过两个例子来给大家演示一下这个函数的使用方式:
例1——排序排序整形数据
比如说我们现在有这样一个数组:
int arr[] = { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
我们想使用qsort将它拍成升序,该怎么办呢?
首先根据它的用法说明:我们需要先定义一个比较整形数据的函数。
int cmp_int(const void* p1, const void* p2) {
return *((int*)p1) - *((int*)p2);
}
为了传参不出错,我们需要把函数的返回值类型和参数类型与qsort定义的保持一致。
我们也可以定义一个打印整型数组的函数:
void print_arr(int* arr, int len) {
assert(arr);
int i = 0;
for (i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
接下来我们就可以进行排序了:
我们可以看到排序成功了,说明我们的使用方法是正确的。
接下来演示排序结构体
例2——排序结构体
假设我们定义了以下结构体和一个结构体数组:
struct boy {
int id;
int age;
char name[32];
};
struct boy b[] = { { 3, 23, "zhangsan"}, { 2, 18, "lisi" }, { 1, 22, "wangwu" }, { 0, 16, "zhouliu" } };
我们也可以定义一个打印结构体成员的函数:
void print_struct_boy(struct boy* p, int num) {
assert(p);
int i = 0;
for (i = 0; i < num; i++) {
printf("[ id = %d, age = %d, name = %s ]\n", p[i].id, p[i].age, p[i].name);
}
}
我们可以定义两个比较结构体的函数,分别根据id比和根据name比:
// 定义一个根据id来比较结构体的函数
int cmp_boy_by_id(const void* p1, const void* p2) {
return ((struct boy*)p1)->id - ((struct boy*)p2)->id;
}
// 定义一个根据name类比较结构体的函数
int cmp_boy_by_name(const void* p1, const void* p2) {
return strcmp(((struct boy*)p1)->name, ((struct boy*)p2)->name);
}
这里其实strcmp的返回值已经是和qsort规定的比较函数的返回值是一样的了,所以直接返回strcmp的返回值就行。
接下来我们就可以进行排序了:
我们可以看到排序的结果是正确的,也就说明我们的使用方法是正确的。
二、qsort函数的模拟实现
接下来我就给大家演示怎么模拟实现一个qsort函数,我这里使用的是冒泡排序的思路,但其实任何排序函数都是可以做到通用的,只要更改一下它们的比较方式即可。
根据冒泡排序的思路,我们很快就能写出我们自己的qsort函数的主体:
void my_qsort(void* base, size_t num, size_t size, int (*cmp)(const void*, const void*)) {
assert(base && cmp);
size_t i = 0;
size_t j = 0;
for (i = 0; i < num - 1; i++) {
int flag = 1; // 假设序列已经有序
for (j = 0; j < num - 1 - i; j++) {
if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0) {
// 进行交换
Swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
flag = 0;
}
}
if (flag) {
return;
}
}
}
有些朋友可能会对这里的一大串参数感到迷惑:
不知道为什么要这样写。
我来给大家解释一下:
其实既然是通用的,肯定就不能事先确定你要排序的数据是什么类型,所以这里base的类型是void*,那我们已经知道指针类型规定了指针的访问权限是一次访问多少个字节,所以函数是不会知道每跳过一个元素是要跳过多少个字节的内存的。
但我们知道数据都是一字节为单位在内存中存储的,例如在小段机器上一个int就占四个字节,它的存储如下图:
那我们是不是也可以字节为单位向后寻找地址呢?
这就是我们为什么要把,一个元素的长度size传给函数的原因,如果我们知道了一个元素占多少个字节,再用size乘以一字节就可以知道每次跳过一个元素该跳过多少个字节了。且因为char指针就是一次访问一个字节的,所以我们就要把base先转化成char类型的指针。
有了以上思路,那我们理解交换函数Swap也就水到渠成了:
void Swap(char* p1, char* p2, size_t size) {
assert(p1 && p2);
// 交换
size_t i = 0;
for (i = 0; i < size; i++) {
char temp = p1[i];
p1[i] = p2[i];
p2[i] = temp;
}
}
我们所做的就是把元素都看成是一串字节序列,然后逐字节的交换内容即可。
所以这也是为什么比较函数cmp不能写成通用的而交换函数能够写成通用的原因。
完成了这些工作,我们就来测试一下我们自己的qsort函数吧
先测测排序整数:
排序正确!
再来测测排序结构体:
排序正确!
说明我们的实现是没有问题的。