不懂函数指针的老铁可以先看看这篇文章【指针函数与函数指针】,上车,准备出发
回调函数就是一个通过【函数指针】调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
最大的一个目的,就是为了实现:解耦!
在主入口程序中,把回调函数像参数一样传入库函数。这样一来,只要我们改变传进库函数的参数,就可以实现不同的功能,且不需要修改库函数的实现,变的很灵活,这就是解耦
主函数和回调函数是在同一层的,而库函数在另外一层。如果库函数对我们不可见,我们修改不了库函数的实现,也就是说不能通过修改库函数让库函数调用普通函数那样实现,那我们就只能通过传入不同的回调函数了,这也就是在日常工作中常见的情况
注:使用回调函数会有间接调用,因此,会有一些额外的传参与访存开销,对于MCU代码中对时间要求较高的代码要慎用
功能与菜单
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("***** 5.Cls 0.Exit*****\n");
printf("**************************\n");
}
主程序与回调函数
void calc(int (*p)(int, int))
{
int x = 0, y = 0;
printf("请输入两个运算数:>");
scanf("%d %d", &x, &y);
int ret = p(x, y);
printf("结果为:%d\n", ret);
}
int main(void)
{
int input = 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 5:
system("cls");
break;
case 0:
break;
default:
printf("请输入正确的内容:\n");
break;
}
} while (input);
return 0;
}
通过画图来看一下是如何通过函数指针来实现的回调
学习过数据结构的同学一定接触过【快速排序】,即QuickSort。不了解的可以看看 数据结构 | 十大排序超硬核八万字详解
base
—— 待排序元素的起始地址,类型为【void】表示可以传递任何类型的数组num
—— 表示待排序数据的元素个数size
—— 表示数组中每个元素所占的字节数int (*compar)(const void*, const void*)
—— 函数指针,用于接收回调函数首先我们用它来排下整型数组试试
cmp_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
void test1()
{
int arr[10] = { 2,3,6,7,5,1,4,9,10,8 };
int sz = sizeof(arr) / sizeof(arr[0]);
printarray(arr, sz);
qsort(arr, sz, sizeof(arr[0]), cmp_int);
printarray(arr, sz);
}
运行结果:
解析:
cmp_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
void*
的指针呢?这种类型的指针一般被我们称作为【垃圾桶】,那垃圾桶我们平常都在用,不考虑垃圾分类的话,可以接收任何种类的垃圾,那么在这里就是可以接收任何类型的数据,即整型、字符型、浮点型,甚至是自定义类型它都可以接受int *
的指针,那么它就指向了我们要待排序的数组。但是要怎么比较和交换两个数据呢,这就要看qsort()函数内部的实现了,它是基于快速排序的思想,如果你懂快速排序的话,脑海里立马就能浮现出它们的比较的场景[strcmp]
也是这样的:
-1
0
1
当然,除了上面这种内置类型外,自定义类型的数据也是可以比较的,接下去我们来比较一下两个学生的信息
typedef struct stu {
char name[20];
int age;
}stu;
void test2()
{
stu ss[3] = { {"zhangsan", 22}, {"lisi", 55}, {"wangwu", 33} };
qsort(ss, 3, sizeof(ss[0]), cmp_byname);
//qsort(ss, 3, sizeof(ss[0]), cmp_byage);
}
void*
类型的指针,但是在比较的时候要转换为结构体指针,否则就无法访问到成员了。对于【姓名】的比较是按照首字母的ASCLL码值来的,这里我们直接使用库函数strcmp
即可,比较的规则和qsort()是一致的Cmp_ByName(const void* e1, const void* e2)
{
return strcmp(((stu*)e1)->name, ((stu*)e2)->name);
}
Cmp_ByAge(const void* e1, const void* e2)
{
return ((stu*)e1)->age - ((stu*)e2)->age;
}
首先来看按照名字排序的结果
然后是按照年龄排序的结果
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n - 1; ++i)
{
for (int j = 0; j < n - 1 - i; ++j)
{
if (a[j] > a[j + 1])
{
int t = a[j];
a[j] = a[j + 1];
a[j + 1] = t;
}
}
}
}
但此时我若是要用这个冒泡排序去排任意类型的数据呢?该如何进行修改
qsort()
函数了。我们可以仿照着它的参数来写写看void bubblesort(void* base, int num, int sz, int(*cmp)(const void* e1, const void* e2))
>
、>
、==
了,此处函数指针就派上了用场,我们可是使用函数指针去接收不同的回调函数,继而去实现不同的类型数据的比较,也就是上面所写的Cmp_int
、Cmp_ByName
、Cmp_ByAge
那现在,我们就来实现一下上面说到的这两块内部逻辑
j
和j + 1
这两个位置上的值要如何进行比较的问题,那既然base指向首元素地址,那有同学说不妨让它进行偏移,但是它的类型是void*
,虽然这种类型的指针可以接收各种各样的数据地址, 但是却无法进行偏移,因为它也不知道要偏移多少字节,所以我上面在回调函数内部对两个形参进行了强转才可以进行比较char
类型的字符,在内存中只占有1个字节的大小,那么char*
的指针每次后移便会偏移一个字节,那既然在形参我们传入了数组中每个元素在内存中所占字节数的话,就可以使用起来了,和char*
的指针去做一个配合//判断两数是否需要交换
if (cmp((char*)base + j * sz, (char*)base + (j + 1) * sz) > 0)
{
//两数据交换的逻辑
}
接下去就来实现两数交换的逻辑
char*
指针一个字节一个字节去访问数据的,所以交换的时候也需要按照字节来交换。单独封装一个Swap()函数,把要交换两个数的地址和单个数据所占的字节数传入声明:
void Swap(char* buf1, char* buf2, int sz)
调用:
Swap((char*)base + j * sz, (char*)base + (j + 1) * sz, sz);
内部逻辑就是单个数据的交换【记住,这只是单个数据,所以循环sz次】
void Swap(char* buf1, char* buf2, int sz)
{
//两个数据按照字节一一交换
for (int i = 0; i < sz; ++i)
{
int t = *buf1;
*buf1 = *buf2;
*buf2 = t;
buf1++;
buf2++;
}
}
具体交换细节可以看下图
测试一下:
仔细看一下这张图,你就清楚整个调用过程了
我们为什么要用回调函数呢?
记得在一次C++开发面试的时候被被一位主面官问到过这个问题,现在再回答一遍。
我们对回调函数的使用无非是对函数指针的应用,函数指针的概念本身很简单,但是把函数指针应用于回调函数就体现了一种解决问题的策略,一种设计系统的思想。
在解释这种思想前我想先说明一下,回调函数固然能解决一部分系统架构问题但是绝不能再系统内到处都是,如果你发现你的系统内到处都是回调函数,那么你一定要重构你的系统。回调函数本身是一种破坏系统结构的设计思路,回调函数会绝对的变化系统的运行轨迹,执行顺序,调用顺序。回调函数的出现会让读到你的代码的人非常的懵头转向。
那么什么是回调函数呢,那是不得以而为之的设计策略,想象一种系统实现:在一个下载系统中有一个文件下载模块和一个下载文件当前进度显示模块,系统要求实时的显示文件的下载进度,想想很简单在面向对象的世界里无非是实现两个类而已。但是问题恰恰出在这里,显示模块如何驱动下载进度条?显示模块不知道也不应该知道下载模块所知道的文件下载进度(面向对象设计的封装性,模块间要解耦,模块内要内聚),文件下载进度是只有下载模块才知道的事情,解决方案很简单给下载模块传递一个函数指针作为回调函数驱动显示模块的显示进度。
下面是模拟实现这个文件下载模块的代码,仅供参考
#include
#include
#include
typedef void(*on_process_callback)(std::string data);
//处理完成的回调
void on_process_result(std::string data)
{
//根据返回消息进行处理
std::cout << data.c_str() << std::endl;
};
class TaskProcessing
{
public:
TaskProcessing(on_process_callback callback) : _callback(callback)
{};
void set_callback(on_process_callback callback)
{
_callback = callback;
};
void do_task()
{
//当文件传输完成
if (_callback)
{
srand((int)time(NULL));
if (rand() & 1)
{
(*_callback)(std::string("ftp succeed"));
}
else
{
(*_callback)(std::string("ftp failed"));
}
}
};
private:
on_process_callback _callback;
};
int main()
{
TaskProcessing* process = new TaskProcessing(on_process_result);
process->do_task();
system("pause");
}
在看这个回调函数的时候,我也联想到了JS和C#中似乎也有类似的身影,这里对比分析一下
例如在下面,有一个add函数,通过外界传入要运算的两个操作符以及一个回调函数的地址,就可以起到在add函数内部去调用
print()
函数的作用
print
作为add()函数的形参,其为函数名,函数名即为函数的地址,此时add函数内部就获取到printf()函数的地址那便可以通过一定的条件去调用这个函数<script>
function add(num1, num2, callback) {
var sum = num1 + num2;
callback(sum);
}
function print(num) {
console.log(num);
}
add(1, 2, print); //3
</script>
如果有学习过C#的同学,说到【回调函数】的话,应该可以很快联想到委托,真的是异曲同工之妙
不清楚的同学可以先看看这个视频,讲得还可以
C#基础教程 delegate 帮你理解委托,知道委托的好处, 不懂委托一定要看下!
class Program
{
public delegate void MyDelegate();
static void Main(string[] args)
{
MyDelegate myDelegate = new MyDelegate(new Test().SayHello);
myDelegate();
}
}
class Test
{
public void SayHello()
{
Console.WriteLine("Hello Delegate!");
}
}
举了两个小小的例子,为了让读者了解到除了C语言其实还有其他语言中也有【回调函数】的声音,了解到什么叫做 ⇒ C生万物
好,最后来总结一下本文所学习的内容
以上就是本文要介绍的所有内容,感谢您的阅读