今天我们学习指针难点之回调函数🆗🆗🆗。
首先我们用思维导图回顾一下前面的内容。
思维导图
回调函数
回调函数就是一个通过函数指针调用的函数。
如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
案例1—计算器
就前文我们学习的计算器,我们再用回调函数来解决一下!🆗🆗🆗
#define _CRT_SECURE_NO_WARNINGS 1 //计算器 #include<stdio.h> void meau() { printf("**************************\n"); printf("** 1.add 2.sub ****\n"); printf("** 3.mul 4.div ****\n"); printf("** 0.exit *****\n"); printf("**************************\n"); } 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; } int main() { int input = 0; int x = 0; int y = 0; int ret = 0; do { meau(); printf("请选择>"); scanf("%d", &input); switch (input) { case 1: printf("请输入2个操作数:"); scanf("%d %d", &x, &y); ret = Add(x, y); printf("ret=%d\n", ret); break; case 2: printf("请输入2个操作数:"); scanf("%d %d", &x, &y); ret = Sub(x, y); printf("ret=%d\n", ret); break; case 3: printf("请输入2个操作数:"); scanf("%d %d", &x, &y); ret = Mul(x, y); printf("ret=%d\n", ret); break; case 4: printf("请输入2个操作数:"); scanf("%d %d", &x, &y); ret = Div(x, y); printf("ret=%d\n", ret); break; case 0: printf("退出游戏"); break; default: printf("选择错误,重新选择\n"); break; } } while (input); return 0; }
回调函数
#define _CRT_SECURE_NO_WARNINGS 1 //计算器 #include<stdio.h> void meau() { printf("**************************\n"); printf("** 1.add 2.sub ****\n"); printf("** 3.mul 4.div ****\n"); printf("** 0.exit *****\n"); printf("**************************\n"); } 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(*p)(int, int))//函数指针传参 { int x = 0; int y = 0; printf("请输入两个操作数\n"); scanf("%d %d", &x, &y); int ret = p(x, y);//函数调用 printf("ret=%d\n", ret); } int main() { int input = 0; do { meau(); printf("请选择>\n"); 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("退出游戏"); break; default: printf("选择错误,重新选择\n"); break; } } while (input); return 0; }
解释如下:
在main函数中,没有直接去调用函数。而是把函数指针传参给另外的一个函数calc,在calc内部使用函数指针调用,通过函数指针就可以找到指针指向的函数,此刻被指向的函数就是回调函数。
像上图所示,通过calc函数调用Add函数指针,找到Add函数,就把Add函数称为回调函数。
calc称为回调函数的机制
老板>>组长>>员工
案例2—qsort函数
那出了上面回调函数的案例,还有一个经典回调函数的案例:qsort
qsort是一个库函数,底层使用的是快速排序的方式,对不同数据进行排序的。
这个函数可以直接使用。
这个函数可以用来排序任意类型的数据。
对数据进行排序方法很多:
冒泡排,序选择排序,插入排序,快速排序等等。
关于qsort函数
NO1.
关于qsort函数的点--->qsort - C++ Reference (cplusplus.com)
需要包含头文件#include
- 排序整型数组,两个整型可以直接使用>比较
- 排序结构体数组,两个结构体的数据可能不能直接使用>比较
也就是不同类型的数据,比较大小的方法是有差异的
最后一个参数,排序不同数据的重要点,需要封装不同的函数去比较不同的数据的大小
void qsort(void* base, //指向了待排序数组第一个元素的首地址 size_t num, //待排序数组的元素个数 size_t size,//每个待排序数组元素的大小 int (*compar)(const void* e1, const void* e2)); //函数指针,compar指向了一个函数,这个函数是用来比较两个元素的大小, //e1和e2存放的是两个元素的地址 //在qsort内部调用这个函数,指向这个函数,这个函数就被称为回调函数 // //qsort内部怎么排序我们不需要过多去探讨 //const也暂不做讲解 //因为不知道要比较的元素类型,所以我们使用void*指针的类型,来统一存放各种类型的指针
NO2.
那怎样通过元素地址,去比较两个整型元素数据的大小呢?
int的数据:将void*类型的数据强制转化成(int*),再作差。
当e1>e2,函数返回>0的值;
当e1
当e1=e2,函数返回=0
//整型 void compar_int(const void* e1, const void* e2) { return *(int*)e1 - *(int*)e2; }
NO3.
那怎样通过元素地址,去比较两个字符元素数据的大小呢?
//字符串 void compar_stu_by_name(const void* e1, const void* e2) { return strcmp(((struct Stu*)e1)->name , ((struct Stu*)e2)->name); //return strcmp((*(struct Stu*)e1).name , (*(struct Stu*)e2).name); }
NO4.
那有人询问为什么不直接对元素地址const void* e1和 const void* e2解引用?
作为void*指针不能直接解引用。
void* 类型的指针—不能进行解引用操作符,也不能进行+-整数的操作
void* 类型的指针是用来存放任意类型数据的地址
void* 无具体类型的指针
void*和int*和char*一样都是指针类型
#include<stdio.h> int main() { char a = 'x'; char* pa = &a; int b = 1; void* p = &b;//存放int* p = &a;//存放char* return 0; }
演示qsort函数的使用
#include<stdio.h> #include<stdlib.h> void print(int arr[], int sz) { int i = 0; for (i = 0; i < 10; i++) { printf("%d ", arr[i]); } } void qsort(void* base, size_t num, size_t size, int (*compar)(const void*, const void*)); void compar_int(const void* e1, const void* e2) { return *(int*)e1 - *(int*)e2; } void test1() { int arr[] = { 10,9,8,7,6,5,4,3,2,1 }; int sz = sizeof(arr) / sizeof(arr[0]); print(arr, sz); printf("\n"); qsort(arr, sz, sizeof(arr[0]), compar_int); print(arr, sz); } int main() { test1(); test2(); return 0; }
以上我只是以整型为例,结构体数据数组也是一样的逻辑,大家可以自行分析。
下面结构体:
#include<stdio.h> #include<stdlib.h> #include<string.h>//strcmp的头文件 void qsort(void* base, size_t num, size_t size, int (*compar)(const void*, const void*)); struct Stu { char name[20]; int age; }; //结构体数据怎么比较呢? //按照年龄比较 //按照名字比较 //按照年龄 int compar_stu_by_age(const void* e1, const void* e2) { return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age; //return (*(struct Stu*)e1).age - (*(struct Stu*)e2).age; } void test2() { struct Stu arr[] = { {"zhangsan",20},{"lisi",30},{"wangwu",12} }; int sz = sizeof(arr) / sizeof(arr[0]); qsort(arr, sz, sizeof(arr[0]), compar_stu_by_age); } //按照名字 int compar_stu_by_name(const void* e1, const void* e2) { return strcmp((struct Stu*)e1)->name , ((struct Stu*)e2)->name); return strcmp((*(struct Stu*)e1).name ,(*(struct Stu*)e2).name); //return (*(struct Stu*)e1).name - (*(struct Stu*)e2).name;//❌❌ } void test2() { struct Stu arr[] = { {"zhangsan",20},{"lisi",30},{"wangwu",12} }; int sz = sizeof(arr) / sizeof(arr[0]); qsort(arr, sz, sizeof(arr[0]), compar_stu_by_name); } int main() { test2(); return 0; }
- 记住字符串的比较必须用strcmp函数
- 两种方式去访问结构体
- 所有要使用的变量必须在它被使用之前就声明了!!例如结构体必须在比较函数之前声明。
- 有返回值return 就不能是void!!
只要qsort函数使用得当,可以对任何数据进行排序!🆗🆗
案例3—冒泡排序
(使用回调函数,模拟实现qsort(采用冒泡的方式)
整型数据冒泡排序
(这种方式只能排列整数,存在局限性)
//冒泡排序 #include<stdio.h> void bubble_sort(int arr[], int sz) { int i = 0; for (i = 0; i < sz - 1; i++) { int j = 0; for (j = 0; j < sz - 1 - i; j++) { if (arr[j] > arr[j + 1]) { int tmp = 0; tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp; } } } } void print_arr(int arr[], int sz) { int i = 0; for (i = 0; i < sz; i++) { printf("%d ", arr[i]); } } int main() { int arr[10] = { 9,8,7,6,5,4,3,2,1,0 }; int sz = sizeof(arr) / sizeof(arr[0]); bubble_sort(arr, sz); print_arr(arr, sz); }
回调函数搞定各类型冒泡排序
经过分析冒泡排序,我们得到
void bubble_sort(void* base, size_t num, size_t size,
int (*cmp)(const void* e1, const void* e2))
cmp_int比较大小
以整型为例
int (*cmp)(const void* e1, const void* e2)e1是一个指针,存放了一个要比较的元素的地址。
e2是一个指针,存放了一个要比较的元素的地址。
e1指向的元素>e2指向的元素,返回>0的数字。
e1指向的元素0的数字。
e1指向的元素==e2指向的元素,返回>0的数字。
cmp是函数指针指向一个我们程序想要待排序的数组。
将比较函数cmp_int的地址传给cmp即可。
//比较大小 void cmp_int(const void* e1, const void* e2) { return *(int*)e1 - *(int*)e2; } //这里就是将cmp_int的地址在调用函数bubble_sort时将其传过去即可。
cmp传参数
NO1.
有同学提出直接对待排序的数组首元素地址解引用找到e1的地址,然后通过一个元素的大小或者+1可以找到e2的地址,可以吗?当然不可以
- 作为void*指针不能直接解引用。
void* 类型的指针—不能进行解引用操作符,也不能进行+-整数的操作
void* 类型的指针是用来存放任意类型数据的地址
void* 无具体类型的指针
void*和int*和char*一样都是指针类型
NO2.
有同学又提出那将void*的指针强制转换成我们想要的int*或double*等,再+1可以吗? 不可以,理由就是,强制转换存在在于我们公共的bubble_sort排序函数中时,不能随着待排序的数组数据类型不同而改变,我们只能改变不同数据类型的不同比较方法。
解决方案
//if(arr[j]>arr[j+1]) if (cmp( (char*)base+j*size,(char*)base+(j+1)*size )>0) { int tmp = 0; tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp; }
交换swap
当我们只知道元素的起始地址,并不知道元素的类型所以我们并没有合适的中间值类型tmp创建。所以我们换一种方法。
我们已知元素e1和e2的起始地址和每个元素的大小。
那我们可以用一个一个char类型的数据交换,用for循环
直到每个元素的大小size结束,也就是元素交换完成。
//交换数据 void change(char* buf1, char* buf2,size_t size) { char i = 0; for (i = 0; i < size; i++) { char tmp = 0; tmp = *buf1; *buf1=*buf2; *buf2 = tmp; buf1++;//*buf1++ buf2++;//*buf2++ } }
总代码
//冒泡排序 #include<stdio.h> #include<stdlib.h> void print_arr(int arr[], int sz) { int i = 0; for (i = 0; i < sz; i++) { printf("%d ", arr[i]); } } void bubble_sort(void* base, size_t num, size_t size, int (*cmp)(const void* e1, const void* e2)) { int i = 0; for (i = 0; i < num - 1; i++) { int j = 0; for (j = 0; j < num - 1 - i; j++) { //if(arr[j]>arr[j+1]) if (cmp( (char*)base+j*size,(char*)base+(j+1)*size )>0) { change((char*)base + j * size, (char*)base + (j + 1) * size, size); } } } } //交换数据 void change(char* buf1, char* buf2,size_t size) { char i = 0; for (i = 0; i < size; i++) { char tmp = 0; tmp = *buf1; *buf1=*buf2; *buf2 = tmp; buf1++;//*e1++ buf2++;//*e2++ } } //比较大小 void cmp_int(const void* e1, const void* e2) { return *(int*)e1 - *(int*)e2;//>0 } void test1() { int arr[10] = { 9,8,7,6,5,4,3,2,1,0 }; int sz = sizeof(arr) / sizeof(arr[0]); print_arr(arr, sz); printf("\n"); bubble_sort(arr, sz, sizeof(arr[0]), cmp_int); print_arr(arr, sz); } int main() { test1(); }
当然我们也可以用结构体类型去测试一下! 🆗🆗试试
//冒泡排序 #include<stdio.h> #include<stdlib.h> #include<string.h> //打印函数 void print_arr(int arr[], int sz) { int i = 0; for (i = 0; i < sz; i++) { printf("%d ", arr[i]); } } //排序函数 void bubble_sort(void* base, size_t num, size_t size, int (*cmp)(const void* e1, const void* e2)) { int i = 0; for (i = 0; i < num - 1; i++) { int j = 0; for (j = 0; j < num - 1 - i; j++) { //if(arr[j]>arr[j+1]) if (cmp( (char*)base+j*size,(char*)base+(j+1)*size )>0) { change((char*)base + j * size, (char*)base + (j + 1) * size, size); } } } } //交换数据函数 void change(char* buf1, char* buf2,size_t size) { char i = 0; for (i = 0; i < size; i++) { char tmp = 0; tmp = *buf1; *buf1=*buf2; *buf2 = tmp; buf1++;//*e1++ buf2++;//*e2++ } } //结构体 struct Stu { char name[20];//20 int age;//4 }; //结构体数据怎么比较呢? //按照年龄比较 //按照名字比较 //按照年龄 void compar_stu_by_age(const void* e1, const void* e2) { return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age; //return (*(struct Stu*)e1).age - (*(struct Stu*)e2).age; } void test2() { struct Stu arr[] = { {"zhangsan",20},{"lisi",30},{"wangwu",12} }; int sz = sizeof(arr) / sizeof(arr[0]); qsort(arr, sz, sizeof(arr[0]), compar_stu_by_age); } //按照名字 void compar_stu_by_name(const void* e1, const void* e2) { return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name); //return strcmp((*(struct Stu*)e1).name , (*(struct Stu*)e2).name); } void test2() { struct Stu arr[] = { {"zhangsan",20},{"lisi",30},{"wangwu",12} }; int sz = sizeof(arr) / sizeof(arr[0]); qsort(arr, sz, sizeof(arr[0]), compar_stu_by_name); } int main() { test2(); }
我们把回调函数这种情况叫做泛型编程。即便泛型编程在C语言中比较牵强。
最后,提出一个问题上面的题目我们都是正序排序。那如果我们想要倒序排序,代码又要怎样去修改呢?什么代码可以修改,什么代码不能修改呢?
那其实我们也在上面提到过,我们的排序函数代码bubble_sort是不能修改的。
以整型为例,所以我们只能修改比较大小的函数cmp_int
✔✔✔
//倒叙 //比较大小 void cmp_int(const void* e1, const void* e2) { return *(int*)e2 - *(int*)e1;//>0 } //正序 //比较大小 void cmp_int(const void* e1, const void* e2) { return *(int*)e1 - *(int*)e2;//>0 }
✔✔最后,感谢大家的阅读,若有错误和不足,欢迎指正!旗鼓相当
代码------→【gitee:唐棣棣 (TSQXG) - Gitee.com】
联系------→【邮箱:2784139418@qq.com】