🏖️作者:@malloc不出对象
⛺专栏:《初识C语言》
👦个人简介:一名双非本科院校大二在读的科班编程菜鸟,努力编程只为赶上各位大佬的步伐🙈🙈
目录
- 前言
- 一、什么是回调函数
- 二、为什么要使用回调函数
- 三、回调函数的意义
- 四、详解qsort函数
- 4.1 qsort函数的功能
- 4.2 qsort函数的参数
- 4.3 qsort函数的基本使用
- 4.4 compar函数的作用机理
- 4.5 qsort函数总结
- 五、void*的用法及注意事项
- 六、利用冒泡排序实现对各种类型的排序
前言
本篇文章主要给大家介绍的是回调函数,兴许有很多读者没有听到过这个名词,它的用途还是比较多的,最重要的是有很好的灵活性以及封装性等性质,关于回调函数最经典涉及到C语言中的一个qsort排序库函数,本文也将着重分析一下qsort函数的实现细节。
一、什么是回调函数
回调函数就是通过一个函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其指向的函数时,我们就说这是回调函数。回调函数不是该函数的实现方直接调用,而是在特定的事件或条件发生时由另外一方进行调用的,用于对该事件的响应。
下面我们就来看看回调函数的基本用法:
void Print(char *str)
{
printf("hello %s!\n",str);
}
void test(void (*p)(char*))
{
(*p)("hncu");
}
int main()
{
test(Print);//这里将Print函数名作为实参传递,所以test函数形参要用一个函数指针来接收Print函数名
//在调用test函数进入到其代码块时,通过函数指针p调用指向其所指向的函数Print,此时Print函数就是一个回调函数,它不是被直接调用的而是在test函数中被间接调用的
return 0;
}
通过这个例子,其实我们可以更通俗的来理解一下回调函数:被回调的函数、 回头执行调用动作的函数;先被当做函数指针传入、后面又被回调的函数就是回调函数。
二、为什么要使用回调函数
很多读者可能会想,为什么不像普通函数调用那样,在回调的地方直接写函数的名字呢?这样不也可以吗?为什么非得用回调函数呢?
要回答这个问题,我们先来了解一下回调函数的好处和作用,那就是解耦,对,就是这么简单的答案,就是因为这个特点,普通函数代替不了回调函数。
何为解耦?
耦合偏向于两者或多者的彼此影响,解耦就是要解除这种影响,增强各自的独立存在能力,这样就更巨灵活性了,可以无限降低存在的耦合度,但不能根除,否则就失去了彼此的关联,失去了存在意义。
下面这幅图很好的描述了回调函数之间的关系:
在回调中,主程序把回调函数像参数一样传入库函数。这样一来,只要我们改变传进库函数的参数,就可以实现不同的功能,这样有没有觉得很灵活?并且丝毫不需要修改库函数的实现,这就是解耦。再仔细看看,主函数和回调函数是在同一层的,而库函数在另外一层,想一想,如果库函数对我们不可见,我们修改不了库函数的实现,也就是说不能通过修改库函数让库函数调用普通函数那样实现,那我们就只能通过传入不同的回调函数了,这也很好的体现了封装性,将一些技术细节隐藏起来,只暴露出一部分内容。
三、回调函数的意义
回调函数可以把调用者与被调用者分开,所以调用者不关心谁是被调用者。它只需知道存在一个具有特定原型和限制条件的被调用函数。简而言之,回调函数就是允许用户把需要调用的函数的指针作为参数传递给一个函数,以便该函数在处理相似事件的时候可以灵活的使用不同的方法。
回调函数在实际中有什么作用?先假设有这样一种情况:我们要编写一个库,它提供了某些排序算法的实现(如冒泡排序、快速排序、shell排序、shake排序等等),为了能让库更加通用,不想在函数中嵌入排序逻辑,而让使用者来实现相应的逻辑;或者,能让库可用于多种数据类型(int、float、string),此时,该怎么办呢?可以使用函数指针,并进行回调,qsort函数就其中最经典的例子,下面我将着重介绍qsort函数。
四、详解qsort函数
为什么我们要提到qsort函数呢?因为它能实现对各种数据类型的排序就是因为使用了回调函数compar,下面我们就来详细解析一下qsort函数。
4.1 qsort函数的功能
我们可以通过下面一张图简单的先了解一下qsort函数的功能:
它的功能是对数组的元素进行排序对数组中指向的元素进行排序,每个元素的长度为
字节
,使用函数确定顺序。此函数使用的排序算法通过调用指定的函数来比较元素对,并将指向这些元素的指针作为参数。该函数不返回任何值,而是通过对其元素进行重新排序来修改所指向的数组的内容。
4.2 qsort函数的参数
还是先给大家放一张标准的qsort函数参数图:
下面是我对qsort函数参数的分析:
4.3 qsort函数的基本使用
下面的代码实现的是对整型数组的升序:
#include<stdio.h>
#include<stdlib.h>
int compare(const void* e1, const void* e2)
{
//(int*)先将元素类型强制类型转换为整型的地址,再进行解引用的操作就得到了元素的值
//通过相减我们得到了返回值,compar函数就是通过返回值来调整元素的顺序的。
return *(int*)e1 - *(int*)e2;
}
int main()
{
int arr[10] = { 10,9,8,7,6,5,4,3,2,1 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), compare);
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
既然compar是回调函数,那么我们之前不是说过它的优点是具有很强的独立性吗?要想实现不同的功能我们只需要改变回调函数的实现就能实现库函数不同的功能了。如果我们想实现对浮点型数组、结构体数组排序,改变compar函数真的能实现功能吗?
下面就以对浮点型排序为例,其实我们只需要稍微改变一下compar函数的实现就能实现排序了。
下面代码是对浮点型数组进行降序:
#include<stdio.h>
#include<stdlib.h>
int compare(const void* e1, const void* e2)
{
return (int)(*(double*)e2 - *(double*)e1);
}
int main()
{
double arr[] = { 5.0, 34.0, 1.0, 3.0, 3.0, 12.0, 38.0 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), compare);
for (int i = 0; i < sz; i++)
{
printf("%.1lf ", arr[i]);
}
return 0;
}
从图片中我们可以看到只需要稍微改变一下compar函数的实现就能对浮点型数组进行降序操作。
那么关于上述代码有几个需要注意的地方:
你有没有发现我在实现浮点型排序的时候在前面将两个浮点型相减的结果强转成了int型?(int)(*(double*)e2 - *(double*)e1);
为何要这么做呢?
这是因为compar函数的返回值就是int类型,它的返回值是不会变的,这就是规定。
实现降序我们只需要交换一下e1与e2的位置就可以了,读者可以记住这样一句话e1写在前面减e2实现的就是升序,e2写在前面减e1实现的就是降序。
4.4 compar函数的作用机理
如下是compar函数的作用机理:
我们来详细分析一下,假设以上述代码对整型的升序为例,compar函数的返回值为:*(int*)e1 - *(int*)e2。如果e1大于e2返回值就为1,这时候e1在e2的前面又比e2大,要得到升序,所以将两者进行交换,这样大的值就往后面走了;如果e1小于e2返回的是-1,不进行交换,位置不变。
同样的,如果我们对数组进行降序的话只需要在返回值的地方把e1和e2交换一下位置即可:*(int*)e2 - *(int*)e1。如果e2大于e1返回值为-1,这时候e2在e1的后面又比e1大,要得到降序,所以将两者进行交换,这样的小的值就往后面走了;如果e2小于e1返回的是1,不进行交换。
升序和降序其实是相反的,我们只要了解其中的交换规则就好了。
4.5 qsort函数总结
qsort可以用于任意类型的排序,它的注意事项有以下几点:
1.compare函数的返回类型一定为int型,那么在对浮点型数组进行排序时我们是要将它们相减的结果强制类型转化为int型的,在自定义的结构体类型中我们也可能定义了浮点型的数据或者数组,所以我们也要将它们相减的结果强制类型转化为int型
2.在进行字符串的比较时我们不是采用的关系运算符来比较,而是用strcmp库函数来专门比较,它的工作原理跟compar函数有点相似:
若字符串1大于字符串2,函数返回值为1;
若字符串1小于字符串2,函数返回值为-1;
若字符串1等于字符串2,函数的返回值为0.
3.最后compar函数的返回形式也是可以写成另外一种形式的,这里以整型数组为例:*(int*)e1 > *(int*)e2 ? 1 : -1;当然的话我更推荐标准写法它更简洁也更方便,写成这种形式的话增大了代码量而且还会影响机器的运行速度。
五、void*的用法及注意事项
前面我们讲到qsort函数为什么能实现对各种数据类型排序?
其根本原因是compar函数的形参类型是void*型,它能接收各种指针类型,由此我们就可以实现对各种数据类型的排序操作。
void*的用途:void*可以被任何类型的指针接收,也可以接收任意类型的指针(常用)。
我们的库、系统接口的设计上尽量设计成通用接口,例如:一个函数被不同的类型来使用…
下面给大家讲一下void*的用法和注意事项:
void*类型的指针可以接收任意类型的地址,也能被任意类型接收(最常见的NULL其实就是void*类型,NULL == (void*)0)
void*类型的指针不能进行解引用的操作,因为指针类型决定了你可以访问几个字节,这里是void*型所以不确定它到底是什么类型。
void*类型的指针不能进行+/-操作,指针类型决定了+/-一个单位走多少个字节,这里同样的指针类型不确定。**
下面来看相应的一组例子:
六、利用冒泡排序实现对各种类型的排序
我们知道冒泡排序有很多缺陷,先不说时间复杂度,它只适用于整型数组的排序,所以它其实也是非常鸡肋的一种排序,那么在学完qsort快速排序之后,我们能不能进行仿写一下改造一下使其能对所有数据类型进行排序呢?
下面我们就用冒泡排序来仿写一下,其实大体上是差不多的,只不过qsort底层是用快速排序的思想实现的,而且我们可以直接进行调用它,冒泡排序就要我们自己去实现了,当然了冒泡排序作为最经常使用的排序之一相信对大家也是没什么难度。
我们先来梳理一下思路,要实现对任意类型数据排序,我们仿照qsort函数的参数形式:void qsort (void* base, size_t num, size_t size,int (*compar)(const void*,const void*));我们发现compar函数其实是可以不用变的,它是用来对任意类型元素比较的,是对数组中指向的元素进行排序,每个元素的长度为字节,*base其实就是数组名,num就数组的大小,size是数组元素的大小,所以我们要做的就是封装一个Bubble_sort间接调用compar对元素进行排序。
下面我们来看看具体的代码实现,这里以升序为例:
int cmp_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
void swap(char* buff1, char* buff2, int width)
{
//既然我们把base强转为char*型,那么如果我们要进行交换元素,是不是这宽度个字节全部都要进行交换才能达到交换俩个元素的目的呢
for (int i = 0; i < width; i++)
{
char temp = *buff1;
*buff1 = *buff2;
*buff2 = temp;
buff1++;
buff2++;
}
}
void Bubble_sort(void* base,int sz,int width,int (*compar)(void* e1, void* e2))
{
//我们不确定元素是什么类型,那我们就先默认把它看成是char*型,由width我们知道了每个元素的宽度,也就是占多少个字节。
//我们每次跳过宽度个字节,是不是每次就是跳过一个元素呢,由此我们就可以每次对比两个元素的大小了
for(int i = 0; i < sz - 1; i++)
{
for(int j = 0; j < sz - 1 - i; j++)
{
if(compar((char*)base + j * width, (char*)base + (j+1) * width) > 0)
{
//每俩个相邻元素进行比较,根据compar函数的返回值来控制升序降序,要不要交换元素
swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
}
}
}
}
void test1()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
Bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
for (int i = 0; i < sz; i++)
printf("%d ", arr[i]);
}
int main()
{
test1();
return 0;
}
我们来看看结果:
接着我们就来看看对结构体数组进行排序的例子吧,刚好在qsort部分我也没有进行举例说明:
struct Stu
{
char name[10];
int age;
};
int cmp_stu_by_age(const void* e1, const void* e2)
{
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
void swap(char* buff1, char* buff2, int width)
{
for (int i = 0; i < width; i++)
{
char temp = *buff1;
*buff1 = *buff2;
*buff2 = temp;
buff1++;
buff2++;
}
}
void Bubble_sort(void* base, int sz, int width, int (*cmp_stu_by_age)(const void* e1, const void* e2))
{
for (int i = 0; i < sz - 1; i++)
{
for (int j = 0; j < sz - 1 - i; j++)
{
if (cmp_int((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
{
swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
}
}
}
}
void test2()
{
struct Stu s[3] = { {"zhangsan",20},{"lisi",30},{"wangwu",10} };
int sz = sizeof(s) / sizeof(s[0]);
Bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_age);
for (int i = 0; i < 3; i++)
{
printf("%s %d\n", s[i].name, s[i].age);
}
}
int main()
{
test2();
return 0;
}
实现了对结构体年龄的升序排序:
如果我们想对名字进行升序操作,只需要改变一下compar部分就可以了,下面我们来看看结果:
int cmp_stu_by_name(const void* e1, const void* e2)
{
//注意这里我们比较字符串一定要使用strcmp函数,将字符一个个进行一次比较
return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
好了本篇文章关于回调函数就讲到这里了,如果有任何疑问或者错处欢迎大家在评论中互相交流🙈🙈