前言
这篇文章来分享一下一个C语言库函数里面的一个函数,叫qsort,它是一个用来进行排序的函数,
它的功能强大在于能对不同类型的数据进行排序,那么它究竟是怎么用?怎么实现呢?
本文将用简单的逻辑帮助你理解,它为什么能实现不同类型数据的排序。
qsort介绍
首先来看一下这个函数,他长这样:
![](https://i-blog.csdnimg.cn/blog_migrate/ad081ab630bc7eb26f61baf3f2c8417d.png)
来简单介绍一下它的一些基本信息:
函数名字是qsort,其实就是快速排序(quick sort)的简写
它包含在<stdlib.h>中
介绍一下参数
void* base | size_t num | size_t width | int(__cdecl*compare)(constvoid*elem1,constvoid*elem2) |
这是个参数是一个指针,它接受数组的起始地址 | 这个参数是数组元素个数,size_t是unsigned int 的重命名 | 这个参数是数组每一个元素的大小,单位是字节 | 这个参数的名字是 compare,它是一个函数指针 |
void *指针
(举例介绍void *指针的作用)
我们知道,指针有类型,指针的类型决定指针指向变量的类型,决定了指针移动的步长。
int main()
{
int a = 10;
int* p = &a;//整型指针接收整型变量的地址
char b= 'A';
char* pp = &b;//字符型指针接收字符变量的地址
*p = 200;//解引用,修改变量的值
*pp = 'C';//解引用,修改变量的值
int arr[10] = { 0 };
int* ppa = arr;
ppa++;//指针可以进行移动
return 0;
}
上面列举了一般的指针,指针的操作有 解引用和移动。
但是,很遗憾,对于void *的指针,他不能进行这两个操作,但是它能存放不同类型的地址。
![](https://i-blog.csdnimg.cn/blog_migrate/da7ae9d7456abaed552dbd3974437140.png)
明显,进行解引用和移动是错误的,但我们关键使用的是它能接受不同类型的地址。
函数指针
刚刚在qsort的参数里面也提到了函数指针,这个也要说明一下。
平时,我们知道,变量有地址,数组有地址,那么函数是否也有地址?函数名是什么?
答案是肯定的,函数有地址,函数名就是函数的地址。
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf)(int, int) = Add;//函数指针的定义
int ret = pf(3, 5);//函数指针的使用
printf("%d ", ret);
}
int (*pf)(int, int) = Add;//函数指针的定义
分析
这里pf是指针的名字,后面括号里面是指向函数的参数类型,前面的int 是指向函数的返回值
这样写是很合理的,因为你想,函数的关键就是:参数列表、返回值、函数名,这种定义的方法把这三个全都描述出来了!!
补充
Add和&Add得到的都是函数的地址,二者没有区别
指针调用函数,可以加上解引用运算符,也可以不加,所以这里函数名等价于指针名。
即,我们可以写出这样的代码:
int Add(int x, int y)
{
return x + y;
}
int main()
{
//int (*pf)(int, int) = Add;//函数指针的定义
int (*pf)(int, int) = &Add;
//int ret = pf(3, 5);//函数指针的使用
int ret = (*pf)(3, 5);
printf("%d ", ret);
}
我们再看回来刚刚参数列表的参数:
int( __cdecl* compare)(const void*elem1,const void*elem2);
compare是这个指针的名字,指向的函数的参数是两个const void *的指针,返回值是int
至于__cdecl,这个不用管,这是函数调用约定,不影响我们分析代码。
上面是我们补充的一些知识点,我们主要的任务是模拟qsort能实现不同数据类型排序的功能,我们采用的是一种常见的排序,冒泡排序来模拟这个功能。
冒泡排序
接下来回忆一下冒泡排序
冒泡排序我们分析的关键是趟数和次数的关系
两两进行比较,每一趟确定一个数据的正确位置
![](https://i-blog.csdnimg.cn/blog_migrate/701b0a39d9a139accfbba7ec20b46009.png)
上面分析了冒泡排序的过程,代码如下:
void Swap(int* a, int* b)
{
int tmep = *a;
*a = *b;
*b = tmep;
}
void Bubble_sort(int* arr, int sz)
{
for (int i = 0; i < sz - 1; i++)
{
for (int j = 0; j < sz - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
Swap(&arr[j], &arr[j + 1]);
}
}
}
}
qsort简单使用
*void qsort(void* base, size_t num, size_t width,
int (__cdecl *compare )(const void *elem1, const void *elem2 ));
案例
整型数据的排序
void Print(int* arr, int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int comp(const void* p1, const void* p2)
{
return *(int*)p1 - *(int*)p2;
}
void test01()
{
int arr[] = { 1,4,5,6,6,8,2,4,8,9,0 };
int sz = sizeof(arr) / sizeof(int);
qsort(arr, sz, sizeof(int), comp);
printf("整形数组排序:>\n");
Print(arr, sz);
}
前面四个参数都好说:
第一个是数组名字,也就是其实第一个元素的地址。
第二个是数组元素的个数。
第三个是数组每个元素的大小,单位是字节。
分析
!!这里是核心!!!
关键是第四个,为什么要写一个函数指针??
这里我们就要讨论一下,为什么qsort要求我们写这么一个函数?
我们想,qsort能实现不同类型数据的排序,这个函数的实现者知道要对什么样的数据进行排序吗?
显然是不知道的,那么谁知道?
函数的调用者知道,那么我们就要提供一个这样的函数,能比较我们传入的数据的函数。
排序最核心的操作就是比较,然后调整相对位置。
那么我们是不是就可以认为,只要我们能给出两个数据的大小关系,这个状态给给这个qsort函数,它的
内部就可以对我们传入的数据进行排序了?
没错就是这个样子。
那么我们就要讨论一下,这个我们写的比较函数的返回值了。
元素1大于元素2,返回大于0的值
元素1小于元素2,返回小于0的值
元素1等于元素2,返回0的值
qsort默认的排序是升序,来看一下结果。
![](https://i-blog.csdnimg.cn/blog_migrate/dd38201b44fb853e16e6a439866b5113.png)
来看,我们写的比较函数里面的两个参数也是void *的指针,但是我们是知道我们排序数据的类
型,这里是int *,那么就把他强制转化为int *,那么这里的p1 p2,就是整型指针,一次能访问4个
字节,解引用后,能实现两个相邻数据的大小比较。
结构体数据的排序
结构体类型是类似的,来看代码
typedef struct student
{
int age;
char name[20];
} student;
void print_s( student* p1, int sz)
{
for (int i = 0; i < sz; i++)
{
printf("姓名:>%5s\t年龄:>%d", p1[i].name, p1[i].age);
printf("\n");
}
printf("\n");
}
int comp_stu_age(const void* p1, const void* p2)//按年龄比较
{
return (( student*)p1)->age- (( student*)p2)->age;
}
int comp_stu_name(const void* p1, const void* p2)//按姓名比较
{
return strcmp((( student*)p1)->name, (( student*)p2)->name);
/*strcmp()字符串比较函数的返回值,刚好满足我们刚刚说的比较函数的返回值,所以这里直接
返回它的值就行了。*/
}
void test02()
{
student arr2[] = { 20,"zhangsan" ,25,"lisi",39,"wangwu" };
int sz = sizeof(arr2) / sizeof(student);
qsort(arr2, sz, sizeof(student), comp_stu_age);
printf("结构体数组按年龄排序:>\n");
print_s(arr2, sz);
qsort(arr2, sz, sizeof(student), comp_stu_name);
printf("结构体数组按姓名排序排序:>\n");
print_s(arr2, sz);
}
效果
![](https://i-blog.csdnimg.cn/blog_migrate/7b9e083d23597c07b5488d32655bf03b.png)
以上是我们的铺垫,能耐心看到这里,你真棒!!
接下来就是重点,有了前面的铺垫,我们就能更好的理解这个函数。
qsort模拟实现
既然是模拟实现,那么我们就从参数开始,很简单,跟qsort的参数是一样的。
void Bubble_Qsort(void* base, size_t num, size_t width,
int(*compare)(const void *p1,const void *p2))
参数这里,我们就不多说了,前面有介绍,这个和qsort的是一样的。
来看框架:
void Bubble_Qsort(void* base, size_t num, size_t width,
int(*compare)(const void *p1,const void *p2))
{
unsigned i = 0;
unsigned j = 0;
for (i = 0; i < num-1; i++)
{
for (j = 0; j < num - 1 - i; j++)
{
if()
}
}
}
这就是冒泡排序的框架,我们今天的主题就是为了实现不同类型数据的排序,但仅仅是这个框架是无法实现的。
那我们思考,如何确地出来使用者传进来的数据类型呢?
我们有一个关键的参数,就是每个数据占据的字节数,它就是每个数据的宽度。
那么现在,我们抛开数据类型,来想一想:
数据在数据存储是以字节为单位的,而且数组中的数据是在内存中连续存放,那么这样我们是不是可
以把数据通过指针按字节进行交换,只要确地好指针的位置,依次按字节把数据进行交换,最终实现的效
果不也是排序了吗?
好,重新回到我们的排序代码中,按照冒泡排序的想法,首先要确定两个数据的大小,根据已有参数
是可以确定两个相邻数据的。
if(compare((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
{
//交换两个数据
}
compare((char*)base + j * width, (char*)base + (j + 1) * width
我们看if中的这个代码,它是一个函数调用,我们关键看参数,先把 base指针强转为char *类型的指
针,再让它移动宽度乘以 j 个字节,这是为了让它能遍历整组数据。
为什么要转化为char *呢?
因为指针的类型决定了指针能移动的步长,而char类型一次是移动一个字节的,这个特点,能让我们把
它和传进来的宽度联系起来,也能实现在相邻两个数据之间的移动,我们关键是要找到起始地址,传给比较函数,根据它返回值来确定是否要交换。
看图:
![](https://i-blog.csdnimg.cn/blog_migrate/a9be6b535eecd93da3408dfeb725c77e.png)
接下来就是交换相邻两个元素了,前面提到,我是按字节依次交换数据,那么代码应该这样写:
void Swap_qsort(char* p1, char* p2, int width)
{
int i = 0;
for (i = 0; i < width; i++)
{
char tmp = *p1;
*p1 = *p2;
*p2 = tmp;
p1++;
p2++;
}
}
相邻两个元素都是占宽度个字节,那么依次按字节交换,那就要交换宽度次,用char *指针来解引用,因为它一次能移动一个字节。
看图:
![](https://i-blog.csdnimg.cn/blog_migrate/9dbb1ca36973d7d09ec9d1f4b926b7fb.png)
按照这样的规则,我们把数据按照这样的方式进行交换,最终是能够完成数据的排序的。
来看一下整体代码:
void Swap_qsort(char* p1, char* p2, int width)
{
int i = 0;
for (i = 0; i < width; i++)
{
char tmp = *p1;
*p1 = *p2;
*p2 = tmp;
p1++;
p2++;
}
}
void Bubble_Qsort(void* base, size_t num, size_t width,
int(*compare)(const void *p1,const void *p2))
{
unsigned i = 0;
unsigned j = 0;
for (i = 0; i < num-1; i++)
{
for (j = 0; j < num - 1 - i; j++)
{
if(compare((char*)base + j * width,
(char*)base + (j + 1) * width) > 0)
{
//交换 两个数据,
Swap_qsort((char*)base + j * width,
(char*)base + (j + 1) * width, width);
}
}
}
}
测试
#include<stdio.h>
#include<string.h>
void Print(int* arr, int sz)//打印整型数组
{
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
typedef struct Student
{
int age;
char name[20];
} Student;
int comp_stu_age(const void* p1, const void* p2)//结构体按年龄排序
{
return (( Student*)p1)->age- (( Student*)p2)->age;
}
int comp_stu_name(const void* p1, const void* p2)//结构体按姓名排序
{
return strcmp(((Student*)p1)->name, (( Student*)p2)->name);
}
int comp(const void* p1, const void* p2)//整型数据排序
{
return *(int*)p1 - *(int*)p2;
}
void Print_s( Student* p1, int sz)
{
for (int i = 0; i < sz; i++)
{
printf("姓名:>%s\t年龄:>%d", p1[i].name, p1[i].age);
printf("\n");
}
printf("\n");
}
void Swap_qsort(char* p1, char* p2, int width)
{
int i = 0;
for (i = 0; i < width; i++)
{
char tmp = *p1;
*p1 = *p2;
*p2 = tmp;
p1++;
p2++;
}
}
void Bubble_Qsort(void* base, size_t num, size_t width,
int(*compare)(const void *p1,const void *p2))
{
unsigned i = 0;
unsigned j = 0;
for (i = 0; i < num-1; i++)
{
for (j = 0; j < num - 1 - i; j++)
{
if(compare((char*)base + j * width,
(char*)base + (j + 1) * width) > 0)
{
//交换 两个数据,
Swap_qsort((char*)base + j * width,
(char*)base + (j + 1) * width, width);
}
}
}
}
int main()
{
int arr[] = { 1,4,5,6,7,8,3,3,5,7,8 };
int sz = sizeof(arr) / sizeof(int);
Bubble_Qsort(arr, sz, sizeof(int), comp);
printf("整形数组排序:>\n");
Print(arr, sz);
printf("\n===============================\n");
Student arr2[] = { 10,"lisi" ,45,"zhangsan",29,"wangwu" };
sz = sizeof(arr2) / sizeof(Student);
Bubble_Qsort(arr2, sz, sizeof(Student), comp_stu_age);
printf("结构体数组按年龄排序:>\n");
Print_s(arr2, sz);
printf("\n===============================\n");
Bubble_Qsort(arr2, sz, sizeof(Student), comp_stu_name);
printf("结构体数组按姓名排序排序:>\n");
Print_s(arr2, sz);
return 0;
}
运行结果
![](https://i-blog.csdnimg.cn/blog_migrate/c567b9555059580cb0d7a788f04abda2.png)
总结
以上就是本次分享的全部内容,总结一下:
关于这个模拟实现,我们要清楚以下几点:
函数指针的理解
void *指针的理解
如何按字节对数据进行操作
~~如果这片文章对你有帮助,希望各位大佬,一键三连走一波~~~
~~我们下一篇再见~~
需要源代码和演草的话,来博主的仓库哦~