前言
回调函数是一种非常常见的编程技术,在许多不同的编程语言和框架中都有广泛的应用。但它到底是什么,以及如何使用呢?本期我们就来说说什么是回调函数,以及回调函数的基础应用
什么是回调函数?
关于回调函数是这样定义的。
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
但看这个定义或许你会觉得很绕,那么我们看下面的代码:
上期我们提到使用函数指针数组可以很好的解决代码冗余的问题,今天我们在此以简单计算器的实现为例,我们使用函数指针来解决(回调函数)
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 Calc(int (*pf)(int, int)) { int x = 0; int y = 0; int ret = 0; printf("请输入两个操作数:"); scanf("%d %d", &x, &y); ret = pf(x, y); printf("ret = %d\n", ret); } int main() { int input = 0; int x = 0; int y = 0; int ret = 0; do { menu(); printf("请选择:>"); scanf("%d", &input); switch (input) { case 1: Calc(Add); break; case 2: Calc(Sub); break; case 3: Calc(Mul); break; case 4: Calc(Div); break; case 0: printf("退出计算器\n"); break; default: printf("选择错误,重新选择\n"); break; } } while (input); return 0; }
以这个简单的程序为例,我们来看一下到底哪个是回调函数?
在上述代码中
Calc
函数,它接受一个函数指针作为参数(int (*pf)(int, int)
),根据不同的选择传入不同函数的地址,并在内部通过该函数指针调用相应的函数完成计算。
而Add
、Sub
、Mul
和Div
函数被Calc
函数调用,当Add函数被调用时Add就是回调函数,当Sub函数被调用时Sub就是回调函数,当我们通过Calc中的pf调用Add、Sub、Mul、Div这些函数时,这些函数就被称为回调函数。
那我们为什么要使用回调函数呢?
通过程序可以发现使用回调函数的方法我们也可以避免程序冗余的问题。这也是它其中的一个特点之一。
回调函数的优势在于可以将代码的控制权交给调用方,使得调用方能够自定义需要执行的功能。这种灵活性使得回调函数在事件处理、异步编程等场景中得到广泛应用。
回调函数的应用
说到回调函数的应用,这里我们就要提及qsort函数的应用了。
qsort函数
一般我们在写的简单排序,如冒泡排序时都只是对简单的整数排序。排序的数据类型也比较单一,但是qsort函数不同,qsort函数可以用于排序任何数据类型,包括基本数据类型(如整型、浮点型等)和自定义数据类型(如结构体、类等)。
它是C标准库中的一个函数,它的函数原型如下:
void qsort(void *base, size_t num, size_t size, int (*compar)(const void *, const void *));
qsort函数接受四个参数:
1.void *base:指向待排序数组的首元素的指针。
2.size_t num:数组中元素的个数。
3.size_t size:每个元素的大小(以字节为单位)。
4.int (*compar)(const void *, const void *):指向比较函数的指针。
比较函数compar 用于定义排序的顺序。它接受两个指向待比较元素的指针,并返回一个整数值,表示两个元素的大小关系。根据返回值的不同,qsort函数会按照升序或降序对数组进行排序。
qsort函数的使用
qsort函数就用到了回调函数。
我们先以最简单的整形排序为例,使用qsort函数进行排序。
#include <stdio.h> //qosrt函数的使用者得实现一个比较函数 int int_cmp(const void * p1, const void * p2) { return (*( int *)p1 - *(int *) p2); } int main() { int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 }; int i = 0; qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp); for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++) { printf( "%d ", arr[i]); } printf("\n"); return 0; }
qsort函数的使用其实也很简单,重点就在于比较函数compar的定义。
比较函数的函数原型如下:
int compare(const void *a, const void *b);
a和b是指向待比较元素的指针,类型为const void*
。在比较函数中,我们需要将这些指针转换为正确的类型,并进行比较。
返回值的含义如下:
- 如果
a
小于b
,则返回一个负整数。- 如果
a
等于b
,则返回 0。- 如果
a
大于b
,则返回一个正整数
这里的void*大家是否疑惑,一般我们见到的都是整形、字符型、浮点型、结构体类型等等,几乎都没见过void*类型的指针。
void* 是C语言中的一种通用指针类型,可以用来指向任意类型的数据。void* 类型的指针可以存储任何类型的地址,但不能直接解引用,因为编译器不知道指针指向的具体类型,这里就是为什么要对指针进行强制类型转换的原因。
在以上示例中,我们首先将指针p1 和 p2转换为 int* 类型,然后通过解引用操作符 *
获取指针指向的值,最后返回它们的差值,当然如果默认返回值,排序都是按照依次递增排序的,想要按照递减排序只需调整返回值的正负号即可。
如我想让数组递减排序就可以这样修改:
int int_cmp(const void * p1, const void * p2) { return (-(*( int *)p1 - *(int *) p2));//或者 return (*( int *)p2 - *(int *) p1) }
既然我们已经了解了qsort函数的基本使用方法,那我们来练一练。答案我会放在本期博客的最后
使用qsort对结构体进行比较(提示:结构体比较可以根据结构体某个成员进行比较排序)。
qsort模拟实现
我们将会使用大家都熟悉的冒泡排序来模拟实现qsort函数,以达到可以排序任意类型的数据。
我们先使用函数封装一个冒泡排序的函数:
void bubble(int* a, int sz) { for (int i = 0; i < sz - 1; i++) { for (int j = 0; j < sz - 1 - i; j++) { if(a[j]>a[j+1]) { int t = a[j]; a[j] = a[j + 1]; a[j + 1] = t; } } } } int main() { int a[10] = { 1,3,6,8,0,9,7,4,2,5 }; int sz = sizeof(a) / sizeof(a[0]); bubble(a, sz); for (int i = 0; i < 10; i++) { printf("%d ", a[i]); } return 0; }
当前的冒泡排序仅限于整形的排序,并不能像qsort那样可以排任意类型的数据,我们依据这个特点对程序进行分析。
问题一:排序单调,函数只能接收整形的数据。
问题二:比较大小,数据类型不同不能直接的使用 “ > ”和“ < ”比较大小
问题三:交换,数据类型不同交换的方法也不同。
这将是我们接下来要解决的问题。
问题一:排序单调问题,我们可以模仿qsort函数使用void*类型的函数去接收不同类型的参数
void bubble(void* arr, int num, int sz, int(*cmp)(const void*, const void*))
问题二:数据比较问题,我们可以像qsort函数那样,定义一个compar函数比较关系,重难点就在于cmp函数的实现。对于整形来说很简单,可以套用上边qsort函数的格式去写,也仅仅只是修改一下变量名。那对于字符串我们要怎么进行比较呢?
在这之前我们需要知道字符串是如何比较的规则如下:
- 如果两个字符串在相同位置上的字符相等,则继续比较下一个位置的字符。
- 如果两个字符串在相同位置上的字符不相等,则返回第一个不相等字符的ASCII码差值(即第一个字符串的字符ASCII码减去第二个字符串的字符ASCII码)。
- 如果遇到其中一个字符串的结尾(即遇到'\0'字符),则返回两个字符串长度的差值(即第一个字符串的长度减去第二个字符串的长度)。
注意点:字符串比较,我们可想到哪个库函数? strcmp,它可以实现对字符串的比较。
如果两个字符串相等,则返回0;如果第一个字符串小于第二个字符串,则返回一个负值;如果第一个字符串大于第二个字符串,则返回一个正值。
return strcmp((*(char*)p1), (*(char*)p2));
出了返回值类型,我们还需要关注的是cmp函数传参的数据类型,相邻两元素作为参数传递给cmp函数,那要怎么设计才能传递任何类型的数据呢?
我们已经知道传参无论是什么类型,传进去的都是数组首元素的地址,根据这点我们可以进行设计:
cmp((char*)arr + j * sz, (char*)arr + (j + 1) * sz)
为什么选择强制类型转化为字符型,因为字符在内存中仅占一个字节,是占内存最小的数据类型,所以char类型是最合适的选择。并且数据的类型不同,arr+1所跳过的空间大小也不同,我们需要通过char类型模拟各个类型的数组遍历,于是便这样设计:(char*)arr + j * sz(sz为传入数据类型所占的字节大小)。
通过+j*sz以实现对任意类型的数组相邻两元素的传参。
一整形为例:
如图所示,数组相邻两个元素之间相差4个字节(不同的数据类型占空间大小也各不相同),正常情况下,数组名+1会自动跳到第二个元素的位置。但我们在上述代码中,将arr强制类型转化为char类型,这时数组名+1就会指向如下图中的位置
这样并不能将数据完整的传过去。所以我们需要根据不同的数据所占的字节大小自动的调整跳过的字节大小,所以(char*)arr + j * sz,整形的sz为4一次跳过四个字节的空间,刚好可以将数据完整的传递。
问题三:数据交换问题,交换的数据可能是字符,整形,浮点型数据,那么我们就要设计一个能够符合所有类型数据交换的函数(swap函数)。
void bubble(void* arr, int num, int sz, int(*cmp)(const void*, const void*)) { for (int i = 0; i < num - 1; i++) { for (int j = 0; j < num - 1 - i; j++) { if (cmp((char*)arr + j * sz, (char*)arr + (j + 1) * sz)>0) { swap((char*)arr + j * sz, (char*)arr + (j + 1) * sz,sz); } } } }
结合上述思路,我们将相邻两元素传到swap函数中,强制类型转换为char类型来实现不同类型数据的传参。此外我们在传值到swap函数时也要将sz传过去。
void swap(char* p1, char* p2,int sz) { int t = 0; for (int i = 0; i < sz; i++) { t = *p1; *p1 = *p2; *p2 = t; p1++; p2++; } }
传过去时为char类型,接收也使用char类型。这里的交换是将两元素对应的每个字节一次交换
于是我们就使用冒泡排序来模拟实现了qsort函数
完整代码整理:
void swap(char* p1, char* p2,int sz) { int t = 0; for (int i = 0; i < sz; i++) { t = *p1; *p1 = *p2; *p2 = t; p1++; p2++; } } void bubble(void* arr, int num, int sz, int(*cmp)(const void*, const void*)) { for (int i = 0; i < num - 1; i++) { for (int j = 0; j < num - 1 - i; j++) { if (cmp((char*)arr + j * sz, (char*)arr + (j + 1) * sz)>0) { swap((char*)arr + j * sz, (char*)arr + (j + 1) * sz,sz); } } } }
模拟函数的使用
我们使用我们模拟实现的qsort函数来对结构体进行排序
先创建一个简单的结构体
struct stu { char name[20]; int age; };
创建一个结构体数组。
int main() { struct stu arr1[3] = { {"zhangsan",17},{"lisi",18},{"wangwu",19} }; bubble(arr1, num1, sizeof(arr1[0]), cmp_stu_name); printf("按名字排序:\n"); print_struct(arr1, num1, sizeof(arr1[0])); bubble(arr1, num1, sizeof(arr1[0]), cmp_stu_age); printf("按年龄排序:\n"); print_struct(arr1, num1, sizeof(arr1[0])); return 0; }
cmp函数定义:
int cmp_stu_age(const void* p1, const void* p2) { return (((struct stu*)p1)->age - ((struct stu*)p2)->age); } int cmp_stu_name(const void* p1, const void* p2) { return strcmp(((struct stu*)p1)->name, ((struct stu*)p2)->name); }
完整代码:
struct stu { char name[20]; int age; }; int cmp_stu_age(const void* p1, const void* p2) { return (((struct stu*)p1)->age - ((struct stu*)p2)->age); } int cmp_stu_name(const void* p1, const void* p2) { return strcmp(((struct stu*)p1)->name, ((struct stu*)p2)->name); } void swap(char* p1, char* p2,int sz) { int t = 0; for (int i = 0; i < sz; i++) { t = *p1; *p1 = *p2; *p2 = t; p1++; p2++; } } void bubble(void* arr, int num, int sz, int(*cmp)(const void*, const void*)) { for (int i = 0; i < num - 1; i++) { for (int j = 0; j < num - 1 - i; j++) { if (cmp((char*)arr + j * sz, (char*)arr + (j + 1) * sz)>0) { swap((char*)arr + j * sz, (char*)arr + (j + 1) * sz,sz); } } } } void print_struct(struct stu* arr1,int num, int sz) { for (int i = 0; i < num; i++) { printf("%s %d\n", (arr1 + i)->name, (arr1 + i)->age); } printf("\n"); } int main() { struct stu arr1[3] = { {"zhangsan",17},{"lisi",18},{"wangwu",19} }; int num1 = sizeof(arr1) / sizeof(arr1[0]); bubble(arr1, num1, sizeof(arr1[0]), cmp_stu_name); printf("按名字排序:\n"); print_struct(arr1, num1, sizeof(arr1[0])); bubble(arr1, num1, sizeof(arr1[0]), cmp_stu_age); printf("按年龄排序:\n"); print_struct(arr1, num1, sizeof(arr1[0])); return 0; }
运行结果
我们可以根据所排序的数据类型不同,调整cmp函数。
使用qsort对结构体进行比较答案公布:
#include<stdlib.h> #include<stdio.h> #include<string.h> struct stu { char name[20]; int age; }; int com_stu_age(const void* p1, const void* p2) { return (((struct stu*)p1)->age - ((struct stu*)p2)->age); } int com_stu_name(const void* p1, const void* p2) { return strcmp(((struct stu*)p1)->name, ((struct stu*)p2)->name); } void print1(int num, int sz, struct stu* arr1) { for (int i = 0; i < num; i++) { printf("%s %d\n", (arr1 + i)->name, (arr1 + i)->age); } printf("\n"); } int main() { struct stu arr1[4] = { {"Zhanglong",16},{"Zhaohu",17},{"Wangchao",18},{"Mahan",19}}; int sz1 = sizeof(arr1) / sizeof(arr1[0]); qsort(arr1, sz1, sizeof(arr1[0]), com_stu_age); printf("按年龄排序:\n"); print1(sz1, sizeof(arr1[0]), arr1); qsort(arr1, sz1, sizeof(arr1[0]), com_stu_name); printf("按名字排序:\n"); print1(sz1, sizeof(arr1[0]), arr1); return 0; }
总结
本期内容到这里就要结束了,希望大家能够理解并学会使用会回调函数,回调函数是一种非常有用的编程技术,它可以帮助我们更好地组织和管理代码。通过深入理解回调函数的工作原理和应用场景,我们可以更加高效地编写代码,提高我们的开发效率。最后,感谢阅读!