C语言指针的那些事:第三篇(函数指针,指针函数,函数指针数组,指向函数指针数组的指针)

1. 函数指针

函数指针就是指向函数的指针;本质上还是一个指针
首先我们要知道函数的地址就是函数名,取地址函数名也是函数的地址;这两个方式事等价的

类如:

int Add(int x, int y)
{
	return x + y;
}
# include<stdio.h>
int main()
{
	int(*p)(int, int) = Add; //这里函数名就是函数的地址
	int add = (*p)(10, 20); //通过函数指针调用函数
	//int add = p(10, 20);
	printf("%d", add);
	return 0;
}

首先关注int(*p)(int, int) = Add;这里说明 有* 号,说明p是个指针指向函数,函数的返回类型为int ,参数列表为(int,int); 那解引用p得到就是指针指向的东西,该指针指向的就是 函数,所以*p就是函数名,那么我就可以这样调用函数:(*p)(10,20);那既然函数名就是函数的地址,那也就是说还可以这么用指针当作函数名,可以这么调用函数 :p(10,20);

所以说,有函数指针,调用函数的方式就多了两种,这三种调用的方式是等价的:
(*p)(10,20),通过解引用指针调用函数
p(10,20),通过函数指针调用函数
Add(10,20);通过函数名调用函数

C语言规定:其实函数指针的变量名就是函数名,通过函数指针去调用函数时候,不需要解引用函数指针,也可以调用函数。
注意:
这三种函数指针的调用方式是等价的,说明调用函数时候*号没用

在这里插入图片描述


1)函数指针的例题

看一道题:void( * (signal( int ,void(*)(int)) ) (int) 请问如何理解这句声明:

首先我们从简单的看起:void(*)(int),看这个*说明是一个指针,指针的类型为去掉指针名,剩下的
就是指针类型,那这里没有指针名,所以 指针类型为 void(*)(int),指向的函数返回类型为为 void 参数列表为 >int,看 signal( int ,void(*)(int)) ;这就是signal 函数声明。 而 去掉函数声明,剩下的void(* )(int)又是一个>函数指针,即为signal函数声明返回的类型,所以我们可以这么说:
void( * (signal( int ,void(*)(int)) ) (int) 是函数声明,signal 是函数名, 返回类型为void(*) (int),参数列表为 intvoid(*)(int);返回类型是一个函数指针,指向的函数返回类型为 void和参数列表为int;
在这里插入图片描述

上面的函数声明等价下面的方式:

typedef void(*pfun_t)(int);//可以这么想 typedef void(* )(int) p_fun_t;
//把 void(* ) (int )类型 命名为 pfun_t类型;
 pfun_t signal(int,P_fun_t);

其实你可以类比一下数组,比如 定义数组声明时候 int arr[10]; 数组的类型为 int [10];写法 也是把名字插入到类型中间,而函数的类型也是,写法也是讲函数名插入到类型中间


看看另一道题目:*((void(*)())0)(),解释一些这语句标识什么意思。(来自《c缺陷和指针》)

解释:

*(   (  void(*)()  )  0  )  ();
首先我们知道一个数组前面带一个括号,括号里面是类型的话,是发生强制类型转化的。
这里就是 0 为数组 ,( void(*)() )0,这个意思就是把0转化为 void(*) () 类型
我们知道  (  void(*)()  ) 0 其实就是把 0转化为 函数指针类型。那么此时 0就表示 函数地址了。
然后 * 号标识解引用该函数指针类型的 0 ,*(   (  void(*)()  )  0  ) 表示解引用函数指针类型的0的地址得到指针指向的内容,其实就是函数名了。
最后的括号表示用函数指针类型解引用去调用函数。

所以最终解释为:*(   (  void(*)()  )  0  )  (); 这是一次函数调用,讲 0强制转化为函数指针类型,通过解引用该 指针类型去调用 0 位置的地址,得到函数名,然后 调用函数。

在这里插入图片描述


函数指针到底有什么用途呢?到时候学深入你就知道了,这里只是点播一下,以后碰到不至于不会。

2. 指针函数

其实指针函数本质是函数,之所以叫指针函数,只不过其返回类型是指针类型。

如:

int* Add(int x,int y); Add就是指针函数
指针函数没有什么神秘的,就是平时我们写的返回类型为指针的函数而已。

值得注意的一点是,不要返回局部变量的地址,让指针去接收她,不然,会有意想不到的错误发生。

3. 函数指针数组

函数指针数组,本质还是数组,只不过数的返回类型为 函数指针。

我们知道,在一个声明语句中,一个变量(指针变量也是变量)的类型,是去掉变量名剩下的类型。

所以 int arr[5]; arr的类型为 int[5];
int*arr[5]; arr 的类型为 int*[5];
int *(*parr)[5];parr 的类型为 int *(*)[5];
你可能,不理解类型的含义是什么?(当然前面的指针篇幅做出过解释),但是
你却可以清楚的知道 一个变量的 类型是什么。

所以 看看这个 声明
int(*arr[5])(int,int) ; 
arr的类型为 :int(* [5])(int ,int);
这是什么呢?假如我把int(*arr[5])(int,int) 写成 int(*)(int,int) arr[5] ;
是否看得更清楚:这样写,则类型为 int(*)(int,int)[5] ;
其实就是一个数组类型,但是我们却不可以这么写,需要写成 这种方式int(* [5])(int ,int);
声明时候也是写成:int(*arr[5])(int,int) ; 即使我们知道 arr是一个数组 ,也是需要 插入到类型的中间去,这是c语言规定的写法咯。

所以我们来解读一下:int(*arr[5])(int,int)声明的含义

int(*arr[5])(int,int);
首先:arr是一个数组,该数组有5个元素,每个元素的类型为函数指针类型 ,即int(*)(int,int);该函数指针指向的函数
返回类型为 int ,参数列表为有两个 分别为int int

在这里插入图片描述
在我的C语言指针的那些事:第一篇(有指针书写的技巧),系统的讲过数组的元素类型,数组类型,变量类型,指针类型,指针指向的类型到底怎么看出来的。顺便说了书写指针技巧。
总得来说:
要得到变量的类型,只要去点变量名就可以,这个变量名广泛(指针变量,函数名,数组名,普通变量名)都是变量名。
要得到指针指向的类型,只要去掉指针名和一个指针*星号,这个星号要和指针名挨着的星号呀,剩下都属于指针指向的变量类型。无论你的指针是多少级的指针,都是合适用的。


1)函数指针数组的用途

函数指针数组,可以存放函数名,也就是函数的地址,因为函数的地址类型就是函数指针。

我们先不讲函数指针数组,先引入一个案例,看看是否能优化,用什么办法优化。(这是极其简单的案例,不涉及过多的细节思考,只为了讲明白函数指针数组的用途)

首先我们有一个需求,就是实现一个计算器,该计算器 有 加减乘除 的方法。
通过输入对应的选项可以执行你指向的加减乘除法,然后输入你的计算数字,可以显示计算结果。

分析需求
我们可以设计四个函数,分别为加法 减法 乘法 除法 函数;
通过循环语句和 witch语句去控制选择 是算 加 减 乘 除;
其次输入计算条件,得到计算结果。

那么开始编码:大概就是这样:

#include <stdio.h>
int add(int a, int b) //加法函数
{
return a + b;
}
int sub(int a, int b) //减法函数
{
return a - b;
}
int mul(int a, int b) //乘法函数
{
return a*b;
}
int div(int a, int b)//除法函数
{
return a / b;
}

int main()
{
int x, y;
int input = 1;
  int ret = 0;
  do
 {
    printf( "*************************\n" );
    printf( " 1:add      2:sub \n" );
    printf( " 3:mul      4:div \n" );
    printf( "*************************\n" );
    printf( "请选择:" );
    scanf( "%d", &input);
    switch (input) //通过选择对应数字决定你是要做加减乘除法
   {
    case 1:

       printf( "输入操作数:" );
       scanf( "%d %d", &x, &y);
       ret = add(x, y);//加法一次调用
       printf( "ret = %d\n", ret);
       break;
    case 2:
       printf( "输入操作数:" );
       scanf( "%d %d", &x, &y);
       ret = sub(x, y); //减法一次调用
       printf( "ret = %d\n", ret);
       break;
    case 3:
       printf( "输入操作数:" );
       scanf( "%d %d", &x, &y);
       ret = mul(x, y);//乘法一次调用
       printf( "ret = %d\n", ret);
       break;
    case 4:
       printf( "输入操作数:" );
       scanf( "%d %d", &x, &y);
       ret = div(x, y);//除法一次调用
       printf( "ret = %d\n", ret);
       break;
    case 0:
        printf("退出程序\n");
breark;
    default:
       printf( "选择错误\n" );
       break;
   }
} while (input);
 
  return 0;
}

本代码很简单,目的是谈一谈什么地方可以优化。
你发现了吗?代码是否很冗长,不够美观。是因为我调用函数时候,写出了 四个调用函数。这写函数的代码基本一致,逻辑也是一致。返回类型也是一样。
是否有更好的办法去解决这个函数调用的问题,我希望有一种方式:可以写1 种代码的形式,就可以调用 上面 4个函数。那就是我们的函数指针数组,数组就是处理一些类型相同的代码而产生的。

所以一旦我们设计把 不同的函数名,初始化函数指针数组,通过下标就可以访问到数组的函数名,再通过()函数调用符号,就可以直接调用函数了,这不美哉?<?font>

说干就干。

#include <stdio.h>
int add(int a, int b)
{
     return a + b;
}
int sub(int a, int b)
{
     return a - b;
}
int mul(int a, int b)
{
     return a*b;
}
int div(int a, int b)
{
     return a / b;
}
int main()
{
  int x, y;

  int input = 1;
  int ret = 0;
  int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
  while (input)
  {
     printf( "*************************\n" );
     printf( " 1:add      2:sub \n" );
     printf( " 3:mul      4:div \n" );
     printf( "*************************\n" );
     printf( "请选择:" );
   scanf( "%d", &input);
     if ((input <= 4 && input >= 1))
    {
     printf( "输入操作数:" );
       scanf( "%d %d", &x, &y);
       ret = (*p[input])(x, y); //重点关注这句
    }
     else
       printf( "输入有误\n" );
     printf( "ret = %d\n", ret);
  }
   return 0;
}

来分析分析:

  int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; 
  这就是函数指针数组的写法了,里面初始值为:函数名;
  当我们调用时候:通过该语句
    ret = (*p[input])(x, y);
    就可以通过数组名p和下标运算符[],访问到数组元素啦,
    由于数组元素是函数名(函数地址),所以解引用一次,加个函数调用符号,就可以调用函数了。

	这真实秒啊。

4. 指向函数指针数组的指针

学了那么多指针,发现了吗,这些指针的名字都是一些排列组合形成的。
要明白指向函数指针数组的指针是什么东西,首先需要观察它的最后两个字:指针;说明就是指针,前面都是修饰作用。

所以:
指向函数指针数组的指针:就是指针指向的类型为函数指针数组类型。

那么我们知道声明这个函数指针类型吗?
想想我上面说过的话,如何定位,知道一个变量的类型。

好了,我们看看这个声明:void (*(*ppfunArr)[10])(const char*)

是不是很复杂? 那么我们来拆解一下:
提出变量名 ppfunArr 剩下的是变量名的类型 :void( (* (*)[10] )(const char*);
假如我们继续把指针ppfunArr挨着的星号*去掉,得到就是指针ppfunArr指向的类型:
void( (* [10] )(const char*);
看到这个有没有熟悉的感觉?
不就是一个函数指针数组嘛。对的,就是它。

所以我们如何解释这个声明呢?
解释: 首先 ppfunArr是一个指针,该指针指向的类型为函数指针数组,
该数组里面有10个元素,每个元素的类型为函数指针,
该函数指针指向的函数返回类型为 void ,参数列表为 const char*;

5. 一些后话

其实 指向函数指针的数组指针,了解的一点就够了,再复杂的不必要了解了,了解到这里,可以说对指针有着非常非常深刻的认识了,至于这个指针到底有什么应用,等到以后有需求时候,或者看别人代码时候,你就会知道怎么应用了,我们都是通过需求来应用知识滴,所以希望,大家可以知道这个点,不要认为学的东西没有用,理解这么复杂指针没有用。你保不定什么时候就用上了呢,对吧。

还有 ,可能,大家对复杂类型的声明还是不太会解释,或者说阅读还是有一定的困难。
所以我会写一篇番外篇: 教你如何阅读复杂类型的声明,图文并茂帮你解决
接下来还有一些指针的其他内容讲,比如一些常见指针笔试题,和一些其他有关的知识,回调函数等

  • 31
    点赞
  • 128
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

呋喃吖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值