C语言学习及应用笔记之七:C语言中的回调函数及使用方式

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

1、什么是回调函数

  既然谈到了回调函数,首先我们就要搞清楚什么是回调函数。在讨论回调函数之前,我们需要说明另一个概念,那就是函数指针。什么是函数指针呢?说的浅显一点,函数指针就是指向函数的指针,说白了也是一种指针,只是它指向的不是整型,字符型等数据量,而是指向函数。在C中,每个函数在编译后都是存储在内存中,并且每个函数都有一个入口地址,根据这个地址,我们便可以访问并使用这个函数。函数指针就是指向这个入口地址,从而调用这个函数。

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

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

2、为何使用回调函数

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

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

  其次,可以使应用更加灵活,这是显而易见的。比如我们设计一个通讯协议栈,这个协议栈在什么平台使用并不局限,我们使用回调的方式具体实现平台相关部分,而协议栈的内核这可以使用于多种平台。

  再者,可以把调用者与被调用者分开,这样调用者不关心谁是被调用者,也不关心他的具体实现。使得软件的设计更加独立,方便与协作或者移植。其实细说起来还有很多,在此仅列举上述几点。

3、如何使用回调函数

  我们已经简单的介绍了什么事回调函数以及为什么要使用它,接下来我们说说怎么使用它。对于使用方式千差万别,而且每个使用者都有相应的心得,在这里我们之宗解一下我们平时常用的几种方式。

3.1、以函数参数的形式使用

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

  void function1int var1int var2

  void function2void *fcintint),float aint b

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

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

3.2、以弱化定义的方式使用

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

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

  __weak void SetSingleCoil(uint16_t coilAddress,bool coilValue)

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

  void SetSingleCoil(uint16_t coilAddress,bool coilValue),具体个功能有使用者更具需要设定。如上述这个函数就是我们在调用Modbus协议栈时实现的,每次都不一样,根据需求而定。

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

3.3、以函数注册的方式使用

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

  当然我们在开发一些外设的驱动时也可以使用这种方式。如我们开发一个外设驱动,该设备即可使用I2C接口也可使用SPI接口,我们要多次使用该设备,但每次,每个人使用那种接口是不确定的,而我们又想复用这部分驱动,但不是每次都改它,就将其作为一个对象封装起来。

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

 1   /*定义BMP280操作对象*/
 2 
 3   typedef struct {
 4 
 5     uint8_t chipID;       //芯片ID
 6 
 7     struct Bmp280_Calib_Param caliPara;   //校准参数
 8 
 9     struct Bmp280_Config config;  //配置寄存器
10 
11     struct Bmp280_Ctrl_Meas ctrlMeas;     //测量控制寄存器
12 
13     void (*Read)(uint8_t regAddress,uint8_t *rData,uint16_t rSize);       //读数据操作指针
14 
15     void (*Write)(uint8_t regAddress,uint8_t command);    //谢数据操作指针
16 
17     void (*Delay)(volatile uint32_t nTime);       //延时操作指针
18 
19   }BMP280Device;

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

3.4、以函数指针类型的方式使用

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

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

  定义一个枚举,同时定义一个函数指针数组:

1 void (*HandleSlaveRespond[])(uint8_t *,uint16_t,uint16_t)=
2 
3 {HandleReadCoilStatusRespond,
4 
5                                                             HandleReadInputStatusRespond,
6 
7                                                             HandleReadHoldingRegisterRespond,
8 
9                                                             HandleReadInputRegisterRespond};

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

  HandleSlaveRespond[fuctionCode](recievedMessage,startAddress,quantity);

  当然,我们只是讨论一种方法,因为使用switch语句一样可以达到效果,但是其代码量却是相差很远。

4、总结

  此篇我们介绍了回调函数及其使用方式,但我们所掌握的不过冰山之一角。并且具体怎么使用它是一个见仁见智的论题,用好了自然是给程序增色,但若是随意使用反倒游客能会有问题。总而言之,回调函数是一种灵活而有强大的功能,但最终的效果还要看使用者。

欢迎关注:

猜你喜欢

转载自www.cnblogs.com/foxclever/p/10160338.html