在指针万字详解这篇博客中初步介绍了函数指针与指针函数,在这篇博客中,我们将学习更多的进阶知识。 |
1.回忆+练习
指针进阶(2)
在《C陷阱和缺陷》这本书中,关于函数指针与指针函数这部分有两段有趣的代码
//代码1
(*(void(*)())0)();
//代码2
void (*signal(int,void(*)(int)))(int);
这段代码看似复杂,但我们如果能找到代码的特点,问题也就迎刃而解了。
代码1:
这段代码看起来毫无头绪,很混乱,要从哪里开始分析呢?从关键运算符开始分析,看到在最外层的括号里边有一个*
,*
号是在算术表达式里是乘的意思,但是这不是算术表达式,那就是解引用,什么可以解引用?指针可以解引用,那么这个*
号后面一定是个指针,(void(*)())0
怎么就变成了个指针呢?现在让我们回想一个知识,(int)后加一个数是强制类型转换成整型
(double)后是变成双精度浮点型,(void(*)())
也是一个类型名,只不过他是函数指针类型,那么现在就应该把0按照函数指针的形式解读,函数指针中存储的是函数的入口地址,这是个什么函数呢?是一个返回值为void,没有参数的函数。你在前面加上*后面填上(),就是在调用0地址的这个无参函数。
如果对函数指针和其调用不了解的,可以去看看我的指针详解一文,有相应的版块。
代码2:
代码2看起来好像有规律的多,不像代码1一样没有入手点。
void (*signal(int,void(*)(int)))(int);
一看就是一个函数的声明,我们都知道函数的声明包括函数名,返回值类型,参数三部分,我们一一对应,就可以很容易解决这个问题,signal
毫无疑问是个函数名,那后面的一长串就是参数,三部分我们已经找出来两部分了,那什么是返回值类型呢?我们把函数名以及参数删掉void (*)(int)
,一共就三部分,去掉两部分,剩下的自然就是返回值的类型,那这个返回值就是一个函数指针,函数指针指向的应该是一个参数为int,返回值为void的函数。
这里再稍微细说一下函数声明中的参数,(int,void(*)(int))
第一个就是整型,没什么好说的,第二个参数则是一个函数指针,指向的函数类型与上面介绍的返回值指向的类型相同。
注意:面对此类复杂但是有迹可循的代码,我们通常使用以下步骤:
1.确定由几个部分组成
2.找出最明显的部分
3.删除它,较为清晰的观察其他部分
对函数指针还不熟悉的同学可以借助图来理解。对指针的学习,画图是必不可少的。
2.函数指针数组,转移表
函数指针数组就是每一个元素都是函数指针的数组。定义如下:
int(*p[5])(int ,int);
我们可以按照之前的思路分析,p先与[ ]结合形成数组,那把数组名去掉剩下的int(*)(int ,int)
就是数组元素的类型了。那么函数指针数组有什么用呢?
函数指针数组通常用于转移表中,举个例子:
在学校学习或者自己应用时,我们通常会写一些小的项目来检验自己的学习成果,我们在写项目时,为了实现多种功能,经常会采用switch语句来做选项,例如:
#include<stdio.h>
int add(int x, int y)
{
return x + y;
}
int sub(int x, int y)
{
return x - y;
}
int mul(int x, int y)
{
return x * y;
}
int div(int x, int y)
{
return x / y;
}
int main()
{
int i, x, y, ret;
do
{
printf("********************************\n");
printf("* 欢迎使用简易计算器 *\n");
printf("* 0.exit 1.add *\n");
printf("* 2.sub 3.mul *\n");
printf("* 4.div *\n");
printf("********************************\n");
printf("请选择:>");
scanf_s("%d", &i);
switch (i)
{
case 1:
printf("输入操作数:");
scanf_s("%d %d", &x, &y);
ret = add(x, y);
printf("ret=%d\n", ret);
break;
case 2:
printf("输入操作数:");
scanf_s("%d %d", &x, &y);
ret = sub(x, y);
printf("ret=%d\n", ret);
break;
case 3:
printf("输入操作数:");
scanf_s("%d %d", &x, &y);
ret = mul(x, y);
printf("ret=%d\n", ret);
break;
case 4:
printf("输入操作数:");
scanf_s("%d %d", &x, &y);
ret = div(x, y);
printf("ret=%d\n", ret);
break;
case 0:
printf("退出程序");
break;
default:
printf("输入错误");
break;
}
} while (i);
return 0;
}
void PrintfFun(int i, int (*p[4])(int, int))
{
int x, y,ret;
if (i)
{
printf("请输入两个操作数:>");
scanf_s("%d %d", &x, &y);
ret = p[i](x, y);
printf("ret = %d\n", ret);
}
else printf("欢迎您下次使用");
}
例如这个简易计算器程序,switch语句中的代码太过赘余,简化的办法有很多,在这里我们只讲转移表的方法:
#include<stdio.h>
int add(int x, int y)
{
return x + y;
}
int sub(int x, int y)
{
return x - y;
}
int mul(int x, int y)
{
return x * y;
}
int div(int x, int y)
{
return x / y;
}
void PrintfFun(int i, int (*p[4])(int, int))
{
int x, y,ret;
if (i)
{
printf("请输入两个操作数:>");
scanf_s("%d %d", &x, &y);
ret = p[i](x, y);
printf("ret = %d\n", ret);
}
else printf("欢迎您下次使用");
}
int main()
{
int (*p1[5])(int, int) = {0,add,sub,mul,div};
int i;
do
{
printf("********************************\n");
printf("* 欢迎使用简易计算器 *\n");
printf("* 0.exit 1.add *\n");
printf("* 2.sub 3.mul *\n");
printf("* 4.div *\n");
printf("********************************\n");
printf("请选择:>");
scanf_s("%d", &i);
PrintfFun(i, p1);
} while (i);
return 0;
}
现在一看程序就简洁了很多,而且没有赘余重复,而这个转移表的关键就是应用了函数指针数组:int (*p1[5])(int, int) = {0,add,sub,mul,div};
通过把函数的入口地址存入数组的函数指针中,再通过数组下标的方式就可以调用多个函数
但是需要注意的是,这几个函数的返回值类型和参数类型都应该相同,否则不能使用转移表(因为数组元素的类型都是相同的)。
3.回调函数
这里我们简单了解一下,回调函数就是通过函数指针来调用的函数,而非使用我们常见的数组名,假如你有一个函数的参数是一个函数指针,你又通过这个函数指针调用了函数指针指向的函数,那么这就叫回调函数。简单举个例子:
#include<stdio.h>
int add(int x, int y)
{
return x + y;
}
void print(int(*p)(int,int),int x,int y)
{
printf("ret=%d\n", (*p)(x, y));
}
int main()
{
print(add, 5, 8);
return 0;
}
在这个段代码中我们发现,add这个简单的相加函数并不是通过函数名的方式调用,而是在print函数中我们通过一个函数指针p来进行调用,其实回调函数并不只有这么简单,他有着十分优越的功能,感兴趣的可以百度一下。