c进阶篇(八)——回调函数用法详解

前言:
  我们都知道变量可以通过指针的方式访问,那么函数其实也可以。本篇文章将详细介绍c语言中回调函数的用法。

1 什么是回调函数

  网上对回调的解释有好几套说法,但是都没有很好的体现出回调和非回调函数的差异特点。比较好的是百度百科的说法,c语言回调函数用函数指针实现,是一种被作为参数传递的函数。为甚普通函数也可以被调用还要用回调函数呢。回调函数可以使得调用者不必关系谁是被调用者,它只需要知道存在一个符合特定函数原型和限制条件的被调函数即可。也就是说回调函数可以“解耦合”。回调函数还能实现接口不变的情况下不同功能切换。

2 定义回调函数

  回调函数的本质是函数指针,即指向函数的指针,我们知道变量是有类型的,而函数是有返回值和形参的。以下示范定义一个名为 CallBack 的回调函数,返回值类型为 byte ,形参1为 byte ,形参2为 byte

1
2
3
4
//定义回调函数CallBack,返回值:byte,参数1:byte型,参数2:byte型。
byte (*CallBack1)(byte, byte) = NULL;
//定义回调函数CallBack,返回值:byte*,参数1:byte*型,参数2:byte*型。
byte* (*CallBack2)(word*, byte*) = NULL;

  定义回调函数需要描述几个要素。

  • 回调函数的名称(因为是指针,所以名称前加上 * 号,建议加上括号);
  • 回调函数的返回值类型;
  • 回调函数的形参列表(只用给出各个参数的类型,不需要变量名,形参列表要加括号);
  • 回调函数的指向(初始时可以初始化为NULL);

3 typedef重定义回调函数类型

  回调函数的本质也是指针变量,和其他变量一样也可以被 typedef 修饰。通常重复率较高的回调函数类型我们会选择将其自定义为一种类型,有时为了规范性也会进行自定义为一种类型。

1
2
3
4
//自定义回调函数类型,类型名为CallBackTyp。
typedef byte* (*CallBackTyp)(word*, byte*);

CallBackTyp FunCb = NULL; //定义了类型为CallBackTyp的回调函数FunCb。

  定义未使用 typedef 修饰时 CallBack 是回调函数名,而使用 typedef 修饰时 CallBackTyp 是类型名,这里 typedef 的使用格式与一般变量的类型重定义略有不同。

4 调用回调函数

  回调函数的本质是函数指针,以下演示通过回调来执行某个函数功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef word (*CallBackTyp)(byte, byte);

word Add(byte byNum1, byte byNum2)
{
return byNum1 + byNum2;
}

int32_t main(void)
{
word (*FunCb1)(byte, byte) = NULL;
CallBackTyp FunCb2 = NULL;
word wSum1 = 0u;
word wSum2 = 0u;

FunCb1 = &Add;
FunCb2 = &Add;

wSum1 = FunCb1(1u, 2u);
wSum2 = FunCb2(1u, 2u);

while(1u);
}

  通过直接定义和类型重定义两种方式定义了相同函数模板的回调 FunCb1FunCb2 ,它们都指向同一个函数 Add ,因此通过回调函数进行函数调用时,实际执行的是 Add 函数。

5 回调解耦合及功能切换

  函数内部调用了外部变量或其他函数,会增加该函数与所调变量和其他函数的关联性,这种关联性用耦合度来表示,耦合度越强则代码越难维护,代码理解起来越困难(体现在与该函数关联的变量或函数出现变动时,该函数也不得不相应进行变动)。好的模块划分设计可以有效降低代码的耦合度,将功能单一纯粹的代码放在一起,不相关的代码不杂糅在一起。回调函数也能起到降低代码耦合度的作用。除了解耦合,回调函数还能实现接口不变的情况下切换多种函数功能呢。以下是示例代码。

  • mod1.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
extern void RegCb(CalcTowNum pCalc);
extern word FunB(void);

word Min(byte byNum1, byte byNum2)
{
return (byNum1 <= byNum2) ? byNum1 : byNum2;
}

word Max(byte byNum1, byte byNum2)
{
return (byNum1 >= byNum2) ? byNum1 : byNum2;
}

int32_t main(void)
{
word wRes1 = 0u;
word wRes2 = 0u;

RegCb(Min); //注册回调函数。
wRes1 = FunB();
RegCb(Max); //注册回调函数。
wRes2 = FunB();

while(1u);
}
  • mod2.h
1
typedef word (*CalcTowNum)(byte, byte); //回调,对两个数进行某种运算。

mod2.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static CalcTowNum s_pCalc = NULL;

void RegCb(CalcTowNum pCalc)
{
s_pCalc = pCalc;
}

word FunB(void)
{
byte byNum1 = 2u;
byte byNum2 = 6u;

return s_pCalc(byNum1, byNum2); //调用回调函数。
}

  因为回调函数是通过函数的地址对功能函数进行调用的,因此不用记住函数名。假设mod1是一个工程师设计的,mod2是另一个工程师设计的,如果mod1工程师修改了自己模块的函数名,此时mod2工程师不需要修改任何代码,因为其使用的是回调函数,主要mod1工程师使用 RegCb 函数将处理函数的地址传过来即可,不需要知道函数名,这就是一种解耦合。另外,mod1工程师通过多次调用回调注册函数 RegCb ,使得回调函数指向了不同的处理函数,从而实现了mod2中接口不做任何修改的情况下切换了不同功能。