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

1、什么是回调函数

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

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

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

2、为何使用回调函数

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

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

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

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

3、如何使用回调函数

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

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

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

  void function1(int var1,int var2)

  void function2(void *fc(int,int),float a,int b)

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

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

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

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

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

  __weak void SetSingleCoil(uint16_t coilAddress,bool coilValue)

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

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

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

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

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

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

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

   /*定义BMP280操作对象*/

   typedef struct {

     uint8_t chipID;       //芯片ID

     struct Bmp280_Calib_Param caliPara;   //校准参数

     struct Bmp280_Config config;  //配置寄存器

     struct Bmp280_Ctrl_Meas ctrlMeas;     //测量控制寄存器

     void (*Read)(uint8_t regAddress,uint8_t *rData,uint16_t rSize);       //读数据操作指针

     void (*Write)(uint8_t regAddress,uint8_t command);    //谢数据操作指针

     void (*Delay)(volatile uint32_t nTime);       //延时操作指针

   }BMP280Device;

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

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

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

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

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

 void (*HandleSlaveRespond[])(uint8_t *,uint16_t,uint16_t)=

 {HandleReadCoilStatusRespond,

                                                             HandleReadInputStatusRespond,

                                                             HandleReadHoldingRegisterRespond,

                                                             HandleReadInputRegisterRespond};

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

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

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

4、总结

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

欢迎关注:

C语言学习及应用笔记之七:C语言中的回调函数及使用方式的更多相关文章

  1. Java中的回调函数学习

    Java中的回调函数学习 博客分类: J2SE JavaJ#  一般来说分为以下几步: 声明回调函数的统一接口interface A,包含方法callback(); 在调用类caller内将该接口设置 ...

  2. C语言学习及应用笔记之一:C运算符优先级及使用问题

    C语言中的运算符绝对是C语言学习和使用的一个难点,因为在2011版的标准中,C语言的运算符的数量超过40个,甚至比关键字的数量还要多.这些运算符有单目运算符.双目运算符以及三目运算符,又涉及到左结合和 ...

  3. C语言学习及应用笔记之五:C语言typedef关键字及其使用

    在C语言中有一个typedef关键字,其用来定义用户自定义类型.当然,并不是真的创造了一种数据类型,而是给已有的或者符合型的以及复杂的数据类型取一个我们自己更容易理解的别名.总之,可以使用typede ...

  4. C语言程序设计做题笔记之C语言基础知识(下)

    C 语言是一种功能强大.简洁的计算机语言,通过它可以编写程序,指挥计算机完成指定的任务.我们可以利用C语言创建程序(即一组指令),并让计算机依指令行 事.并且C是相当灵活的,用于执行计算机程序能完成的 ...

  5. C语言程序设计做题笔记之C语言基础知识(上)

    C语言是一种功能强大.简洁的计算机语言,通过它可以编写程序,指挥计算机完成指定的任务.我们可以利用C语言创建程序(即一组指令),并让计算机依指令行事.并且C是相当灵活的,用于执行计算机程序能完成的几乎 ...

  6. GO语言学习(十六)Go 语言结构体

    Go 语言结构体 Go 语言中数组可以存储同一类型的数据,但在结构体中我们可以为不同项定义不同的数据类型. 结构体是由一系列具有相同类型或不同类型的数据构成的数据集合. 结构体表示一项记录,比如保存图 ...

  7. GO语言学习(十四)Go 语言数组

    Go 语言数组 Go 语言提供了数组类型的数据结构. 数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整形.字符串或者自定义类型. 相对于去声明number0 ...

  8. GO语言学习(十二)Go 语言函数

    Go 语言函数 函数是基本的代码块,用于执行一个任务. Go 语言最少有个 main() 函数. 你可以通过函数来划分不同功能,逻辑上每个函数执行的是指定的任务. 函数声明告诉了编译器函数的名称,返回 ...

  9. C语言学习书籍推荐《C程序设计语言(第2版•新版)》下载

    克尼汉 (作者), 等 (作者, 译者), 徐宝文 (译者) 下载地址:点我 <C程序设计语言(第2版•新版)>是由C语言的设计者Brian W.Kernighan和Dennis M.Ri ...

随机推荐

  1. Django rest framework 源码分析 (1)----认证

    一.基础 django 2.0官方文档 https://docs.djangoproject.com/en/2.0/ 安装 pip3 install djangorestframework 假如我们想 ...

  2. spring和mybatis的整合配置

    参考自: http://www.cnblogs.com/wangmingshun/p/5674633.html 链接中的文章里一共有三种整合方式,太多了怕记混了. 我这里只保留第二种. spring中 ...

  3. Win10图片打不开文件系统错误2147416359解决方法

    该问题表现为win10打开所有图片都会提示这个‘文件系统错误-2147416359’,打开其他文件没问题.此问题应该是win10自带的图片查看器出了故障. 在网上找到如下方案,但是我的服务列表里没有这 ...

  4. 函数遍历DOM树

    //获取页面中的根节点--根标签   var root=document.documentElement;//html   //函数遍历DOM树   //根据根节点,调用fn的函数,显示的是根节点的名 ...

  5. [模板] 后缀自动机&&后缀树

    后缀自动机 后缀自动机是一种确定性有限状态自动机, 它可以接收字符串\(s\)的所有后缀. 构造, 性质 翻译自毛子俄罗斯神仙的博客, 讲的很好 后缀自动机详解 - DZYO的博客 - CSDN博客 ...

  6. linux 定时下载github最新代码

    场景:网站的代码在github上托管,静态网站部署在服务器上,每次自己修改完本地代码后,提交到github上,需要自己去服务器上执行git pull 拉取最新代码, 为了解决这种操作,自己再服务器上  ...

  7. Nginx-Tomcat搭建负载均衡(转载)

    一.   工具 nginx-1.8.0 apache-tomcat-6.0.33 二.    目标 实现高性能负载均衡的Tomcat集群: 三.    步骤 1.首先下载Nginx,要下载稳定版: 2 ...

  8. GateOne Web SSH 环境搭建

    环境配置安装python及tornadoyum -y install python-pippip install tornado GateOne安装下载源码:git clone https://git ...

  9. 题解-洛谷P1601 A+B Problem(高精)

    https://www.luogu.org/problemnew/show/P1601(题目传送) 显然数据范围超过了long long类型,故不能简单的用两个长整型存起来相加.这里用到大数据的高精度 ...

  10. Vue(小案例_vue+axios仿手机app)_购物车

    一.前言 1.购物车 二.主要内容 1.效果演示如下,当我们选择商品数量改变的时候,也要让购物车里面的数据改变 2.具体实现 (1)小球从上面跳到下面的效果 (2)当点击上面的“加入购物车按钮”让小球 ...