学过C语言的同僚或其他读者,大都应该对其指针的概念感到比较敬畏
有人学完C语言后,指针使用的炉火纯青,当然也有同学此后的程序再没用过指针来做
这并不是说没使用指针的程序不是好程序,不过指针确实是使得C语言之所以是C语言的重要原因之一
本文并不直接再叙述一遍指针的概念,而是向各位介绍C语言指针的一种妙用——钩子函数
对于追求执行效率的中大型程序而言,一段程序即使能少执行一条简单的代码,也可能得到一定的性能提升
这种软件程序中,经常存在需要按数据类型、业务类型定位处理的函数,这个定位速度如果能提高
整个程序的执行速度也会变快(这里是理论上的,并不是想说肉眼可见的变快)
钩子函数本质其实就是灵活的使用函数指针,我们先看看不用钩子函数时一段程序一般怎么写:
(需求:输入两个整数和运算法则编号,输出该法则下两个数的计算结果)
#include <stdio.h>
#define OPER_TYPE_ADD 0 /* 加法 */
#define OPER_TYPE_MINUS 1 /* 减法 */
#define OPER_TYPE_MULTI 2 /* 乘法 */
int add(int a, int b) { return a + b; }
int minus(int a, int b) { return a - b; }
int multi(int a, int b) { return a * b; }
/***************** Main程序 ********************/
int main(void)
{
int x, y, oper;
scanf("%d %d %d", &x, &y, &oper);
switch(oper)
{
case OPER_TYPE_ADD:
printf("result = %d\n", add(x, y));
break;
case OPER_TYPE_MINUS:
printf("result = %d\n", minus(x, y));
break;
case OPER_TYPE_MULTI:
printf("result = %d\n", multi(x, y));
break;
defult:
printf("Null oper\n");
}
return 0;
}
嘛,这儿我用的switch,当然if-else也没问题,不过这两种流程设计的话在这种业务类型很少的时候还OK
对于实际生产中的可能有几十上百种业务或者数据类型的时候,再这样做,程序的执行顺序就是:
(1)switch结构:从头开始挨个比对oper,直到匹配到oper再开始执行代码
(2)if-else结构:各个if-else分支的逻辑表达式挨个计算挨个比较,直到逻辑成立再执行程序
假设有100种计算法则,上述实现最糟糕的情况是每次都在第100次比对才执行,当计算请求很大量很频繁时
这段程序的执行效率就显得不是那么高了
下面我们看钩子函数怎么处理这个需求:
#include <stdio.h>
int add(int a, int b) { return a + b; }
int minus(int a, int b) { return a - b; }
int multi(int a, int b) { return a * b; }
/* 注册消息 */
enum{
OPER_TYPE_ADD = 0, /* 加法 */
OPER_TYPE_MINUS, /* 减法 */
OPER_TYPE_MULTI, /* 乘法 */
OPER_TYPE_DEV, /* 除法 */
OPER_TYPE_MAX
};
typedef struct handle_cb{
int type;
int (*handle)(int a, int b);
}HANDLE_CB;
/*
注册钩子,这里显式地把消息类型和钩子函数对应关系体现出来了,
handle_cb里面的type可以不要的,注意维护好注册消息表中
每个枚举值先后顺序和下面钩子函数表中的先后顺序一致,能对应上
就行了
*/
HANDLE_CB g_handle [] = {
{ OPER_TYPE_ADD, add },
{ OPER_TYPE_MINUS, minus },
{ OPER_TYPE_MULTI, multi },
{ OPER_TYPE_DEV, NULL}
};
/***************** Main程序 ********************/
int main(void)
{
int i;
int type;
int x, y, oper;
int (*func)(int x, int y);
scanf("%d %d %d", &x, &y, &oper);
/*
这就是挂钩子的过程,如果回调消息类型很多,
可以用type来定位g_handle中注册的钩子函数,
钩子函数好处在于是提供一个接口,公共平台部分代码
不用关系接口内实现,要做新功能的时候直接注册
消息类型和回调函数即可,不用大动公共部分
*/
func = g_handle[oper].handle;
printf("result = %d\n", func(x, y));
/*
同理,一则消息需要多种处理时可以用for循环
*/
func = NULL;
printf("\n");
for (i = 0; i < OPER_TYPE_MAX; i++)
{
func = g_handle[i].handle;
if (func != NULL)
{
printf("result = %d\n", func(x, y));
}
}
return 0;
}
对代码敏感的话你已经看出区别来了,钩子函数在确定业务类型后就直接能确定处理程序
无论需求中的业务和数据类型有多少种,只要类型能确定,就能按数组角标找到对应的钩子
在大量和频繁的计算请求到来时,每个处理都不需要和其他类型比对,只执行一次就能正确处理该类型
从代码维护和迭代的角度看,这种代码设计方式也是很合理的
在新增业务类型时,直接在全局注册表中新增enum和钩子函数指针即可
如果一个类型暂时没有处理但该类型必须有,我们把钩子置为NULL即可
一则是代码量不会像switch和if-else那样膨胀式增长,二则代码简练,可读性好,执行效率高
以下这种把函数指针定义为类型来处理的方式也是钩子函数的一种应用:
#include <stdio.h>
/* 用typedef定义以后,可以当变量名一样自定义其他函数名字 */
typedef int(*FUNC)(int x, int y);
/* 两个钩子函数 */
int add(int a, int b) { return a + b; }
int minus(int a, int b) { return a - b; }
int main(void)
{
FUNC p; /* p可以理解为钩子,挂在哪个函数上就实现哪种流程 */
/* 以下两个流程就是同一接口调用两种实现 */
p = add;
printf("%d + %d = %d\n", 100, 211, (*p)(100, 211));
p = minus;
printf("%d - %d = %d\n", 100, 67, (*p)(100, 67));
return 0;
}
如果你对UNIX/Linux系统的源码实现有一定了解,就会见到这种钩子函数的实现
函数指针带来的灵活性赋予了C语言很大的生机
在C语言实现各个经典的设计模式的时候基本都会用到钩子函数的技巧