前言:本篇是关于函数指针的保姆级教程
一、函数指针的定义和修饰
函数指针广泛应用于嵌入式软件开发中,其常用的两个用途:调用函数和做函数的参数。
void (*fptr)();
//把函数的地址赋值给函数指针,一般采用如下形式:
fptr=Function;
//如果是函数调用,还必须包含一个圆括号括起来的参数表。可以采用如下方式来通过指针调用函数:
x=(*fptr)();
使用typedef来“修饰”一个函数指针
typedef void (*fptr)();
fptr func;
fptr func1;
fptr func2;
在刚开始学C语言的时候,总认为typedef取别名的一般形式为
typedef 旧名字 新名字;
但遇到给函数指针类型、数组类型等定义别名的时候就要特别区分了。如:
typedefchar ARRAY20[20];
ARRAY20 a1,a2; /* 等价于char a1[20],a2[20]; */
再论typedef
typedef int size; // typedef行
int i; // 原型行
size i; // 应用行
//同理:
typedef char Line[81]; // typedef 行
char t[81]; // 原型行
Line t; // 应用行
//再引申下:
typedef int (*fun_ptr)(int,int); // typedef 行
int (*fp)(int,int); // 原型行
fun_ptr fp; // 应用行
以上三个例子都有以下几个共同点。
首先,“typedef 行”和 “原型行”相比,“typedef 行”仅仅多个 typedef 而已。就函数指针的例子来说,“typedef 行”和 “原型行”的根本区别在于,fun_ptr 是类的别名,fp 是该类的变量。
其次,“原型行”和“应用行”的编译结果是一样的。就函数指针的例子来说,它们都是创建了一个类型为 int(*)(int,int) 的函数指针 fp。只是 fun_ptr fp(应用行)比 int(*fp)(int,int)(原型行)这种形式更加简洁,便于书写和理解。形式越复杂,typedef 的优势就越明显。
二、函数指针的具体示例
(一)示例1:指针函数与函数指针的类型
int *pf(int *, int); // int *(int *, int)类型
int (*pf)(int, int); // int (*)(int, int)类型
指针函数的类型:int *(int *, int)
函数指针的类型:int (*)(int, int)
(二)示例2:一个极简的函数指针
//返回值类型 (*指针名称)();
int (*)()
//定义了一个指向返回值为int,无参数的函数的指针;
定义了一个指向返回值为int,无参数的函数的指针。
(三)示例3:函数指针的跳转
我们在真实的项目开发过程中,可能需要直接跳转到函数的某个地址去指针。
void (*function_p)(void); //定义函数指针function_p,无返回值,无参数
function_p = my_func; //函数指针指向function函数
(*function_p)(); //采用函数指针运行函数
这个等同于直接调用my_func
函数,那么这个有什么意义呢?其实这样提出了一个思路,就是可以根据函数的地址,跳转到函数的地址。(具体可参照示例4)
(四)示例4:使用函数指针调用特定内存地址的函数
首先看一个例子:
(*(void(*) ())0)()
void (*)()
是一个函数指针,只是把p
去掉了而已。把上面的
void (*)()
用PN
代替,上面的表达式变成(*(PN)0)()
;
PN
后面有一个0
,这个是让人咋舌的地方,然后想一下(char)a
这样的表达式;因此所以*(PN)0
就是把0
当成一个地址,强制转换为PN类型,用*
这个钥匙取出这个地址区域的值。把
(*(PN)0)()
替换成PM
,原来的表达式变成PM()
,这样看起来就明了了,就是正常的函数调用。
(五)示例5:bootloader中经典的内存跳转
比如我们在bootloader中,当把二进制文件加载到内存中后,如何去执行这个kernel程序呢?也就是实现一个bootloader到kernel的跳转。
((void(*)())0x80000)();
这里就是说0x80000
处的地址是函数类型,并且没有返回值。当我们的kernel地址为0x80000
时程序跳转过去,不再返回。这就是一个比较经典的例子。实际上此示例同示例4。
const修饰指针
此处插入一下,如果指针所指向内存中的数据或指针指向不想被修改,可以参考const用法。
const int *p;//指针变量p所指向的空间内容不可修改即*p不可修改,但p可修改
int const *q;//指针变量q所指向的空间内容不可修改即*q不可修改,但q可修改
int *const r;//指针变量r不可修改,但*r所指向的空间内容可修改
const int * const x;//指针变量x自身不可修改,x所指向的空间内容亦不可修改
(六)示例6:函数指针数组形式的调用
void (*f[])(char *)
f
是个什么what?
[]
的优先级 比*
的优先级高,所以f
首先是修饰了数组,然后跟后面的*
组合,就说明这个数组里面住的都是指针,这些指针是什么呢,再出来看看就看到了,这个指针是 一个函数,这个函数的 参数是char *
返回值是void
。因此这是一个函数指针数组!!!
函数指针数组1:
#include <stdio.h>
void (*f[3])(char *);
void efunction(char * s)
{
printf("%s\n",s);
}
int main()
{
f[0] = efunction;
//void (*f[])(char *) = {efunction};
(*f[0])("hello code");
return 0;
}
函数指针数组2:
#include <stdio.h>
#include <string.h>
char * fun1(char * p)
{
printf("%s\n", p);
return p;
}
char * fun2(char * p)
{
printf("%s\n", p);
return p;
}
char * fun3(char * p)
{
printf("%s\n", p);
return p;
}
int main()
{
char * (*pf[3])(char * p);
pf[0] = fun1; // 可以直接用函数名
pf[1] = &fun2; // 可以用函数名加上取地址符,等价于pf[1] = fun2
pf[2] = &fun3;// 等价于pf[2] = fun3
pf[0]("fun1");
pf[0]("fun2");
pf[0]("fun3");
getchar();
return 0;
}
三、函数指针的调用
(一)示例1:用typedef申请一个简单的函数指针
//声明一个函数指针,一般的方法是
int (*pfunc)(int a,int b)
//当命名很多个函数指针的时候,用上面的方法显得非常不方便,可以这样做
typedef int (*PF) (int a,int b)
PF pfunc;
/********************************************************************/
//例程
#include "stdio.h"
typedef int(*PF)(int, int);
int add(int a, int b)
{
return a + b;
}
int reduce(int a, int b)
{
return a - b;
}
int main()
{
PF pfunc = NULL;
pfunc = add;
printf("add:%d\n",pfunc(3, 4));
pfunc = reduce;
printf("reduce:%d\n", pfunc(3, 4));
/*getchar是用VS编写方便查看输出*/
getchar();
return 0;
}
(二)示例2:一种隐藏式的函数指针调用
#include<stdio.h>
#include <assert.h>
double getMin(double *dbData, int iSize) // 求最小值
{
double dbMin;
assert((dbData != NULL) && (iSize > 0));
dbMin = dbData[0];
for (int i = 1; i < iSize; i++)
{
if (dbMin > dbData[i])
{
dbMin = dbData[i];
}
}
return dbMin;
}
double getMax(double *dbData, int iSize) // 求最大值
{
double dbMax;
assert((dbData != NULL) && (iSize > 0));
dbMax = dbData[0];
for (int i = 1; i < iSize; i++)
{
if (dbMax < dbData[i])
{
dbMax = dbData[i];
}
}
return dbMax;
}
double getAverage(double *dbData, int iSize) // 求平均值
{
double dbSum = 0;
assert((dbData != NULL) && (iSize > 0));
for (int i = 0; i < iSize; i++)
{
dbSum += dbData[i];
}
return dbSum/iSize;
}
double unKnown(double *dbData, int iSize) // 未知算法
{
return 0;
}
/*******************************************
定义函数指针类型: double(*)(double* , int)
输入参数:double指针和int
返回值:指向double的函数
********************************************/
typede double (*PF)(double *dbData, int iSize);
PF getOperation(char c) // 根据字符得到操作类型,返回函数指针;注意返回值“PF”隐式调用
{
switch (c)
{
case 'd':
return getMax; //隐式调用;若显示则为“PF = getMax”,且getOperation定义为void
case 'x':
return getMin; //隐式调用,返回函数指针
case 'p':
return getAverage;//同上
default:
return unKnown;
}
}
int main(void)
{
double dbData[] = {3.1415926, 1.4142, -0.5, 999, -313, 365};
int iSize = sizeof(dbData) / sizeof(dbData[0]);
char c;
printf("Please input the Operation :\n");
c = getchar();
PF pf = getOperation(c);
printf("result is %lf\n", pf(dbData, iSize));//!!!使用函数指针
return 0;
}
使用这个函数的主要步骤为:
1. 定义与PF类型匹配的函数,如getMax、getMin等。
2. 确定要进行的操作对应的字符,如'x'表示getMin。
3. 调用getOperation()函数,传入操作字符,如getOperation('x')。
4. 函数会返回对应操作的函数指针,如返回getMin函数指针。
5. 我们可以直接使用返回的函数指针调用getMin()函数。
6. 所以通过这个函数,可以根据简单的输入选择调用不同的函数,增加了程序的灵活性。
使用这个根据输入返回函数指针的函数需要注意:
1. 定义的函数必须严格匹配PF的类型定义,否则编译无法检查,运行时会出错。
2. 输入的操作字符必须在函数支持的选项范围内,否则返回未定义函数指针。
3. 返回的函数指针可以直接调用,但函数的定义和指针的有效范围必须正确。
4. 该函数隐藏了具体的函数调用,增加了抽象性但也增加了难度。如果出现问题,追踪会更加困难。
具体的显示调用可参考下一篇文章。
(三)示例3:强制类型转换将函数地址赋值给函数指针以实现调用
void (*p)(); //指向返回void的,参数为空的函数指针
/*********************************/
#include "stdio.h"
void Function()
{
printf("Call Function!\n");
}
int main()
{
void(*p)();//类型为:void(*)()
*(int*)&p = (int)Function;//将Function函数的地址强制转换为int类型,赋值给p;
(*p)();//通过(*p)()调用p指向的函数,也就是调用Function函数
getchar();
return 0;
}
这个程序的主要步骤为:
1. 定义一个void Function()函数,用于打印信息。
2. 在main函数中声明一个void(*)()类型的函数指针p。
3. 将Function函数的地址强制转换为int类型,赋值给p。
4. 这实际上是将Function的地址保存到p这个函数指针。
5. 通过(*p)()调用p指向的函数,也就是调用Function函数。
6. 所以最终这个程序会打印出"Call Function!"。
7. getchar()用于阻塞,等待用户输入以防窗口自动关闭。所以,这个程序展示了如何通过强制类型转换的方式,将一个函数的地址赋值给函数指针,并通过函数指针调用该函数。这属于函数指针的高级用法。
使用这个方法需要注意:
1. 必须确保p的类型与Function的类型严格匹配,否则无法正确调用Function。
2. 将Function的地址赋值给p时,需要通过强制类型转换方式,这会导致编译器无法进行安全检查。如果类型不匹配,运行时会导致问题。
3. 如果Function的类型发生变化,p调用Function的语句也需要相应修改,否则会出现问题。这增加了代码的维护难度。
4. 这种通过 address 直接获取函数地址的方式是实现函数指针的基础,但实际使用中,通常通过函数名直接为函数指针赋值更加简单与安全。
5. 这种通过强制类型转换实现函数调用的方式,增加了代码的抽象度,但也增加了调试的难度。需要慎用。
(四)示例4:函数指针作为函数的返回值
void sCal(void *param,void *fuc)
{
((void (*)(void*))fuc)(param);
}
1、这个sCal函数主要是使用void*实现一个公共的接口,包括数据和方法,所以函数内部对fuc指针进行了一个强制类型转化,并且把param作为函数参数传入。
2、使用注意:
1)fuc作为void*类型,实际上是个函数指针,但类型不定。
2)将fuc强制转换为void (*)(void)类型,即指向参数为void*,返回void的函数指针;
3)利用转换后的函数指针进行函数调用,传入param作为参数;
4)作为参数传递的param可以在所调用的函数中使用;【示例2的一种“显示”调用】
5)因为返回类型为void,所以不返回任何结果。
3、void (*) 表示返回值为void的函数指针;(void*)表示该函数指针接收参数的类型。
同样使用typedef进行简化,其真面目就很明显了:
((void (*)(void*))fuc)( param );
-->
( (pFuc * ) fuc)( param );
(五)函数指针的三大要素
当看到复杂的函数指针形势时,准确区分三大要素就能把握函数指针。
1、函数指针的返回值;
2、函数指针的参数;
3、函数指针所指向函数的参数;
(六)函数指针作为返回值,实现回调函数的注册
函数指针的形式如此复杂的主要原因是 : 函数指针既可以作为函数的参数,也可以作为函数的返回值;而作为函数参数的函数指针又可以带有函数指针参数和函数指针返回值,层层嵌套,一旦展开那还是非常恐怖的。
void ( *signal(int signum,void(*handler)(int)) )(int)
pFuc1 *signal(int signum,void(*handler)(int))
pFuc1 *signal(int signum,void( *handler )(int))
pFuc2 void(*)(int)
pFuc1 *signal(int signum,pFuc2 *handler )
void (signal(int , void()(int)))(int) 是一个复杂的函数原型,它的作用是:
signal() 函数接收两个参数:第一个参数是中断信号,第二个参数是中断信号对应的处理函数。
signal() 函数返回值是中断信号之前的处理函数。也就是说,这个函数原型定义了 signal() 函数,它可以用来注册中断信号对应的处理函数,并返回中断信号之前的处理函数。
这个函数原型的具体语法为:
void (*signal(int signum, void (*handler)(int)))(int);
它返回一个指向参数为int,返回值为void的函数指针。
使用这个函数的步骤为:
1. 定义中断信号处理函数,其原型为 void fun(int)。
2. 调用 signal() 函数,传入要注册的中断信号编号和处理函数名。如:
void (*old_handler)(int);
old_handler = signal(SIGINT, fun);
3. signal() 函数会注册 fun() 函数为 SIGINT 信号的处理函数。
4. signal() 返回之前的 SIGINT 信号处理函数,存储在 old_handler 中。
5. 当产生中断时,fun() 函数会被调用,用于处理该中断。
6. 如果需要恢复老的处理函数,可以调用:signal(SIGINT, old_handler)。
具体的回调函数和回调注册机制将在写一篇文章中阐述。
以上signal函数的简化写法
void (*signal(int , void(*)(int)))(int);
/*
1、signal 是一个函数声明;
2、这个函数的参数有 2 个:
第一个是 int 类型;
第二个是函数指针,该指针类型为“返回void,函数参数 int”;
3、signal 函数的返回类型也是函数指针,该指针指向的函数参数 int,返回类型是 void 。
*/
typedef void(*pfun_t)(int);
//原函数
void (*signal(int , void(*)(int)))(int);
//简化后
pfun_t signal(int , pfun_t);