在上一篇文章中我们学习了指针与数组之间的关系,学习了指针数组,数组指针变量,函数指针变量和函数指针数组。那么接下来我们要学习的是一个利用函数指针变量去解决和简化各种问题的回调函数。
一、回调函数
回调函数在编程中是一种很常见的概念,它是指将一个函数作为参数传递给另一个函数,并在特定的条件或事件发生时被调用执行的函数。回调函数不是直接由函数的实现方所调用,而是在特定的条件或事件发生时,由接收它的函数间接的去调用它。它的作用就好比前段时间热映的影视剧狂飙中,高启强因为某些原因,自己没办法直接下手时,就告诉老默"我想吃鱼了",而老默作为一个接收信号的人,它接收到了高启强给他的信号,于是能够帮助高启强去间接的完成他要做的事。
光举例子肯定是不行,让我们实际操作写一段比较简单的代码,来试着用一下回调函数吧~
int Add(int x, int y)
{
return x + y;
}
void Calc(int(*num)(int, int))
{
int a = 0;
int b = 0;
printf("请输入两个计算数:>");
scanf("%d %d", &a, &b);
int m = num(a, b);
printf("结果为%d\n", m);
}
int main()
{
Calc(Add);
return 0;
}
在这段代码中,我们创造了一个void类型的函数Calc来接收一个"一个名为num,能够接收两个整形变量的函数"的函数指针作为参数。当Calc调用Add函数时,那么此时Add函数就是一个回调函数。
而此时可能有人就想发出疑问了:通常情况下我们会直接调用Add函数去计算整数之和,并且与这段代码相比,好像直接使用Add函数会更加快捷简便,那为什么还要使用回调函数呢?回调函数的作用是什么呢?在上一篇文章中,我们写过一个实现计算机的代码,最初的版本有非常多的冗余语句。
而回调函数就能很好的帮我们规避掉这些冗余的句子,从而实现简化代码的作用。让我们来实际操作一下回调函数简化计算机吧~
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 Calc(int(*num)(int, int))
{
int a = 0;
int b = 0;
printf("请输入两个计算数:>");
scanf("%d %d", &a, &b);
int m = num(a, b);
printf("结果为%d\n", m);
}
int main()
{
int m;
int n;
int tmp = 1;
do {
printf("***********************\n");
printf("**** 1.Add 2.Sub ****\n");
printf("**** 3.Mul 4.Div ****\n");
printf("******** 0.quit *******\n");
printf("***********************\n");
printf("请选择:>");
scanf("%d", &tmp);
switch (tmp)
{
case 1:Calc(Add);
break;
case 2:Calc(Sub);
break;
case 3:Calc(Mul);
break;
case 4:Calc(Div);
break;
case 0:printf("退出计算机");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (tmp);
return 0;
}
这样就能够很明显的体现出回调函数的作用了,在一定情况下,它能够一定程度上简化我们的代码,并且解决一些解决不了的问题。
二、库函数strlen与strcmp
在讲sqort函数之前,我们需要先了解一下strlen函数和strcmp函数。它们是在一些对字符型数据进行计算时使用的,用处特别多,用法特别广泛的两个函数。
(两种函数的使用都需要加上头文件 <string.h>)
① strlen
strlen的作用是:获取字符串长度,返回C字符串str的长度。
注:c字符串的长度由终止空字符确定,C字符串的长度等于字符串的开头和终止空字符之间的字符数(不包括终止空字符本身)。
比如我们定义了一个char str[100],但我们只为它初始化了11个字符,那么用int num = strlen(str)来获取字符串的长度时,就应该获取带11个字符。
② strcmp
strcmp的作用是:比较两个字符串,将C字符串 str1 与C字符组 str2进行比较。
比较方式为:该函数开始比较每个字符串的第一个字符。如果它们彼此相等,则继续进行以下配对,直到字符不同或达到终止空字符。此函数执行字符的二进制比较。
若str1小于str2,则返回负整数,即小于0的数。
若str1和str2相等,则返回0。
若str1大于str2,则返回正整数,即大于0的数。
我们还是来通过代码来进一步的了解一下它的作用效果:
#include <stdio.h>
#include <string.h>
int main()
{
char str1[] = "abcdef";
char str2[] = "abcdefg";
if (strcmp(str1, str2) > 0)
{
printf("str1比str2长\n");
}
else if (strcmp(str1, str2) < 0)
{
printf("str2比str1长\n");
}
else
{
printf("str1和str2一样长\n");
}
return 0;
}
这样就能够实现,通过strcmp来比较两个字符串的大小了。
三、qsort函数
通过上面我们对回调函数的解释,大家应该对回调函数的应用有了一定的了解。而说到回调函数的应用,这里我们就不得不提到qsort函数的应用了。
①qsort的参数及原理
qsort函数的作用就是对数据进行排序,比如现在有一组整型数组arr,里面有十个大小不同随意顺序的十个整形变量,那我们就能够通过qsort对它进行大小的排序。在学习qsort之前我们学习过冒泡排序,冒泡排序同样也能够对整型数组中的元素进行大小的排序:
这样就能够使数组中的元素大小排列了,但是仔细观察我们并不难发现,其实冒泡排序的缺点非常明显:
① 冒泡排序只适用于整型变量的排序。
② 时间复杂度O(n^2)太高,效率慢。
而这次我们要学习的qsort函数就能够完美的避免这两种问题,不仅能适用多种类型变量的排序,并且效率相较于冒泡排序也要大大提高。
qsort函数的格式:
void qsort (void* base, size_t num, size_t size,int (*compar)(const void*,const void*));
我们来分别了解一下qsort这四个参数:
①void *base:指向需要排序的数组的首元素,该对象转换为void*。
②size_t num:表示数组中元素的个数。
③size_t size:数组中每个元素的大小(以字节为单位)。size_t是无符号整型类型。
④int (*compar)(const void*,const void*):指向比较两个元素的函数。qsort调用这个函数来比较两个元素的大小。函数compar需要接收两个参数(都转换成const void*)并对两个参数进行比较,通过返回值来表示两个元素的大小关系。
Ⅰ— 如果返回值小于 0,表示 a
应该排在 b
的前面。
Ⅱ — 如果返回值等于 0,表示 a
和 b
相等,顺序不变。
Ⅲ — 如果返回值大于 0,表示 a
应该排在 b
的后面。
②qsort对整型数组排列
那么既然已经了解了qsort函数的使用方法,那让我们来利用qsort函数试试实现冒泡排序吧~
int compar(const void* a, const void* b)
{
return (*(int*)a - *(int*)b);
}
int main()
{
int arr[] = { 7,5,3,9,8,4,6,2,1,0, };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), compar);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
因为数组名代表的就是数组首元素的地址,所以第一个参数直接传arr就行,并且在定义比较函数compar时,返回值的计算需要我们将传入的const void*强制类型转换成int*并解引用,从而就能够计算出数组中两个元素的大小关系了。
按照这种写法输出的冒泡排序是升序,那么我们能不能把它变成降序呢?其实很简单,像冒泡排序一样修改大于和小于号一样,我们只需要将return (*(int*)a - *(int*)b)修改成return (*(int*)b - *(int*)a)就好了。
那我们能不能再尝试一下用qsort来为字符型数组排序呢?让我们尝试一下吧~!!
③qsort对字符串排列
在使用qsort对字符型数组排序前,我们需要思考一个问题:整型变量能通过大于号,小于号来进行大小的表示,可是字符型不可以呀,那应该怎么表示两个元素的大小关系呢?嘿嘿~这就体现了我上面讲到的strcmp函数的作用了,strcmp可以用于比较字符之间的大小,并且大于返回>0的数,小于返回<0的数,等于返回0,和qsort中比较函数的返回值也是一样的形式,而获取字符串长度可以使用上面讲到的strlen函数,因为它不会把\0也算进长度。那思路有了,让我们尝试一番吧
int compar(const void* a, const void* b)
{
return strcmp((char*)a, (char*)b);
}
int main()
{
char arr[] = "ihfknac";
int sz = strlen(arr);
qsort(arr, sz, sizeof(arr[0]), compar);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%c ", arr[i]);
}
return 0;
}
这样就成功的把字符串也按顺序排列好啦~
④qsort对结构体排列
那么我们再来尝试一次,用字符串按顺序来对结构体进行排列~对结构体排列需要明确我们要排列的元素类型,比如我们现在创造一个结构体:
struct stu
{
char name[20];
int age;
};
如果模棱两可的,不知道想要比较哪一个数据,那当我们创造比较函数的时候就会陷入返回值的强制类型转换不知道该怎么写。所以我们要明确自己想要排列的数据才行。比如我们想排列结构体stu中的name,那我们就需要先定义一个结构体数组,然后在比较函数中使用strcmp比较大小(名字也是字符型),让我们动手试试吧:
struct stu
{
char name[20];
int age;
};
int compar(const void* a, const void* b)
{
return strcmp(((struct stu*)a)->name, ((struct stu*)b)->name);
//也可以写作return strcmp((*((struct stu*)a)).name, (*((struct stu*)b)).name);
}
int main()
{
struct stu s[3] = { {"xiaowang",23} ,{"xiaoli",18}, {"xiaozhang",29} };
int sz = sizeof(s) / sizeof(s[0]);
qsort(s, sz, sizeof(s[0]), compar);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%s ", s[i].name);
}
return 0;
}
前面的xiao都相同,从第五个字符开始w,l,z都不同,于是通过对w,l,z的大小比较来进行排列。由于l<w<z,最后结果则是xiaoli,xiaowang,xiaozhang。那我们再试着对age排序吧~
struct stu
{
char name[20];
int age;
};
int compar(const void* a, const void* b)
{
return (((struct stu*)a)->age) - (((struct stu*)b)->age);
}
int main()
{
struct stu s[3] = { {"xiaowang",23} ,{"xiaoli",18}, {"xiaozhang",29} };
int sz = sizeof(s) / sizeof(s[0]);
qsort(s, sz, sizeof(s[0]), compar);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", s[i].age);
}
return 0;
}
怎么样,是不是感觉以前很难实现的问题现在使用qsort就能非常轻松简便的解决啦~
四、构造并模拟实现qsort函数
其实我们学到刚刚也只是学会了如何使用qsort,如果想真的弄懂qsort的运作原理,不妨我们尝试一下,以冒泡排序的函数为基底,对冒泡排序函数一步步进行改造,将它变成一个qsort函数呢?
①BubbleSort的函数定义
对于自行构造出一个函数并使它实现qsort的功能,首先我们需要写出函数接收的参数,而参数并不需要什么多余思考,我们只需要借鉴qsort函数的参数就好了。
void BubbleSort (void* base, size_t num, size_t size,int (*compar)(const void*,const void*));
但是我们需要思考一下:为什么传进BubbleSort函数中的base要定义成void*类型呢?
这是因为想要实现为不同类型的数据进行排序,就要能够做到对各种类型的数据进行接收,如果我们在这里只是单一的写成int*,那么就无法对char*类型进行接收,如果写成char*,又无法对结构体类型进行接收,而void * 为"无类型指针",可以指向任意类型的数据,就是说可以用任意类型的指针对 void 指针赋值。
而后面函数指针中传参的const void*类型也是同样的道理。
②BubbleSort中实现元素互换
在BubbleSort()函数中,我们并无法确定接收到的会是什么类型的数据,因此我们无法像之前的整形冒泡排序一样,直接以一个整形变量tmp间接的转换元素。而我们如果直接使用某种类型来接收,就会导致我们无法接收其他的元素。而此时我们可以选择不去管它传进来的到底是什么类型(因为就算纠结,也还是不知道传进的是什么),就算我们不直接对值进行交换,别忘了我们还学过指针呢~而指针通常以字节为单位,虽然我们不知道传进来的是什么类型的参数,但是我们可以知道一个类型变量占多少个字节,而将两个元素的所有的字节都相互替换,也就能够做到互换两个元素啦~
void swap(char* p1, char* p2, int sz)//用char是因为char只有一个字节
{ //这样才能做到每次只跳过一个字节
int i = 0; //从而做到交换每一个字节
for (i = 0; i < sz; i++)
{
char tmp = *p1;
*p1 = *p2;
*p2 = tmp;
p1++;
p2++;
}
}
这样就能实现利用指针,一个字节一个字节的交换,实现元素的交换~
③BubbleSort的函数结构体
对于BubbleSort函数的结构,我们就仿照冒泡排序的结构就可以,但我们仍然不清楚传进来的参数是什么类型,所以没办法直接进行对比,还是用字节来一个字节一个字节的进行比较,所以传进compar的两个参数也强制转换成char*类型,对之后的操作也都方便。
void BubbleSort(void* base, size_t num, size_t size, int (*compar)(const void* a, const void* b))
{
int i = 0;
int j = 0;
for (i = 0; i < num - 1; i++)
{
for (j = 0; j < num - 1 - i; j++)
{
if (compar((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
{
swap((char*)base + j * size, (char*)base + (j + 1) * size,size);
}
}
}
}
这样我们就成功的使用BubbleSort函数构造并模拟实现qsort函数的功能啦~!费尽周折终于算是把qsort模拟出来了,那么让我们尝试一下,用我们自己构造的BubbleSort给整型数组排序一下试试看吧~
int compar(const void* a, const void* b)
{
return *(int*)a - *(int*)b;
}
void swap(char* p1, char* p2, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
char tmp = *p1;
*p1 = *p2;
*p2 = tmp;
p1++;
p2++;
}
}
void BubbleSort(void* base, size_t num, size_t size, int (*compar)(const void* a, const void* b))
{
int i = 0;
int j = 0;
for (i = 0; i < num - 1; i++)
{
for (j = 0; j < num - 1 - i; j++)
{
if (compar((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
{
swap((char*)base + j * size, (char*)base + (j + 1) * size,size);
}
}
}
}
int main()
{
int arr[] = { 7,8,4,5,1,6,2,9,3,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
BubbleSort(arr, sz, sizeof(arr[0]), compar);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
结果是没有问题的呢,那么我们再尝试一下给字符串儿排序。
int compar(const void* a, const void* b)
{
return strcmp((char*)a, (char*)b);
}
void swap(char* p1, char* p2, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
char tmp = *p1;
*p1 = *p2;
*p2 = tmp;
p1++;
p2++;
}
}
void BubbleSort(void* base, size_t num, size_t size, int (*compar)(const void* a, const void* b))
{
int i = 0;
int j = 0;
for (i = 0; i < num - 1; i++)
{
for (j = 0; j < num - 1 - i; j++)
{
if (compar((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
{
swap((char*)base + j * size, (char*)base + (j + 1) * size,size);
}
}
}
}
int main()
{
char arr[] = "hdngijedg";
int sz = sizeof(arr) / sizeof(arr[0]);
BubbleSort(arr, sz, sizeof(arr[0]), compar);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%c ", arr[i]);
}
return 0;
}
依然是没有问题的,那么我们再尝试一次性把结构体的name和age都排序输出吧!
struct stu
{
char name[20];
int age;
};
int compar1(const void* a, const void* b)
{
return ((struct stu*)a)->age - ((struct stu*)b)->age;
}
int compar2(const void* a, const void* b)
{
return strcmp(((struct stu*)a)->name,((struct stu*)b)->name);
}
void swap(char* p1, char* p2, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
char tmp = *p1;
*p1 = *p2;
*p2 = tmp;
p1++;
p2++;
}
}
void BubbleSort(void* base, size_t num, size_t size, int (*compar)(const void* a, const void* b))
{
int i = 0;
int j = 0;
for (i = 0; i < num - 1; i++)
{
for (j = 0; j < num - 1 - i; j++)
{
if (compar((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
{
swap((char*)base + j * size, (char*)base + (j + 1) * size,size);
}
}
}
}
int main()
{
struct stu s[3] = { {"xiaowang",19},{"xiaozhang",28},{"xiaoli",12}};
int sz = sizeof(s) / sizeof(s[0]);
BubbleSort(s, sz, sizeof(s[0]), compar1);
BubbleSort(s, sz, sizeof(s[0]), compar2);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%s ", s[i].name);
}
printf("\n");
for (i = 0; i < sz; i++)
{
printf("%d ", s[i].age);
}
return 0;
}
也是运行成功啦~这样看来我们的构造并模拟实现qsort函数就大功告成啦,那今天关于回调函数的知识分享就到这里啦,我们下次再见哦~