目录
一、前言
我们初识C语言时,会做过让一个整型数组按照从小到大来排序的问题,我们使用的是冒泡排序法,但是如果我们想要比较其他类型怎么办呢,显然我们当时的代码只适用于简单的整形排序,对于结构体等没办法排序,本篇将引入一个库函数来实现我们希望的顺序。
二、简单冒泡排序法
#include <stdio.h> int main() { int arr[10] = { 9,8,7,6,5,4,3,2,1,0 }; int i = 0; int sz = sizeof(arr); for (i = 0; i < sz-1; i++) { int j = 0; for (j = 0; j < sz-1-i; j++) { int tep = 0; if (arr[j] > arr[j + 1]) { tep = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tep; } } } return 0; }
这是我们最初学习的冒泡排序法,我们如果把其修改一下,能不能比较其它类型的数据呢,结果是肯定的。
三、qsort函数的使用
1、qsort函数的介绍
图片1为整体描述,下面的图片2、3、4、5、6为图片1的解释。
首先我们可以看出qsort函数不需要返回类型,有四个参数,分别为空指针、无符号型、无符号型、函数指针。图二说明该函数无返回类型。图三说明我们base为要排序中的起始地址,是一个空指针类型,所以我们传首元素地址。图四说明num为元素个数,是一个无符号整型,所以我们传一个无符号整型。图五说明width为每个元素的宽度,单位为字节,是一个无符号整型,所以我们传一个无符号整型。图六说明这个函数指针指向一个含有两个空指针作为参数并且返回类型为int的(比较)函数。这个是qsort的最后一个参数,作用是让我们提供比较大小的规则,所以我们传的时候需要传一个函数地址让库函数中qsort的函数指针来接收。补充:qsort对应的头文件为<stdlib.h>。(比较)函数中的两个参数如果第一个大返回一个大于0的数,如果第二个大返回一个小于0的数,如果相等则返回0。
看完这些,相信你对qsort函数有了初步了解。
2、qsort函数的运用
在这里我们举出两个例子来直观看出qsort的应用方法,分别为整型数组排序和结构体排序。
2.1、qsort函数排序整型数组
#include<stdio.h> #include <stdlib.h> int cmp_int(const void* e1, const void* e2) { return *(int*)e1 - *(int*)e2; } int main() { int arr[10] = { 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); int i = 0; for(i = 0;i<sz;i++) printf("%d ", arr[i]); }
这样就完成了排序。
疑问:
1.qsort第四个参数(函数地址)为什么不需要传参?
首先:我们要知道,我们第四个参数传的是一个函数地址(函数名加不加取地址符号都可以表示函数的地址),而不是一个函数,所以就没有传参这个说法。其次:等到库函数qsort用函数指针接收后会进行传参等工作(这是程序内部进行的,我们不需要管)。
2.为什么函数定义里面的参数是空指针类型,而不是特定类型?
因为qsort函数并不知道你要传的是什么类型,也就是它使用这个函数时不知道传什么类型的实参,所以统一先传一个空类型地址然后函数用空类型指针接收。
3.空指针是什么意思,怎么用?
空指针就像一个垃圾桶,我们可以对其赋上任意类型的指针,这很方便,但是它不能直接使用,就像我们赋的时候一样,用的时候它并不知道你是什么类型,它相当于只储存了一个字节的地址,没有明确的步长(指针+1跳过的字节)和访问权限大小(解引用指针访问的字节个数)。所以当我们用空指针时,需要进行强制类型转换,转换成我们需要的类型。
4.为什么com_int函数在用的时候要把参数强转为int?
因为当我们使用qsort函数时,我们肯定知道要进行排序的类型,所以我们需要根据自己的需要的类型来写出对应的函数让qsort函数调用。
2.2、qsort函数排序结构体
#include <stdio.h> #include <stdlib.h> #include <string.h> struct stu { char name[10]; int age; }; //排序结构体年龄的回调函数 int cmp_struct_stu_age(const void*e1,const void*e2) { return ((struct stu*)e1)->age - ((struct stu*)e2)->age; } //排序结构体姓名的回调函数 int cmp_struct_stu_name(const void* e1, const void* e2) { return strcmp(((struct stu*)e1)->name, ((struct stu*)e1)->name); } //运用qsort函数排序结构体年龄 void test1() { struct stu s[3] = { {"xiaoming",30},{"lihua",60},{"wangli",40} }; int sz = sizeof(s) / sizeof(s[0]); qsort(s, sz, sizeof(s[0]), cmp_struct_stu_age); } //运用qsort函数排序结构体名字 void test2() { struct stu s[3] = { {"xiaoming",30},{"lihua",60},{"wangli",40} }; int sz = sizeof(s) / sizeof(s[0]); qsort(s, sz, sizeof(s[0]), cmp_struct_stu_name); } int main() { test1(); //test2(); return 0; }
补充:
字符串比较大小使用strcmp函数,对应的头文件为<string.h>,依次从左到右比较字母的ASCII值直到不相等,左边大返回大于0的数,右边大返回小于0的数,直到最后都相等返回0,如果你仔细看了最前面关于qsort的介绍,你会发现他们很相似,qsort中用到的回调函数也是需要返回这样的形式。
四、利用冒泡排序模拟实现qsort函数
你对于qsort内部如何执行操作的是否感兴趣呢?你对于qsort中传的那些参数是否存在疑问,不知道他们有什么作用呢,接着看下面的内容,让我们更进一步理解qsort的工作原理,同时让你的冒泡排序变得具有普遍性。
首先我们先试着用my_qsort函数模拟出qsort函数来对一个整型数组排序。
#include <stdio.h> int cmp_int(const void* e1, const void* e2) { return *(int*)e1 - *(int*)e2; } void swap(char* buf1, char* buf2, int width) { int i = 0; for (i = 0; i < width; i++, buf1++, buf2++) { char tep = 0; tep = *buf1; *buf1 = *buf2; *buf2 = tep; } } void my_qsort(const void* base, int sz, int width, int(*cmp)(const void* e1, const void* e2)) { int i = 0; for (i = 0; i < sz - 1; i++) { int j = 0; for (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 test3() { int arr[10] = { 9,8,7,6,5,4,3,2,1,0 }; int sz = sizeof(arr) / sizeof(arr[0]); my_qsort(arr, sz, sizeof(arr[0]), cmp_int); int i = 0; for (i = 0; i < sz; i++) printf("%d ", arr[i]); } int main() { test3(); return 0; }
以上代码就是我们模拟出的qsort函数,相信看完这个你会对qsort理解更深刻,不过有一点需要注意,我们模拟时使用的是冒泡排序,而qsort其实内部使用的是快排,所以其实稍有差距。但大体是一致的。
补充:
1.在my_qsort函数中,我们在比较函数(cmp)中传入的是内容解释:
☃️(char*)base+jwidth 这个实际上就是原始的base是空指针类型,所以我们无法直接使用,但我们也不确定传来的具体是什么类型,因此我们先将其强制类型转换为char,然后根据指针加几就跳过几个步长个字节来确定每次跳过一个元素。
2.在my_qsort函数中,我们的swap函数也是一个重要部分,它是如何实现的呢?
☃️首先和1刚开始一样,我们传的实参的形式就是为了保证每次可以跳过一个元素,然后当传到swap函数里后,我们不像cmp函数一样有提前准备好的强转类型,因此我们需要用一种特别的方法保证可以互换元素,那么我们会想到元素是怎样储存的呢,是以二进制以字节为单位储存的,那么我们如果需要交换元素,是不是只需要把每个字节都交换一次就好了呢?答案是肯定的,于是我们也将传来的地址强制类型转换为char*,保证每次交换的是一个字节,然后又有一个问题就是我们需要交换几次呢,还记得之前传的那个参数width吗,它是每个元素的宽度,单位是字节,因此我们可以通过一个for循环,修女换width次来使得一个元素所占的字节全部交换。
我们这一部分虽然是用模拟的qsort来排序整型数组,但其实如果这个学会了的话,结构体和前面说的是一模一样的,因为模拟的qsort不用变,只需要改一下cmp函数的类型即可。
完整代码如下:
#include <stdio.h> #include <stdlib.h> #include <string.h> //排序整型数组顺序时的回调函数 int cmp_int(const void* e1, const void* e2) { return *(int*)e1 - *(int*)e2; } //声明结构体类型 struct stu { char name[10]; int age; }; //排序结构体年龄顺序时的回调函数 int cmp_struct_stu_age(const void*e1,const void*e2) { return ((struct stu*)e1)->age - ((struct stu*)e2)->age; } //排序结构体名字顺序时的回调函数 int cmp_struct_stu_name(const void* e1, const void* e2) { return strcmp(((struct stu*)e1)->name, ((struct stu*)e1)->name); } //运用qsort函数排序整型数组 void test1() { int arr[10] = { 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); } //运用qsort函数排序结构体 void test2() { struct stu s[3] = { {"xiaoming",30},{"lihua",60},{"wangli",40} }; int sz = sizeof(s) / sizeof(s[0]); qsort(s, sz, sizeof(s[0]), cmp_struct_stu_name); //qsort(s, sz, sizeof(s[0]), cmp_struct_stu_age); } //模拟qsort中的交换函数 void swap(char* buf1, char* buf2, int width) { int i = 0; for (i = 0; i < width; i++, buf1++, buf2++) { char tep = 0; tep = *buf1; *buf1 = *buf2; *buf2 = tep; } } //模拟qsort void my_qsort(const void* base, int sz, int width, int(*cmp)(const void* e1, const void* e2)) { int i = 0; for (i = 0; i < sz - 1; i++) { int j = 0; for (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); } } } //运用模拟qsort函数(my_qsort)排序整型数组 void test3() { int arr[10] = { 9,8,7,6,5,4,3,2,1,0 }; int sz = sizeof(arr) / sizeof(arr[0]); my_qsort(arr, sz, sizeof(arr[0]), cmp_int); int i = 0; for (i = 0; i < sz; i++) printf("%d ", arr[i]); } //运用模拟qsort函数(my_qsort)排序结构体 void test4() { struct stu s[3] = { {"xiaoming",30},{"lihua",60},{"wangli",40} }; int sz = sizeof(s) / sizeof(s[0]); my_qsort(s, sz, sizeof(s[0]), cmp_struct_stu_name); //qsort(s, sz, sizeof(s[0]), cmp_struct_stu_age); } int main() { //test1(); //用qsort实现整型数组排序 //test2(); //用qsort实现结构体排序 test3(); //用模拟qsort函数实现整型数组排序 //test(4) //用模拟qsort函数实现结构体排序 return 0; }
回调函数:回调函数就是一个通过函数指针调用的函数。如果你把函数指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外一方调用的,用于对该事件或条件进行响应。
通常来讲就是:把这个函数地址作为参数传过去,另一个函数用函数指针接收然后再调用这个函数,那么这个被调用的函数就叫回调函数。(通过函数指针调用的这个函数,叫做回调函数)