何思何虑,居心当如止水;勿取勿忘,为学当如流水。— 出自《格言联璧·学问类》
解释:无思无虑,心境应当平静如水;不求冒进也不忘记,学业当如流水一般永无止境。
这篇博客我们将会理解回调函数这个概念,以及借用qsort帮助理解,并且最终用qsort的思路来实现冒泡排序。
回调函数🍀
概念
回调函数就是一个通过函数指针调用的函数。
如果你把函数的指针(地址)作为参数传递给另一个函数
,当这个指针被用来调用其所指向的函数时
,我们就说这是回调函数。
回调函数不是由该函数的实现方直接调用
,而是在特定的事件或条件发生时由另外的一方调用的
,用于对该事件或条件进行响应。
如果这样看你还是不太理解,我们可以举出一个简单的例子。
void test2() { printf("haha\n"); } void test1(void (*p) ()) { p(); } int main() { test1(test2); return 0; }
这这段代码中:
test1的实参是test2,而test1的形参也对应函数指针类型,
{ p(); }
而p()就是借助p的存储的test2的地址调用test2,就相当于test2()
。
归纳一下:
在这个过程中谁是回调函数?
答:test2,因为test2并不是直接调用
,而是通过将test2的地址传递给test1
的实参,然后通过形参的地址调用。
现在我们已经理解了什么是回调函数。
现在我们可以用一个简单的计算器来运用一下:
int Add(int x, int y) { return x + y; } int Sub(int x, int y) { return x - y; } int Mul(int x, int y) { return x * y; } int Div(int x, int y) { return x / y; } void menu() { printf("**************************\n"); printf("**** 1.add 2.sub ****\n"); printf("**** 3.mul 4.div ****\n"); printf("**** 0.exit ****\n"); printf("**************************\n"); } void cal(int (*p) (int, int)) { int x = 0; int y = 0; scanf("%d %d", &x, &y); printf("%d\n", p(x, y)); } int main() { int input = 0; do { menu(); printf("请输入"); scanf("%d", &input); switch (input) { case 1: cal(Add); break; case 2: cal(Sub); break; case 3: cal(Mul); break; case 4: cal(Div); break; case 0: printf("退出计算机\n"); break; default: printf("输入错误请重新输入\n"); } } while (input); }
这就是用回调函数的一个运用方法,虽然没有转移表那么简洁,但是也是一种方法。
qsort函数🤢
我们先来看看这个函数的介绍。
参数和返回值:
这个函数的参数应该是算比较多的了。下面是参数的具体解释:
这个函数的功能:
qsort函数是C语言标准库中的一个排序函数,用于对数组进行快速排序(其实就是根据快排来实现的)。它的基本功能是将给定的
数组
(当然也包括结构体数组)按照指定的比较函数进行排序。
头文件:
那么是根据什么来判断要排序呢?
实际上跟strcmp有点相似
,比较两个元素的大小,根据回调函数
(compar就是一个函数名,当qsort的参数里包含另一个函数的名字,就与我们说的回调函数相符)的返回值来判断是否需要排序。
我们现在就来用用看:
int cmp(const void* e1, const void* e2) { //if (*(int*)e1 - *(int*)e2 > 0) // return 1; //else if (*(int*)e1 - *(int*)e2 == 0) // return 0; //else // return -1; return (*(int*)e1 - *(int*)e2); } int main() { int arr[7] = { 8, 7, 4, 3, 6, 1, 2 }; int sz = sizeof(arr) / sizeof(arr[0]); qsort(arr, sz, sizeof(arr[0]), cmp); for (int i = 0; i < 7; i++) { printf("%d ",arr[i]); } return 0; }
我们简单的排序一个整型数组,
qsort这个函数能排序任何类型的数组的原因就是在于,
cmp这个回调函数是由我们自己来实现的。
介绍中也强调了这一点。那为什么是void * 类型的参数呢?
因为void *类型能接收其他类型,所以才使得我们能排序所有类型的数组,
但是cmp函数的实现也需要根据不同的类型进行不同的改动。
接下来我们看看结构体数组元素的排序。
typedef struct student { char name[20]; int age; }student; void cmp(const void* e1, const void* e2) { if ((*(student*)e1).age - (*(student*)e2).age > 0) { return 1; } else if ((*(student*)e1).age - (*(student*)e2).age == 0) { return 0; } else return -1; //return ((*(student*)e1).age - (*(student*)e2).age);简写 } //void cmp(const void* e1, const void* e2) //{ // if (strcmp((*(student*)e1).name , (*(student*)e2).name)> 0 ) // { // return 1; // } // else if (strcmp((*(student*)e1).name, (*(student*)e2).name) == 0) // { // return 0; // } // else // return -1; //} int main() { student s1[3] = { {"zhangxiaofan",20} ,{"shihao",18} ,{"fangyuan",25} }; qsort(s1, sizeof(s1) / sizeof(s1[0]), sizeof(s1[0]), cmp); for (int i = 0; i < 3; i++) { printf(" %s is %d\n",s1[i].name, s1[i].age); } return 0; }
分析(排序age)
- 在排序结构体数组成员时,首先必须要有结构体数组,然后我们再进行初始化。
- 当我们要排序的是
int age;
时,我们所创建的回调函数就与整型数组的排序有区别。 - 区别首先在于
强制类型转换
,整型数组的元素类型是整型所以可以直接(int *)
,但是结构体数组的每个元素是结构体(所以用到student *
),结构体的成员(int age)才是我们要排序的目标。 - 在整型数组时我们直接讲
e1,e2
进行强制类型转换解引用然后相减,是因为e1,e2就直接表示的是整型元素
,而结构体数组并不能和整型数组一样,因为结构体数组强制类型转换再解引用得到的是这个结构体,
结构体怎么能和结构体相减呢?所以我们需要用到.
去找到int age
-
分析(排序名字)
排序名字与排序年龄其实差不了太多,最重要的就是需要用到函数strcmp来比较字符的ASCII码,strcmp((*(student*)e1).name , (*(student*)e2).name)
这样来判断就行了。
我们已经学习完qsort函数了,我们知道qsort函数其实是用快速排序实现的,虽然我们没有学过快速排序,但是我们知道冒泡排序。下面我们就来自己用冒泡排序来实现一下吧。
用冒泡实现qsort💥
首先先简单的回顾一下冒泡排序。
void bubble_sort(int* arr, int sz) { int i = 0; int j = 0; for (i = 0; i < sz - 1; i++) { for (j = 0; j < sz - 1 - i; j++) { int tmp = 0; if (arr[j] > arr[j + 1]) { tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp; } } } } int main() { int arr[] = { 10, 9, 8, 7, 6 ,5, 4, 3, 2, 1 }; int sz = sizeof(arr) / sizeof(arr[0]); bubble_sort(arr, sz); for (int i = 0; i < sz; i++) { printf("%d ", arr[i]); } return 0; }
然后我们思考一下到底该如何能够排序任意的类型的数组呢?我们必须有个思路。
- 就像qsort函数的创作者一样,他并不知道我们要排序什么样类型的数组,但是呢?他通过qsort函数的参数能够解决这个问题。
- 也就是说我们
给出数组的地址,元素多少,一个元素的大小,还有回调函数
就能够实现排序的功能,那我们也需要想办法通过元素的多少和大小来实现我们自己的能排序各种类型数组的冒泡排序。当然我们可以自己试试看。
下面是参考
int cmp(const void* e1, const void* e2) { return *(int*)e1 - *(int*)e2; } void swap(char* e1,char * e2, int width) { char tmp = 0; while (width--) { tmp = *e1; *e1 = *e2; *e2 = tmp; e1++; e2++; } } void my_bubble_sort(void *base, int size, int width, int (*p)(const void*, const void*)) { int i = 0; int j = 0; for (i = 0; i < size - 1; i++) { for (j = 0; j < size - 1 - i; j++) { char tmp = 0; if (cmp((char *) base+j *width,(char *)base+(j+1)*width)> 0) { swap((char*)base + j * width, (char*)base + (j + 1) * width, width); } } } } int main() { int arr[] = { 10, 9, 8, 7, 6 ,5, 4, 3, 2, 1 }; int sz = sizeof(arr) / sizeof(arr[0]); my_bubble_sort(arr, sz,sizeof(arr[0]),cmp); for (int i = 0; i < sz; i++) { printf("%d ", arr[i]); } return 0; }
如果想要排序逆序的数组,把 if 中的> 0 改成 < 0 就行了
分析
其实在自己实现的过程中最重要的就是my_bubble_sort
这个函数,
1. 首先我们要把参数写正确,
void my_bubble_sort(void *base, int size, int width, int (*p)(const void*, const void*))
由于我们并不知道排序的类型所以我们将该数组的首地址放在void *,最后也要把函数指针写正确。
- 我们在冒泡中需要修改的就是if ()中的表达式,我们并不能直接判断大小,而是用cmp函数来判断。
(char *) base+j *width,(char *)base+(j+1)*width
我们先将base强转为char *类型,为什么呢?为什么不是int *,short *呢?
我们知道内存中最小的单位是自己,而char *指针解引用只能访问一个字节,我并不需要知道具体是什么类型,我只需要知道数组一个元素的大小就行,通过(char *) base+j *width
实际上找到了数组的一个元素width就是来表达大小的。 - swap函数
swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
我们需要传的参数其实就三个,元素1的地址,和元素2的地址,最后是元素的大小,为什么要这样做?
void swap(char* e1,char * e2, int width) { char tmp = 0; while (width--) { tmp = *e1; *e1 = *e2; *e2 = tmp; e1++; e2++; } }`
可以看到我们用char *来接收这两个地址,目的就是让字节让字节和字节交换,比如说一个结构体数组的一个元素的大小是20字节,那么我用字节为单位与另一个元素交换20次不就行了吗?这里的20不就相当于width吗?
总结😈
我们在这篇博客中从回调函数的概念到学会运用,再列举出qsort函数来帮助大家理解,并且也学会了这个强大的排序函数,最后我们还用冒泡排序来模拟出了一个可以排序任意类型数组的函数,可谓是收获颇多啊。
完。