一篇学会回调函数

开发 前端
关于回调函数,我的态度是:回调函数可以使我们的代码更高效且更易于维护,降低耦合。明智地使用它们很重要,否则过度使用回调(函数指针)会使代码难以进行排查和。

函数指针

学习回调函数,其实就是函数指针的应用,关于函数指针在之前的文章《​​指针与函数​​》中有详细的讲解,这里不再展开详解,重新贴一下之前文章中函数指针的示例代码:

#include <stdio.h>
void MyFun1(int x);
void MyFun2(int x);
void MyFun3(int x);
typedef void (*FunType)(int); /* ②. 定义一个函数指针类型FunType,与①函数类型一致 */
void CallMyFun(FunType fp, int x);
int main(int argc, char *argv[])
{
CallMyFun(MyFun1, 10); /* ⑤. 通过CallMyFun函数分别调用三个不同的函数 */
CallMyFun(MyFun2, 20);
CallMyFun(MyFun3, 30);
}
void CallMyFun(FunType fp, int x) /* ③. 参数fp的类型是FunType。*/
{
fp(x); /* ④. 通过fp的指针执行传递进来的函数,注意fp所指的函数是有一个参数的。 */
}
void MyFun1(int x) /* ①. 这是个有一个参数的函数,以下两个函数也相同。 */
{
printf("MyFun1:%d\n", x);
}
void MyFun2(int x)
{
printf("MyFun2:%d\n", x);
}
void MyFun3(int x)
{
printf("MyFun3:%d\n", x);
}

运行结果如下:

为什么需要回调函数

这里先说一下软件分层的问题,软件分层的一般原则是:上层可以直接调用下层的函数,下层则不能直接调用上层的函数。这句话说来简单,在现实中,下层常常要反过来调用上层的函数。

比如你在拷贝文件时,在界面层调用一个拷贝文件函数。界面层是上层,拷贝文件函数是下层,上层调用下层,理所当然。但是如果你想在拷贝文件时还要更新进度条,问题就来了。

一方面,只有拷贝文件函数才知道拷贝的进度,但它不能去更新界面的进度条。另外一方面,界面知道如何去更新进度条,但它又不知道拷贝的进度。怎么办?

常见的做法,就是界面设置一个回调函数给拷贝文件函数,拷贝文件函数在适当的时候调用这个回调函数来通知界面更新状态。

上面主要说的一个大型软件分层理念,作为嵌入式开发程序员,特别是单片机的开发中,由于和硬件结合紧密且需要快速响应,软件结构大部分是面向过程开发的,回调函数使用频率并不高。但在软件中使用回调函数,可以让软件更加模块化。

上图形象展示了回调函数的作用,上面说到了软件分层,在嵌入式代码中我们一般将和硬件交互的代码称为硬件层,业务逻辑代码称为应用层代码,对于优秀的的嵌入式代码,一般要求硬件层和应用层代码分开。

一般的回调函数代码结构如下:


typedef void (*ReceiveFarmDataFun)();

static CallbackReceive_t HandlerCompleted;

/*用来注册回调函数的功能函数*/
void CallbackRegister (CallbackFunc_t callback_func) {
HandlerCompleted = callback_func;
}

串口应用

在嵌入式应用中,串口通信是很经典且常用的外设,举一个简单的栗子,接收的串口数据帧头是@,帧尾是*。中间数据不可能出现@和*。那么一般情况下代码如下编写。


/*串口中断函数*/
uint8_t receive_flg = 0;
uint8_t receive_data[100];
uint8_t USART1_data = 0;
uint8_t USART1_data_len = 0;
uint8_t USART1_receive_sta = 0;
void USART1_IRQHandler(void)
{
uint8_t data_tmp;
if(USART_GetFlagStatus(USART1, USART_FLAG_RXNE))
{
data_tmp = USART_ReceiveData(USART1);
if((data_tmp == '*')&&(USART1_receive_sta == 1))
{
receive_flg = 1;
USART1_receive_sta = 0;
receive_data[USART1_data_len++] = data_tmp;
}
if(receive_flg == 0){
if(data_tmp == '@')
{
USART1_receive_sta = 1;
USART1_data_len = 0;
}
if(USART1_receive_sta)
receive_data[USART1_data_len++] = data_tmp;
if(USART1_data_len > (100-1))
{
receive_flg = 0;
USART1_receive_sta = 0;
}
}
USART_ClearFlag(USART1, USART_FLAG_RXNE);
}
}
/*应用层代码,简单化->在main函数*/
void main()
{
/*省略其他代码*/
while(1)
{
if(receive_flg == 1)//通过检查receive_data判断是否接收到函数
{
/*通过receive_data数组处理数据*/
receive_flg = 0;
}
}
}

这样实现功能是没有问题的,在我接触到很多的项目中的确是类似的架构,但是它的移植性较差。

还有一种情况,那就是如果你接到需求把硬件层封装给客户使用,不让客户看到源码,封装成库,起到"保护通讯协议"的目的,那么你要告诉客户,需要判断receive_flg变量,然后读取receive_data数组的内容???

不得不说,你这样干是可以的,但是大部分公司不会这样干的。这时候可以使用回调函数来解决这个问题。

/*开放给客户的头文件*/
/* Includes ------------------------------------------------------------------*/
#include <stdio.h>
typedef void (*ReceiveFarmDataFun)(uint8_t *buff,uint32_t bufferlen);
extern void CallbackRegister (CallbackFunc_t callback_func)

/*封装的函数*/
static CallbackReceive_t HandlerCompleted;
void CallbackRegister (CallbackFunc_t callback_func) {
HandlerCompleted = callback_func;
}
uint8_t receive_flg = 0;
uint8_t receive_data[100];
uint8_t USART1_data = 0;
uint8_t USART1_data_len = 0;
uint8_t USART1_receive_sta = 0;
void USART1_IRQHandler(void)
{
uint8_t data_tmp;
if(USART_GetFlagStatus(USART1, USART_FLAG_RXNE))
{
data_tmp = USART_ReceiveData(USART1);
if((data_tmp == '*')&&(USART1_receive_sta == 1))
{
receive_flg = 1;
USART1_receive_sta = 0;
HandlerCompleted(receive_data,USART1_data_len);
}
if(receive_flg == 0){
if(data_tmp == '@')
{
USART1_receive_sta = 1;
USART1_data_len = 0;
}
if(USART1_receive_sta)
receive_data[USART1_data_len++] = data_tmp;
if(USART1_data_len > (100-1))
{
receive_flg = 0;
USART1_receive_sta = 0;
}
}
USART_ClearFlag(USART1, USART_FLAG_RXNE);
}
}

那么客户拿到的有用信息如下:

typedef void (*ReceiveFarmDataFun)(uint8_t *buff,uint32_t bufferlen);
extern void CallbackRegister (CallbackFunc_t callback_func)

客户可以写如下代码:

void uartdatadeal(uint8_t *buff,uint32_t bufferlen)
{
/*buff指针存储了串口数据,bufferlen存储数据长度*/
/*客户的应用层代码*/
}
void main()
{
/*省略其他代码*/
CallbackRegister (uartdatadeal);
while(1)
{
}
}

这样的话,就可以解决上述问题,客户只要注册一下串口接收的函数,当接收到有效数据后,就可以跳转到用户的代码,而你可以将自己的硬件层封装起来。

看到这里可能有嵌入式大佬意识到某些问题了,这样写代码,数据处理的函数就等于在中断里了,这是不合理的啊。

是的,是有这个问题,所以给客户的库文件必须说明这一点,让客户自行选择,客户不想在中断中执行,可以再按照我们一开始的逻辑写啊,如下:

void uartdatadeal(uint8_t *buff,uint32_t bufferlen)
{
/*buff指针存储了串口数据,bufferlen存储数据长度*/
receive_flg = 1;
}
void main()
{
/*省略其他代码*/
CallbackRegister (uartdatadeal);
while(1)
{
if(receive_flg == 1)
{
/*处理数据*/
receive_flg = 0;
}
}
}

事实上,芯片/模块厂家写SDK经常这样做,一些大型的开源库也会这样用,典型的如lwip库。

后记

读到这里的同学可能觉得这完全是“脱裤子放屁”啊,这属于“炫技”啊,没什么用啊。诚然在很多应用中,特别是一些单片机项目中,代码量不大,使用类似receive_flg全局变量控制,代码结构也清晰啊。

并且项目不需封装库给客户,一个单片机软件开发工程师可以吃透整个项目的代码,根本不需要这样的“骚操作”。

关于回调函数,我的态度是:回调函数可以使我们的代码更高效且更易于维护,降低耦合。明智地使用它们很重要,否则过度使用回调(函数指针)会使代码难以进行排查和调试。

责任编辑:武晓燕 来源: 知晓编程
相关推荐

2021-04-07 13:28:21

函数程序员异步

2021-12-01 11:33:21

函数Min

2022-03-02 11:37:57

参数性能调优

2022-01-02 08:43:46

Python

2022-02-07 11:01:23

ZooKeeper

2021-05-27 07:12:19

Python函数装饰器

2021-07-02 09:45:29

MySQL InnoDB数据

2021-07-05 22:11:38

MySQL体系架构

2023-01-03 08:31:54

Spring读取器配置

2021-05-11 08:54:59

建造者模式设计

2021-07-06 08:59:18

抽象工厂模式

2022-08-23 08:00:59

磁盘性能网络

2023-11-28 08:29:31

Rust内存布局

2022-08-26 09:29:01

Kubernetes策略Master

2021-07-16 22:43:10

Go并发Golang

2021-10-27 09:59:35

存储

2021-07-02 08:51:29

源码参数Thread

2023-03-13 21:38:08

TCP数据IP地址

2021-10-29 07:35:32

Linux 命令系统

2022-03-11 10:21:30

IO系统日志
点赞
收藏

51CTO技术栈公众号