函数指针
函数指针是指针的一种,它指向函数的首地址(函数的函数名即为函数的首地址),可以通过函数指针来调用函数。
A) char* (fun1)(char p1,char* p2);
B) char** fun1(char* p1,char* p2);
C) char* fun1(char* p1,char* p2);
注意函数申明和函数指针的区别,B、C都是函数声明,函数名字为fun1,返回类型分别是二级指针和指针。A才是建立一个指针变量,指向一个有两个char*参数,返回值是char*的函数。
这里(*fun1)也可以指向其他具有同类型参数的函数。
函数指针使用例子
#include <stdio.h>
#include <string.h>
char * fun(char * p1,char * p2)
{
int i = 0;
i = strcmp(p1,p2);
if (0 == i)
{
return p1;
}
else
{
return p2;
}
}
int main()
{
char * (*pf)(char * p1,char * p2);
pf = &fun;//注 pf = fun;
(*pf) ("aa","bb");//注 pf("aa","bb");
return 0;
}
注在Visual C++6.0里,给函数指针赋值时,可以用&fun或直接用函数名fun。这是因为函数名被编译之后其实就是一个地址,所以这里两种用法没有本质的差别。
typedef定义某一函数的指针类型
像自定义数据类型一样,我们也可以先定义一个函数指针类型,然后再用这个类型来申明函数指针变量。
上述代码也可以这样用
typedef char * (*type_pf)(char * p1,char * p2);
type_pf pf;
int main()
{
pf = &fun;//注 pf = fun;
(*pf) ("aa","bb");//注 pf("aa","bb");
return 0;
}
这样好处在于我们可以声明多个该类型函数指针,代码设计更加清晰
类似的函数指针数组或者是指向函数指针数组相关问题可以参考陈正冲老师的《c语言深度剖析》
这里提供带目录的下载:
函数指针用途
初学时候,虽然弄懂了函数指针的用法如何去使用,但是完全不了解为什么和在何时何处使用,
因为从表面看起来,使用函数指针比直接调用函数代码更加复杂难懂。
简单来说,函数指针用途:提供结构清晰的分层设计和灵活性,便于抽象。
- 便于分层设计:函数指针是引用,是间接层,或曰隔离层。它输出到上层,给上层用户用。函数实体是实现,在下层,给开发者用,实现者(软件工程师)关注。这就是简单的分层的概念了。上层用户想让一个函数所做的东西会变化时,我们只需要改变底层实现,并用函数指针指向新的实现就行了。
再精炼一下分层:分层的核心是对接口进行设计和实现。函数指针的作用就是提供不同实现的统一接口。 - 利于系统抽象:只有存在多个类似的实体需要模拟、操作或控制时(这种情况很多)才需要抽象。多个类似的实体就是对象,抽象的结果就是类。在C里边,可以用函数指针数组完成这种抽象。如, fopen 就是一个例子。他可以打开文件。C里面将磁盘文件、串口、USB等诸多设备抽象为文件。提供封装,实际上就是oo编程的特点。
如果一定要具体到用法,其实就是:
1. 回调函数
2. 实现面向对象编程
回调函数
网上有句话说的很好:本质上是,你想让别人调用你的代码,但别人的代码你不能动。
在实际代码层里,其实就是函数指针作为其他函数的参数。但抽象上这个其实涉及到分层设计的思想。
在嵌入式编程中,一般按设计上大致分可以分为系统编程,和应用编程。系统编程,简单来说,就是编写库;而应用编程就是利用写好的各种库来编写具某种功用的程序,也就是应用。系统程序员会给自己写的库留下一些接口,即API(application programming interface,应用编程接口),以供应用程序员使用,而应用程序员不能去修改到底层库代码。所以在抽象层的图示里,库位于应用的底下。
在VC6.0里面,我们可以理解系统提供的库函数其实也是系统库的一种。
抽象例子,有一家旅馆提供定时叫醒服务,但是要求旅客自己决定叫醒的方法。可以是打客房电话,也可以是派服务员去敲门,睡得死怕耽误事的,还可以要求往自己头上浇盆水。这里,“定时叫醒”这个行为是旅馆提供的,相当于库函数,但是叫醒的方式是由旅客决定并告诉旅馆的,也就是回调函数,回调在应用层。而旅客告诉旅馆怎么叫醒自己的动作,也就是把回调函数传入库函数的动作,称为登记回调函数。这个过程中我们发现,应用层(旅客)调用了底层的库(定时叫醒),而底层同样调用了应用层(叫醒方式),而这个底层调用高层的方式,我们称之为回调。
更多解释请看https://www.zhihu.com/question/19801131
例1:
在ucos中的软件定时器就是一个很好的回调函数例子。有兴趣可以去了解下,下面简单说说。
void funtimer();//希望定时调用的函数funtimer()
void task()
{
OSTmrCreate(OS_TMR_CALLBACK_PTR funtimer());//创建一个软定时器
//继续执行其他代码
}
//定时时间到之后会调到函数funtimer()
这里我们可以看到这其实就是一个函数回调的例子,从代码层看,其实就是funtimer()函数指针作为OSTmrCreate传递参数,但从设计思路看,应用层task代码不关心定时器的实现和调用方式。
例2:
实现一个文件系统,但是文件的介质有硬盘和软盘U盘sd卡等等,那么它们各自的读写函数实现肯定是不一样的。
实现函数
int y_write(char *data_stream, int LBA);
int r_write(char *data_stream, int LBA);
int Udisk_write(char *data_stream, int LBA);
int sd_write(char *data_stream, int LBA); .........
有一个结构体维护:
typedef int (*write_operation)(char* data, int LBA);
struct {
write_operation op;
...
} file_struct;
最后有一个写函数:
int file_wirte(char *data_stream, int LBA)
{
return file_struct.op(data_stream, LBA);
}
从上面代码可以看出,这时候就可以针对不同的设备实现统一的虚拟接口。
关于抽象
其实上面几个例子也有抽象和接口设计的思想,所以现实中往往使用回调其实也包含着分层和抽象的概念,笔者觉得函数指针这种用法更多应该注重在抽象思维层上面,而不是实际的代码层,这个方面后续有机会详谈
参考资料
http://blog.csdn.net/yao_qinwei/article/details/8982860
http://blog.csdn.net/wujiangguizhen/article/details/17153495
再举几个简单的网上例子:
1一个串口接口数据结构
可以看到,代码接口非常的简洁
/* 数据结构 */
typedef struct _Protocal_Callbacks{
BOOL (*pPackage_Check)(u8 *pdata, u16 len); /* 数据包地址域、接口类型、校验确认 */
BOOL (*pSet_Addr)(u8 addr); /* 设置设备地址 */
BOOL (*pSet_BPS)(u8 bps); /* 设置波特率 */
BOOL (*pSet_Dot_Group)(u8 *pdata, u8 len); /* 测温点分组配置 */
BOOL (*pSet_Time)(u8 *pdata, u8 len); /* 设置时间 */
BOOL (*pSet_OverTem)(u8 group, s16 tem); /* 设置过温阈值 */
BOOL (*pSet_Font)(u8 group, u8 dot, u8 num, u8 *pdata, u8 len); /* 设置字模 */
BOOL (*pSet_Tem_Y)(s16 tem); /* 设置Y轴标度 */
BOOL (*pRead_Addr)(void); /* 读地址 */
BOOL (*pRead_BPS)(void); /* 读波特率 */
BOOL (*pRead_Dot_Group)(void); /* 读分组信息 */
BOOL (*pRead_OverTem)(void); /* 读过温阈值 */
BOOL (*pRead_Version)(void); /* 读软件版本 */
BOOL (*pSet_433M_Frq)(u8 frq); /* 设置433M频点 */
BOOL (*pSet_Pan_ID)(u16 panid); /* 设置PanID */
BOOL (*pRestart_System)(void); /* 重启设备 */
BOOL (*pSet_IP_Imf)(u8 *pdata, u8 len); /* 设置IP信息 */
BOOL (*pEnd)(void); /* 触发包发送 */
void (*pNULL_Fun)(u8 code); /* 不支持此选项码 */
void (*pData_Err)(void); /* 造成溢出的错误 */
}CB_Typdef;[/mw_shl_code]
2转移表快速处理分支
1.可以用在转移表快速处理分支
处理分支,一般的做法是
if(vlue == 0)
{
fun0();
}
else if(value == 1)
{
fun1();
}
...
else if(value == 199)
{
fun199();
}
/*假如有200个分支,要判断200次(耗时,而且代码非常混乱)。 */
这时候就可以用函数指针做成数组
const void (* handle[200])(void) =
{
fun0,
fun1,
...
fun199
}
handle[value]();//通过value的值快速取得分支函数。