指针篇之十一 函数指针

    程序包含指令段和数据段,各自占用存储空间,有相应地址。函数可看作一组代表指令的二进制数集合,因此也能用指针指向和访问。函数指针就是指向可执行代码段中的某函数入口地址的指针,通过它可以把函数当成普通数据那样存储访问甚至移动拷贝。

    函数指针定义为:返回值类型(*指针变量名)(形参列表);如:int (*f)(int x);注意形似的int *f(int x)代表返回值为指针的普通函数。函数指针的特殊在于::对它取*号代表调用指针所指函数,而不是取值,例如:

    typedef void (*pfunc) ( );  //定义一个无参数、无返回类型的函数指针类型

    pfunc pf_rst = (pfunc)0;   // 函数指针指向0地址,假设为CPU启动后首条指令位置

    *pf_rst();                         // 调用函数,重启

    这里没有定义函数,但只要系统在0地址处预先加载重启代码,*pf_rst()就可以"软重启",跳转到上电首条指令。这种指向常量地址的函数指针是特例,因为C函数名本身就代表函数首地址,所以通常把定义好的函数名做为右值赋给函数指针,如:

    int func(int x){…};    // 定义一个函数

    int (*f) (int x);           //声明一个函数指针

    f = func;                   // func函数的首地址赋给指针f

    (*f)(x);                     // 通过函数指针调用函数

    赋值时作为右值的函数名func不需带括号和参数,赋值后指针f指向函数func(x)的代码首地址。不过函数名和函数指针之间不能任意赋值,须满足三个条件:

    a)具有完全相同的函数参数,包括参数数目和参数类型;

    b) 具有相同的函数返回值;

    c) 具有相同的函数调用规范,调用规范可看做函数类型的一部分(可上网查),规范不兼容的函数和函数指针间不能赋值,如:

      __stdcall int callee(int);

      __cdecl int(*p)(int) = callee;     // 错,指针pcallee()的调用规范不同,不能赋值

    换句话,函数指针不能与任何类型进行转换,只能同类型赋值。

1)一个接口,多种实现

    函数指针可为模块间灵活配置转接接口,使软件更"":根据不同条件选择不同函数实现,赋给统一的函数指针,这个指针对外提供唯一接口,这是指针高级灵活的应用。

    很多程序里init接口功能之一就是初始化函数指针,从多个版本中通过为函数指针赋值选择函数实现,在保持接口一致性的同时实现了函数功能动态(运行时)选择。比如(伪码):

    void (*fir)(int16 *pCoef);

    init()

    {

      int cputype = detectCPU();  //假设能实现这样的功能函数

      switch(cputype )

      {

        case ARMV4:  fir = fir_armv4; break;

        case ARMV5:  fir = fir_armv5; break;

        case ARMV6:  fir = fir_armv6; break;

        case ARMV7:  fir = fir_armv7; break;

        default:       fir = fir_c;     break; 

      }

    }

    这里函数指针void (*fir)(int16 *pCoef)可根据不同硬件平台选择不同汇编优化函数,从而提高软件的多平台兼容性。可见:有了函数指针,就具备中间变换的余地,可构造中间适配层,根据外部输入让函数指针在运行时指向适当的函数,上层保持唯一不变的接口,这样整个系统结构更清晰,具有更好的可扩展性

2)先调用后实现,自顶向下的模块化设计

    此外一些大型软件C模块间的接口调用除了用函数指针代替直接函数调用,还使用dlopen/dlsym方式实现渐进式开发,比如:

    libptr = dlopen("lib_functest.so",RTLD_NOW);
    *(void **)&(fp_test1) = dlsym(libptr , "func_test1");
   *(void **)&(fp_test2) = dlsym(libptr , "func_test2");
   ......

   if(fp_test1)     *f_test();

   ......

    初看似乎多此一举,然而当系统规模巨大时,任何将模块切割出来的方法都有重要意义:

    a. 首先用dlopen可选择性加载库,比如手机中试图调用某解码库时用函数指针和动态库就可以很灵活:

    libmpeg4dec_lib = dlopen("lib_hwdec.so",RTLD_NOW);    //先优先尝试打开硬件解码库

    if(libmpeg4dec_lib  ==NULL)                                             //如果不存在

      libmpeg4dec_lib = dlopen("lib_swdec.so",RTLD_NOW);  //转而选择软件解码库。

    b. 其次如果库中不存在某函数,也可进行判断提示,后续逐渐完善,不影响整体程序运行。即:

        if(!(fp_test1=dlsym(libptr, "func_test1")) {

          printf("test1 not implemented!"

        }

     想象下如果直接使用函数调用func_test1(),而其在lib_fuctest库中不存在,那只能卡死在编译错误了。函数指针和动态加载库可以使上层在底层函数不存在的情况下,规划完成系列接口调用,而接口实现可在之后逐步实现。这就把大系统“分解”成小模块,自上向下实现。在软件模块和硬件驱动程序的测试程序里,这种方式经常使用。

总结

    函数指针是一种软编码方式,使函数调用时跳转的目标地址为变量,因此能够在运行时动态选择和改变函数执行分支;与此对应的直接函数调用相当于一种硬编码,编译完成后函数跳转地址就固定为常量。想比函数指针,直接调用函数显得笨拙和僵硬,所以linux驱动等系统软件中大量使用函数指针,在上层封装出统一的访问接口,这体现了函数指针的万能接口功能。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值