vlambda博客
学习文章列表

C语言应用笔记(七):C语言中的回调函数和使用方式


在使用 C 语言实现相对复杂的软件开发时,经常会碰到使用回调函数的问题。但是回调函数的理解和使用却不是一件简单的事,这里根据个人的理解和应用经验做浅显的分析。

(一)什么是回调函数

同样回调函数就是一个通过函数指针调用的函数。如果我们把函数的指针作为参数传递给另一个函数,而接收这个参数的函数在其运行过程中,反过来使用这个指针调用其作指向的函数,我们就把这个被通过函数指针调用的函数称为回调函数。

根据上述的描述我们可以知道,回调函数有别于一般意义上的函数调用方式。它一般不是由该函数的实现直接调用它,而是由已经存在的其他对象间接调用它。而且回调函数的调用是调用方所需要的,但是其具体实现确实非常灵活的,我们可以根据需要来实现它,只要调用的格式相符,我们不需要去考虑调用它的对象的具体内容。


(二)为什么使用回调函数

前面我们简单介绍了回调函数,那我们为什么需要使用回调函数呢?既然用它,当然是有用它的理由。接下来我们简单讨论一下使用回调函数的优势在哪里。

首先,可以使上层的应用更完整,但又不需要考虑底层的实现细节。比如我们设计了一个通讯应用,但在设计时我并不能确定底层接口,或者说不想局限于某一接口。那么我们可以将接口部分的实现留在具体使用中,所以采用回调函数的方式就非常方便。

其次,可以使应用更加灵活,这是显而易见的。比如我们设计一个通讯协议栈,这个协议栈在什么平台使用并不局限,我们使用回调的方式具体实现平台相关部分,而协议栈的内核就可以在多种平台使用。再者,可以把调用者与被调用者分开,这样调用者不关心谁是被调用者,也不关心它的具体实现。这样就使得软件的设计更加独立,方便于协作或移植。


(三)如何使用回调函数

我们已经简单介绍了什么是回调函数和为什么要使用它,接下来说说怎么使用回调函数。对于使用方式千差万别,而且每个使用者都有相应的心得,具体怎么使用是一个见仁见智的论题。


(1)以函数参数的形式使用

在大多数情况下,我们可能都是讲函数指针作为参数传递给调用者来实现回调,比如声明以下函数:

void function1(int var1, int var2);void function2(void *fc(int, int), float a, int b);

这样调用时使用 function2(function1, a, b); 就可以了。当然还有另一个函数与 function1 的声明形式一致,也可以作为参数传递给 function2 函数。

这种方式最容易理解,而且函数名不受限制,只要声明形式一致就可以,我们在外设驱动的调用是会使用这一形式。


(2)以弱化定义的方式使用

所谓弱化函数就是调用者以 _weak 定义一个没有操作或者默认操作的函数,该函数允许定义与其名称和形式完全一样的函数。若使用者重新定义了该函数则会调用新函数,否则使用 _weak 修饰的默认函数。在 STM32的 HAL 库中使用了很多这样的函数,比如各种 msp 函数。

首先需要有一个以 _weak 修饰的函数声明:

__weak void SetSingleCoil(uint16_t coilAddress, bool coilValue);

而在使用时定义一个与其同名且形式一样的函数:

void SetSingleCoil(uint16_t coilAddress, bool coilValue);

具体的功能由使用者根据需要设定。如上述这个函数就是在调用 Modbus 协议栈时实现的,每次都不一样,根据需求而定。

这种方式使用虽然方便,但有一个局限就是必须与原函数声明一致,且只能有一个。

(3)以函数注册的方式使用

有时候我们会对一些对象进行封装,同时将操作函数的函数指针也封装在内,这样我们可以在使用对象时直接调用其操作。这种方式主要应用于对一些复杂的外设对象的操作。如网卡对象等,在 WIZnet 以及 LwIP 等协议栈中都是以这种方式将网卡密切相关的特定操作以函数指针的方式封装在对象中。

当然我们在开发一些外设的驱动时也可以使用这种方式。如我们开发一个

外设驱动,该设备既可以使用 IIC 接口也可以使用 SPI 接口,我们要多次使用该设备,但每次,每个人使用哪种接口是不确定的,而我们又想复用这部分驱动,但不想每次都改它,就将其作为一个对象封装起来。

定义一个结构体类型,包括对象的主要属性和基本接口:

/* 定义BMP280操作对象 */typedef struct{ uint8_t chipID; //芯片ID    struct BMP280_Cali_Param caliParam; //校准参数    struct BMP280_Config config; //配置寄存器    struct BMP280_Ctrl_Meas ctrlMeas; //测量控制寄存器    void (*Read)(uint8_t regAddr, uint8_t *rData, uint16_t rSize); //读数据操作指针    void (*Write)(uint8_t regAddr, uint8_t CMD); //写数据操作指针    void (*Delay)(volatile uint32_t nTime); //延时操作指针} BMP280_Device;

使用时,我们只需要声明某一特定对象,并注册相应的函数就可以使用,调用者并不关心具体接口实现。


(4)以函数指针类型的方式使用

以声明函数指针类型的方式其实是与函数参数非常类似的,也可用于形参声明,而且更简洁。但它最主要的优势在于我们可以使用其处理多个回调函数条件调用的问题。

比如在处理 Modbus 协议时我们在处理不同功能码的消息时,需要采用不同的处理方式,就可以采用这种方式:

定义一个枚举,同时定义一个函数指针数组:
void (*HandleSlaveRespond[])(uint8_t*, uint16_t, uint16_t) = { HandleReadCoilStatusRespond,    HandleReadInputStatusRespond,    HandleHoldingRegisterRespond,    HandleReadInputRegisterRespond};

这样我们通过功能码的枚举来调用不同的回调函数就非常简洁了:

HandleSlaveRespond[functionCode](recievedMessage, startAddress, quantity);
当然,这里只是讨论一种方法,因为使用 switch 语句一样可以达到效果,但是其代码量相差很远。




— OpenSSR —

ID:openssr2018



⏫长按二维码关注


专注分享各类软件安装教程,致力打造资源星球与开源社区共享平台。

  

编辑不易,请多支持!

PS:点击阅读原文可查看历史消息哟