关于回调函数这一块一直以来都是很多人在学习C语言、嵌入式软件这块比较难理解的东西,网上一搜“回调函数”、“回调函数的作用”、“回调函数的实现”等等,很多说的都是云里雾里的,基本都是说其实就是一个函数指针,把函数作为参数传递进去,网上的这些说法其实没毛病,也是事实,但是很多时候就很难理解这样做的目的是什么?网上再有就是说很灵活,但还是不能理解啊,就很抽象,于是今天专门来用微代码讨论下这个神奇的“回调”
在说回调函数之前先来简单的复习下C语言中的函数指针,指针有很多类型,如char *、int *等等,分别是指向字符和整形变量的指针,那其实也很容易理解,也有一个指针指向一个函数,我们就把指向函数的指针,就叫做函数指针。这个地址保存的是函数的地址,我们知道这个函数的入口地址,自然也就能通过这个地址去运行这个函数。
关于网上搜索相关回调函数的示例可能都是像下面这样的,把一个函数作为参数传递进去,然后运行,实现不同的功能,这种效果普通函数也能完成,为什么还要回调呢?这样做不是多此一举吗?
上面的示例确实描述了回调函数的基本用法,但是完全没有示范清楚为什么需要回调函数,简单的来说,回调函数最大的作用的“解耦”,划重点。在嵌入式系统中芯片的功能越来越强大,能做的事情也越来越多,带来的就是代码量的快速膨胀,像Linux发展到现在已经2700万+的代码量了,代码量多的情况下为了更好的维护、更新、多人协作就需要软件分层设计,分层设计的最基本原则就是上层可以去调用下层的API接口,反之截止,即单向调用。一般嵌入式系统中会分成硬件接口层(BSP)、驱动层、中间件、操作系统、应用层、用户交互等等,不同复杂度的嵌入式系统的分层数都是不同的,下面我们就软件的分层思想来简单看下下面的示例代码,示例代码总共三个文件分别是main.c、dev.c、dev.h,把dev.c和dev.h划分为底层,main.c为上层应用,来实现一个错误输出的功能,即模拟dev是接收消息体,当接收到错误消息的时候根据不同的错误类型输出不同的错误信息。
不使用回调函数和函数指针也能实现,下面来看下代码实现:
上面的代码也实现了这个功能,但是带来的问题是很明显的,这两个文件没有层次之分,dev.c调用了main.c中的接口,main.c调用了dec.c中的接口,极度耦合,当需要修改某一层的时候可能两个文件都需要修改,代码的耦合度太高,且没有遵循单向调用的规定。如果把main.c文件删除,dev.c是无法编译的。
这时候有人可能会说,把error_func定义在dev.c中不就好了吗,完美解决耦合问题,确实,但是一般底层的文件都是以库或者其他形式存在,不允许上层修改底层甚至连底层的代码你都看不到,又何谈修改呢?再说如果谁都能去修改底层实现,多人开发一个项目的时候这不是很危险,所以一般底层都是提供固定的API接口给上层调用,但是仍有一些逻辑是需要分别处理,如举例的错误处理,可能底层出现不同的错误上层要做不同的操作,如释放内存、关闭进程、退出程序等等,这些是需要上层去实现的。
有了上面的例子和问题点,再来看下怎么解决这类问题吧,解决的方式就是通过函数指针以及回调函数、注册回调函数的方式实现。
相当清楚了吧,即便是我把main.c文件删除,dev.c都能正常编译,完全不依赖于mian.c的接口,他的逻辑很简单,不管dev.c运行的时候有没有错误先把出现错误需要调用的代码注册进去,发生错误的时候就能直接运行,如果不注册也没问题,也不影响dev.c的编译。
那为什么会被称为回调函数(callback)呢,这也很好理解了,在main.c中定义,调用dev.c接口的时候运行到函数指针再回头调用main.c中的函数。运行完再回到dev.c中继续执行。
源码分享:
main.c
#include "dev.h" /* 回调函数--发生错误的时候运行 */ void ErrorPrintf(void) { printf("<%s> <%s>\t Device message error.\n", __FILE__, __func__); /* do something... */ /* error handling code...*/ } int main(void) { /* 注册回调函数 */ DeviceErrorRegister(ErrorPrintf); /* 调用底层开放接口 */ GetDeviceMsg("false"); return 0; }
dev.h
#ifndef __DEV_H__ #define __DEV_H__ #include <stdio.h> #include <stdlib.h> #include <string.h> /* 错误处理函数指针 */ typedef void (*dev_err)(void); extern void DeviceErrorRegister(dev_err err_func); extern void GetDeviceMsg(char *msg); #endif /* __DEV_H__ */
dev.c
#include "dev.h" dev_err error_func; /* 注册回调函数接口 */ void DeviceErrorRegister(dev_err err_func) { error_func = err_func; } void GetDeviceMsg(char *msg) { printf("<%s>\t <%s>\t enter.\n", __FILE__, __func__); if(strstr(msg, "true")) { printf("<%s>\t <%s>\t get device msg and check pass.\n", __FILE__, __func__); } else if(strstr(msg, "false")) { printf("<%s>\t <%s>\t receive false message.\n", __FILE__, __func__); if(error_func != NULL) error_func(); } printf("<%s>\t <%s>\t exit.\n", __FILE__, __func__); }
上述代码可以在VS Code中直接编译运行。
到这里我相信大家对回调函数以及运行过程都有一个大概的了解了,如果觉得本篇文章多少有点帮助的话,求赞、求关注、求评论、求转发,创作不易!你们的支持是小编创作最大的动力。