在日常代码编写过程中,函数指针可以在一些情况下大大优化代码的时间复杂度和空间复杂度,而回调函数则是在进行复杂函数实现即编写过程中一种更加高效的工具。
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外一方调用的,用于对该事件或条件进行响应。
目录
一、回调函数的运用原理
#include <stdio.h>
void menu()
{
printf("*******************\n");
printf("*******1.ADD*******\n");
printf("*******2.SUB*******\n");
printf("*******************\n");
}
int add(int x, int y)
{
return x + y;
}
int sub(int x, int y)
{
return x - y;
}
void Calc(int(*p)(int x, int y))
{
int x = 0;
int y = 0;
printf("请输入要进行运算的数字:>\n");
scanf("%d %d",&x,&y);
int ret = p(x, y);
printf("运算的结果是: %d\n",ret);
}
int main()
{
int n = 0;
int x = 0;
int y = 0;
menu();
printf("请选择:>\n");
scanf("%d",&n);
switch (n)
{
case 1:
Calc(add);
case 2:
Calc(sub);
}
return 0;
}
为了方便大家理解回调函数这个概念,通过上面一段简洁板的整数加减代码来进行演示。
第一步,创建函数ADD和SUB分别进行加减法的运算处理。创建Calc函数用来获取所需要计算的数据并调用ADD或SUB函数进行运算。
第二步,打印菜单,选择使用加法还是减法,选择完成后进入switch语句,根据所选值来进行不同的运算。
第三步,如果是case 1就向Calc传参add的指针,如果是case 2就向Calc传参sub的指针。
第四步,Calc接收函数指针并存放在函数指针p中,在scanf接收x,y两个值后,通过p调用add或sub并对x,y进行传参,在接收返回值后进行打印。此时add就是回调函数。
此代码只是一个简单的演示,类似于switch语句在不同情况下需要做出多种同类型运算的情况下,使用回调函数可以大大优化代码,减少其复杂度。
二、qsort函数
冒泡排序是一种最基础的排序写法。而qsort作为C语言自带的排序函数,它自身的优点也非常的显而易见:
1.效率比冒泡排序更高,并且使用起来更加方便。
2.适合于任意类型数据的排序。
qsort函数的参数由四部分构成,
1、void* base指向了需要排序的数组的第一个元素。
2、size_t表示排序元素的个数。
3、size_t size 则是一个元素的大小,单位是字节。
4、int (*cmp) (const void*,const void*) 是函数指针类型-这个函数指针指向的函数,能够比较base指向数组中的两个元素的大小。这个函数的实现需要自己去构建。而qsort就是比较典型的运用回调函数的库函数。下面我们就将通过模拟qsort的实现来更加清晰地熟悉回调函数的使用方式以及qosrt函数的实现方式。
在使用之前需要声明它的头文件 #include <stdlib.h>
了解qsort的组成之后,我们可以根据要求通过qsort来编写一段代码来把一个乱序数组,从小到大按升序排列并进行打印。
//void qsort(
// void* base,
// size_t num,
// size_t size,
// int (*compar)(const void*, const void*)
//);
#include <stdio.h>
#include <stdlib.h>
int test(const void* x, const void* y)
{
return *(int*)x - *(int*)y;
}
int main()
{
int arr[] = { 3,4,6,7,8,1,2,5,10,9 };
int sz = sizeof(arr) / sizeof(arr[0]);
int size = sizeof(arr[0]);
int (*compar)(const void*, const void*) = test;
qsort(&arr[0],sz,size, compar);
for (int i = 0; i < sz; i++)
{
printf("%d ",arr[i]);
}
return 0;
}
在将qsort所需参数依次传过后我们将arr数组中的元素依次打印出来,如下图所示,qsort可以高效的对arr进行重新排序。
三、qsort函数模拟实现
qsort函数对于排序非常的方便快捷,而为了更好的了解和掌握其运用情景和具体细节,下面将自创一个功能和qsort一样的bubble_sort函数来对其进行模拟和实现。
根据上面的qsort所需的四个参数我们可以分别知道它们的用处和功能,第一个参数是数组首元素的地址,只有获取第一个数据的地址才能接着通过在地址上根据数据类型做加减操作从而找到下一个地址。
第二个参数是数组所含元素个数,知道了元素个数才能计算出比较的趟数。
第三个参数是数组单个元素大小,获取单个元素大小才能得出数据类型和单个元素所占内存大小。
第四个参数就是自己所创建的比较函数的函数指针,从而在对数据进行比较时通过回调函数来调用函数,根据比较函数的比较方式来对数组进行排序。
void bubble_sort(void* base, int num, int size, int (*cmp)(const void*, const void*))
{
int i = 0;
for (i = 0; i < num - 1; i++)
{
int j = 0;
for (j = 0; j < num - 1 - i; j++)
{
if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
{
Swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
}
}
}
}
int cmp(const void* x, const void* y)
{
return *(int*)x - *(int*)y;
}
void Swap(char* buf1,char* buf2,int size)
{
int i = 0;
char tmp = 0;
for (i = 0; i < size; i++)
{
tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
通过以上代码可以看到,base传参的类型用void型,因为qsort函数可以用于任何类型的排序,而各类型数据所占内存的大小都是各不相同的,所以用void就可以接收任意类型的数据,不会出现因类型不同而引起的内存分配问题。
而int i决定趟数int j决定每次比较的次数,两两比较如果前一个数大于后一个数则交换,小于则进行下一次比较。第一趟结束后最大的元素一定是位于最后面,所以下一趟比较最后一个数就不需要参加比较,可以避免重复的无意义比较,大大节约代码运行时间。
在向cmp这个自创的比较函数传参时,因为bubble_sort接收的base是void类型,此时编译器不知道数据的具体类型,也无法自动进行识别,此时size的作用就体现了出来。因为可能每次需要比较的类型不同,而void型无法直接使用,所以此时直接将void*强制类型转换成char*,char作为最小的数据类型只有一个字节。指针类型在进行整数加减时,是根据其类型来决定指针变化的步长的,而char*在+1或者-1时因为char只占一个字节,所以char*也只会往后移动一个一个字节,而想要做到不管是什么类型都可以精确的把所需要比较的元素传给cmp,此时就可以拿base加上数据类型的字节个数也就是size乘上每次需要传参的数据所在的位数也就是j,从而实现任何类型都可以兼容的传参方式。比如需要对int型的数组进行排序,先拿第一个元素和第二个元素进行比较,
(char* )base+0*4就得到了第一个元素的地址。(char*)base+1*4,此时往后跳了4个字节,刚好是一个整形的大小,就得到了第二个元素的地址。
进入cmp因为cmp的参数类型也是void*,所以在return时将参数类型也强制类型转换成int*然后用前一个参数减去后一个参数然后return,如果大于0就证明前数大于后数走Swap函数进行交换,小于0证明前数小于后数,就跳过if语句进行下一轮比较。
如果前数大于后数那么就进行交换,这里用Swap函数来进行实现,和cmp传参时一样,由于数据类型可能不同Swap函数也是通过char*的方式来将两个数据一个一个字节进行交换,size是数据所占字节个数,用来决定循环的次数,然后就是一个循环交换一个字节,然后++进入下一个循环。最后将两个数据完成交换。
本文以qsort为切入点对回调函数的原理和使用进行叙述,在日后进行庞大的时间复杂度和空间复杂度都较高的代码编写时,熟练掌握回调函数的使用会大大的减少繁杂冗余的多次重复编写,可以大大减少我们的工作量,提高代码运行效率的同时也增强了代码的可读性,使其更加凝练更加简介。
本章内容到此结束,更多好文关注博主CSDN。一键三连不迷路。