表驱动


 

函数指针(1)

函数指针含义
函数
 在C语言中,函数作为功能完成的基本单位,它的定义如下:
 FunctionType FuncName(ArgumentList…)
{
 FunctionBody…

函数指针
 函数作为功能的执行实体,它也占用一段存储空间,我们可以通过这段空
间的起始地址来访问函数;如函数在编译形成目标代码时被分配一个入口地址(第
一条指令的地址),这个地址是函数执行的入口地址。(如0x82345678)
 有时我们需要访问这个地址,直接访问地址的方式(0x82345678)不是很方便。虽
然通过函数的名字(如上面提及的FuncName)也可以访问,但我们还有另外一种方法
--可以用一类指针来指向这个地址,而通过指针来访问,这类指针被称为函数指
针。


函数指针(2)
函数指针定义 typedef FunctionType (*FuncName)(ArgumentList…);
     其中 FunctionType是函数指针的型--是指向什么类型函数的函数指针; FuncName是类型名--函数指针的类型; ArgumentList是函数的参数列表。
函数指针示例

//定义一个CALLBACK_t的函数指针类型
typedef int(*CALLBACK_t) (int type, int *para);
// 定义了一个CALLBACK_t类型的函数指针
CALLBACK_t  func1;
//给 函数指针赋初值 --指向一个函数。
func1 = callback_func1;
/* 其中callback_func1下面函数的函数名称*/
int callback_func1(int type, int * para)
{  ……
}
函数指针(3)
  // 函数的执行--函数调用。
  func1 ( type, para );
     
      /*  可以看出:
       func1 ( type, para )与
       callback_func1( type, para )的执行结果是等效的。
      */

消除误解
函数指针与指针函数
函数指针是指向函数的指针,如下:
   int (* pfunp)(int ia,int ib);
       指针函数是返回值为指针的函数,如下:
       int *pfunp(int ia,int ib);

函数指针是一级指针
函数名可以看作一个函数指针,
函数指针的应用举例(1)
   函数指针的应用很多。下面以表驱动和回调函数为例,说明函数指针的应用。
表驱动
表(驱动)的含义,就是将各种分支处理以表的方式实现。
问题:
 有一组处理函数:functionA, functionB,… functionZ,它们的函数形式如下
                    int functionA(int event);
           int functionB(int event);
                    …….
           int functionZ(int event);
 他们分别是不同状态(A,B…,Z)的处理。编写这段处理代码,我们该如何做?
     下面代码可行吗?
             switch (status) {
            case A:    functionA(event)
              break;
            case B:    functionB(event)
              break;
   ……
            case Z:    functionZ(event)
              break;
函数指针的应用举例(2)
     可行!但是不好!原因是生成目标代码大,而且可维护弱一些。
解决方法:
先定义一个函数指针:
   typdef  int (*pFunc)(int event);
再定义一个枚举类型:
  typedef enum { 
                    A =0,
                    B,
                     …
                    Z
                 } Status_t;
    构造表:
     pFunc  functionlist[Z+1] ={
      functionA,
      functionB,
                                                             …
                                                       functionZ
        };
函数指针的应用举例(3)
 处理:
   Status_t   status;
           ……
            status被赋值
                   ……
            functionlist[status](event);
小节
这种表处理functionlist[status](event),大大简化了代码处理;
目标代码得到了缩减;
代码的扩展性得到了改善;如再增加一个分支处理的话,可以很容易扩展;


函数指针的应用举例(4)
回调函数
    A、B两个模块(任务),各自完成独立的功能,同时他们也需要协作。当A模块(任务)完成一定功能后,需要等待B模块完成相应功能后,A模块(任务)才能继续进行。但是A模块(任务)不知道B模块(任务)什么时候能够完成,因此B模块(任务)需要通知A模块(任务)的手段;通知的手段包括:
    - B模块(任务)直接发送消息给A模块(任务)
    - B模块(任务)直接调用A模块(任务)的函数
    。。。。。。
     但这几种方法都必须要求A模块(任务)向B模块(任务)暴露信息,如消息名、函数名等,这不利于模块的封装性。
       还有另外一种方法,利用回调函数的方式。
     先将A模块(任务)的方法(函数),登陆到B模块(任务),由B模块(任务)来调用这个方法(函数),而这个方法(函数)中完成通知这个工作。这种方式保证了  A模块(任务)和B模块(任务)的封装性和独立性。而登陆的方法(函数)--回调函数就是一个函数指针。 
 在后面内容,我们会继续讨论回调函数。


回调函数(1)
回调函数的含义
概括的讲,回调函数就是登陆到某个模块(任务),而被这个模块(任务)调用的函数。
更加形象的描述,回调函数还真有点像您随身带的BP机,告诉了别人号码,在他(这个人)有事情时Call您。
回调函数原理
回调函数(2)
举个例子
int max(int x,int y){  return x>y?x:y; }
int min(int x,int y){  return x<y?x:y; }
int add(int x,int y){  return x+y; }    
int process(int x,int y, int (*f)()) /* 通用两数的处理函数 */
{ return (*f)(x,y); }
main()

int a,b;   
printf("Enter two num to a,b:");scanf("%d%d",&a,&b);
printf("max=%d/n",process(a,b,max));  /* 调用通用处理函数 */
printf("min=%d/n",process(a,b,min));
printf("add=%d/n",process(a,b,add));
}
回调函数(3)
程序说明
函数process处理两个整数,并返回一个整型值。同时又要求process具有通用处理能力(处理求大数、小数、和),所以可以考虑在调用process时将相应的处理方法(“处理函数”)传递给process
process应该有一个函数指针作为形式参数,以接受函数的地址。这样process函数的函数原型应该是: int process(int x,int y,int (*f)());
函数指针变量的定义在通用函数process的形参定义部分实现;函数指针变量的赋值在通用函数的调用时实现;用函数指针调用函数在通用函数内部实现
main函数调用通用函数process处理计算两数中大数的过程如下:
回调函数(4)
将函数名max(实际是函数max的地址)连同要处理的两个整数a,b一起作为process函数的实参,调用process函数
process函数接受来自主调函数main传递过来的参数,包括两个整数和函数max的地址,函数指针变量f获得了函数max的地址
 在process函数的适当位置调用函数指针变量f指向的函数,即调用max函数。本例直接调用max并将值返回。这样调用点就获得了两个数当中求大数的结果,由main函数的printf函数输出结果
同样,main函数调用通用函数process处理计算两个数当中的小数以及两数求和的过程基本一样。
process函数是一个“通用”整数处理函数,它使用函数指针作为其中的一个参数,以实现同一个函数中调用不同的处理函数。
回调函数(5)
从这个例子我们可以看出使用回调函数有以下好处:
良好的编程结构、编程接口
模块设计的良好的封装性,简化模块的接口;
 

 

回调函数(6)
使用回调函数的注意事项
避免死锁
要充分考虑服务器端的性能
死锁问题举例
提到死锁,就不可能不提到多任务。下面是两类任务,利用回调函数协调工作的例子,在使用回调函数时,是如何出现死锁的?又是如何解决的?
两类任务的主角姑且叫做Client, Server,其中Client可以有多个,图中只描述一个。其中Client向Server中登陆回调函数,由Server 来调用这些函数。 Server端由于资源为多个Client所共享,会有很多锁;而Client由于需要可能也会有锁。
回调函数(7)
死锁问题一:在回调函数执行期间,直接再次调用Server; 可能Server端在调用回调函数时,已经LockX(在本次调用之后UnLock);但在执行回调函数再次调用了Server端,再次进行LockX--发生了死锁。
回调函数(8)
解决方法:在回调函数中不要再调用Server,而应该在任务切换后,再调用就没有问题了。如图 
回调函数(9)
死锁问题二:如果回调函数执行期间发生了任务切换,切换到Client执行调用Server端函数的场景(Scene) ,并且回调函数中有锁A,而Server端有锁B(其中执行回调函数的调用打开锁B,接口函数也需要打开锁B);很明显这样就出现了相互等待的情况,而出现死锁,如下图。
回调函数(10)
解决方法:
   上面出现的死锁,问题实际上是出在客户端,如果客户端在回调函数中,和在调用服务器端接口的时候使用不同的信号量就可以避免死锁了
注意:
   使用回调函数还要注意性能问题,在回调函数中不能有很浪费时间的处理,道理和上面一样,要知道回调函数本身被调用是占用服务器端的线程的,服务器端还要处理自己要做的事情,并且不是只为一个客户端来服务,如果这个服务器的资源长时间被这个回调函数占用的话,对整个系统来说都是不好的。
   和上面问题一样,在客户端来个线程切换就可以解决这个问题。


 表驱动法介绍

函数指针的使用其实是很灵活的,它有很多技巧性很高的应用,在表驱动方法中的应用很典型,这里着重介绍一下。
1 什么是表驱动方法
表是几乎所有数据结构课本都要讨论的非常有用的数据结构。表驱动方法出于特定的目的来使用表,下面将对此进行讨论。
程序员们经常谈到"表驱动"方法,但是课本中却从未提到过什么是"表驱动"方法。表驱动方法是一种使你可以在表中查找信息,而不必用很多的逻辑语句(if或Case)来把它们找出来的方法。事实上,任何信息都可以通过表来挑选。在简单的情况下,逻辑语句往往更简单而且更直接。但随着逻辑链的复杂,表就变得越来越富有吸引力了,通过下面的这个例子大家就能知道什么是所谓的表驱动方法了。
假设你需要一个可以返回每个月中天数的函数(为简单起见不考虑闰年),一个比较笨的方法是一个大的if语句:
int iGetMonthDays(int iMonth)
{
 int iDays;
 if(1 == iMonth) {iDays = 31;}
 else if(2 == iMonth) {iDays = 28;}
 else if(3 == iMonth) {iDays = 31;}
 else if(4 == iMonth) {iDays = 30;}
 else if(5 == iMonth) {iDays = 31;}
 else if(6 == iMonth) {iDays = 30;}
 else if(7 == iMonth) {iDays = 31;}
 else if(8 == iMonth) {iDays = 31;}
 else if(9 == iMonth) {iDays = 30;}
 else if(10 == iMonth) {iDays = 31;}
 else if(11 == iMonth) {iDays = 30;}
 else if(12 == iMonth) {iDays = 31;}
 return iDays;
}

可以看出本来应该很简单的一件事情,代码却是这么冗余,解决这个的办法就可以用表驱动方法。
static int aiMonthDays[12] = {31,28,31,30,31,30,31,31,30,31,30,31};
/* 我们可以先定义一个静态数组,这个数组用来保存一年十二个月的天数 */
int iGetMonthDays(int iMonth)
{
 return aiMonthDays[(iMonth - 1)];
}

接下来不用多说了,大家都能看出用这种表驱动的方法代替这种情逻辑行不强,但分支很多的代码是多么令人"赏心悦目"的了。

函数:

函数指针在表驱动方法中的应用
在使用表驱动方法时需要说明的一个问题是,你将在表中存储些什么。在某些情况下,表查寻的结果是数据。如果是这种情况,你可以把数据存储在表中。在其它情况下,表查寻的结果是动作。在这种情况下,你可以把描述这一动作的代码存储在表中。在某些语言中,也可以把实现这一动作的子程序的调用存储在表中,也就是将函数的指针保存在表中,当查找到这项时,让程序用这个函数指针来调用相应的程序代码,这个就是函数指针在表驱动方法中的应用。
其实说到这已经说了很多表驱动方法的相关问题了,现在要把函数指针也应用进去,很多人应该已经想到会是个什么样子了,其实也很简单,通过下面这两段伪代码的例子就可以充分体现函数指针在表驱动方法中应用会使代码更加精致。
 我们在写一段程序的过程中会经常遇到这样的问题,我们在写一个Task的主函数中有时会要等待不同的Event通知,并且处理不同的分支,首先有如下的Event Bit的宏定义和相应的处理函数的声明。
#define TASK_EVENT_BIT00  (1 << 0)
#define TASK_EVENT_BIT01  (1 << 1)
#define TASK_EVENT_BIT02  (1 << 2)
#define TASK_EVENT_BIT03  (1 << 3)
#define TASK_EVENT_BIT04  (1 << 4)
#define TASK_EVENT_BIT05  (1 << 5)
#define TASK_EVENT_BIT06  (1 << 6)
#define TASK_EVENT_BIT07  (1 << 7)
#define TASK_EVENT_BIT08  (1 << 8)
#define TASK_EVENT_BIT09  (1 << 9)

void vDoWithEvent00();
void vDoWithEvent01();
void vDoWithEvent02();
void vDoWithEvent03();
void vDoWithEvent04();
void vDoWithEvent05();
void vDoWithEvent06();
void vDoWithEvent07();
void vDoWithEvent08();
void vDoWithEvent09();

我们一般首先想到的写法是
unsigned long ulEventBit;
for(;;)
{
 xos_waitFlag(&ulEventBit);
 if(ulEventBit & TASK_EVENT_BIT00)
 {
  vDoWithEvent00();
 }
 if(ulEventBit & TASK_EVENT_BIT01)
 {
  vDoWithEvent01();
 }
 if(ulEventBit & TASK_EVENT_BIT02)
 {
  vDoWithEvent02();
 }
 if(ulEventBit & TASK_EVENT_BIT03)
 {
  vDoWithEvent03();
 }
 if(ulEventBit & TASK_EVENT_BIT04)
 {
  vDoWithEvent04();
 }
 if(ulEventBit & TASK_EVENT_BIT05)
 {
  vDoWithEvent05();
 }
 if(ulEventBit & TASK_EVENT_BIT06)
 {
  vDoWithEvent06();
 }
 if(ulEventBit & TASK_EVENT_BIT07)
 {
  vDoWithEvent07();
 }
 if(ulEventBit & TASK_EVENT_BIT08)
 {
  vDoWithEvent08();
 }
 if(ulEventBit & TASK_EVENT_BIT09)
 {
  vDoWithEvent09();
 }
}

可以看出这样写是不是显得程序太长了呢。
下面我们再看看同样的一段代码用函数指针和表驱动方法结合的方法写出会是什么样子。
typedef struct {
 unsigned long   ulEventBit;     
 void     (*Func)(void);    
} EventDoWithTable_t;
/* 定义EventBit 与相应处理函数关系的结构体 */

static const EventDoWithTable_t astDoWithTable[] = {
 { TASK_EVENT_BIT00    , vDoWithEvent00},
 { TASK_EVENT_BIT01    , vDoWithEvent01},
 { TASK_EVENT_BIT02    , vDoWithEvent02},
 { TASK_EVENT_BIT03    , vDoWithEvent03},
 { TASK_EVENT_BIT04    , vDoWithEvent04},
 { TASK_EVENT_BIT05    , vDoWithEvent05},
 { TASK_EVENT_BIT06    , vDoWithEvent06},
 { TASK_EVENT_BIT07    , vDoWithEvent07},
 { TASK_EVENT_BIT08    , vDoWithEvent08},
 { TASK_EVENT_BIT09    , vDoWithEvent09}
};
/* 建立EventBit与相应处理函数的关系表 */

ulong ulEventBit;
int  i;
for(;;)
{
 xos_waitFlag(&ulEventBit);
 for(i = 0 ; i < sizeof(astDoWithTable)/sizeof(astDoWithTable[0]); i ++)
 {
  if ( ( ulEventBit & astDoWithTable[i].ulEventBit ) &&
    ( astDoWithTable[i].Func != NULL ) )
  {
   (*astDoWithTable[i].Func)();
   /* 通过函数指针来调用相应的处理函数 */
  }
 }
}

可以看出这种代码的风格使代码变得精致得多了,并且使程序的灵活性大大加强了,如果我们还要再加入EventBit,只修改表中的内容就可以了。
总结
通过上面介绍的,相信大家已经对函数指针的使用方法有所了解了,但是需要提醒大家,凡事都要具体情况具体分析,使用函数指针的时候一定要多加小心,因为函数指针有它的一个致命的缺点。
函数指针的致命缺点是:无法对参数 (parameter) 和返回值 (return value) 的类型进行检查,因为函数已经退化成指针,指针是不带有这些类型信息的。少了类型检查,当参数或者反回值不一致时,会造成严重的错误。有些编译器并不会帮我们找出函数指针这样的致命错误。所以,许多新的编程语言都不支持函数指针了,而改用其他方式。
从上面的例3中我们可以看到
int max(int x,int y){  return x>y?x:y; }
int min(int x,int y){  return x<y?x:y; }
int add(int x,int y){  return x+y; }
    
这三个函数都有两个参数,而在后面却把处理函数定义成

int process(int x,int y, int (*f)())   
{
 return (*f)(x,y);
}
其中第三个参数是一个函数的指针,从表面上看它是个没有参数,并且返回int型的函数的指针,但是在后面却用process(a,b,max)的方式进行调用,max带有两个参数,这段程序在C语言中就可以顺利的编译通过(但是在C++中却编译不通过),可以看出如果编译器没有检查出错误,而我们又不小心写错的话,后果是很严重的,比如return (*f)(x,y);不小心写成return (*f)(x);在C语言中可以正常的被编译通过,但是运行结果一定不是我们想要的。
因此在C语言中使用函数指针的时候,一定要小心"类型陷阱",小心地使用函数指针,只有这样我们才可以从函数指针中获益。

 

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值