[深入浅出C语言]深析指针(篇三)

447 阅读8分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情

前言

        学习C语言,不得不学到的就是指针,甚至可以这么说:指针是C语言的精髓之所在。

        本文就来分享一波作者的C指针学习见解与心得。本篇属于进阶第三篇,主要讲解了函数指针及其使用和函数指针数组的一些内容。后续还有进阶的其他内容,可以期待一下。

        笔者水平有限,难免存在纰漏,欢迎指正交流。

函数指针

        类比一下数组指针,可以知道函数指针就是指向函数的指针。

函数地址

        以下两种形式都可以拿到函数的地址。

#include <stdio.h>
void test()
{
     printf("hehe\n");
}

int main()
{
     printf("%p\n", test);
     printf("%p\n", &test);
     return 0;
}

image.png

        输出的是同一个地址,是 test函数的地址。

函数本质是什么

        一个函数就是一组存储在连续内存块中的指令集合,用来执行一个子任务(也就是函数,具有特定功能)。 

        我们写的程序源代码经过编译器处理最终得到机器代码,而函数相关指令存放在code代码区,机器语言中的函数调用基本上就是一条跳转指令,跳到函数的入口点(函数的第一条指令)。

image.png

        实际上,函数只要在程序中写出来了在真正执行程序前就有地址了,不论有无被调用。

函数地址的保存

        那我们的函数的地址要想保存起来,怎么保存?

int (*pf)(int, int) = &Add;

int ret = (*pf)(2, 3); 
//*其实可以不写,如:pf(2, 3),实际上有没有没关系的,只是有的话看起来更清晰易懂。

        在获得函数地址时,&加不加都可以,只是加上更好理解。

        在通过指针调用函数时,*加不加都可以,只是加上*更好理解,如果要加必须带上括号。

区别返回指针的函数和函数指针

void test()
{
    printf("hehe\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void *pfun2();

        上面的是对应类型的指针,下面的是一个返回值类型为void*的函数。

        ()优先级比*高,若要表示函数指针则要注意让指针名和*靠得更近也就是(*pf)加上括号,如果没加括号就是函数,还是返回指针的函数。

趣例探究

例1

//代码1
(*(void (*)())0)();

        想想看,这是个啥?

image.png

分析:

以上代码实质上是一次函数调用,调用的是0作为地址处的函数。

1.把0(int类型)强制类型转换成了void(*)()的函数指针类型,并且值为0,也就是地址为0

2.再调用0地址处的这这个函数。

例2

//代码2
void (*signal(int , void(*)(int)))(int);

        想想看,这是个啥?

image.png

分析:

        声明的signal函数的第一个参数的类型是int,第二个参数的类型是void(*)(int)函数指针,signal函数的返回类型也是一个函数指针,类型void(*)(int)

如何简化代码

        重命名类型名就清晰多了:

typedef void(*pf_t)(int)//把void(*)(int)类型重命名为pf_t

        这样的话原来的代码可以写成:

pf_t signal(int, pf_t);

        是不是一下子就看出来是什么了?


函数指针的使用

问题发现

        写一个简单的计算器小程序:

#include <stdio.h>

void menu()
{
        printf( "*************************\n" );
        printf( " 1:add           2:sub \n" );
        printf( " 3:mul           4:div \n" );
        printf( "*************************\n" );
        printf( "请选择:" );
}

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
   {
	    menu();
        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;
}

发现什么不太好的地方没有?

如图:

image.png

使用函数指针优化

        对于这个冗余问题,可能有人就会说,简单嘛,我把这个模块封装成函数再调用不就省事了吗?

        但是这四个模块是有差异的呀,里面调用的计算函数不同啊,比如调用的是add函数,那这个封装的函数不就只能执行加法运算了嘛。

        那可不可以把函数指针作为函数参数呢,在主函数中分了不同的情况,每种情况下传特定的函数的指针行不行呢?比如要执行加法运算了,就把add函数地址传入,再通过指针调用函数完成运算,即使要执行别的运算,也只是传入的函数地址不同,其他的代码完全是相同的。

//把运算统一起来
void calc(int(*pf)(int, int))
{
    int x =  0;
    int y = 0;
    int ret = 0;
    printf("请输入两个操作数:");
    scanf("%d %d", &x, &y);
    ret = pf(x, y);
    printf("%d\n", ret);
}

//主函数发生变动
int main()
{
     int x, y;
     int input = 1;
     int ret = 0;
     do
     {
        menu();
        scanf( "%d", &input);
        switch (input)
       {
            case 1:
                 calc(add);
                 break;
            case 2:
                 calc(sub);             
                 break;
            case 3:
                 calc(mul);
                 break;
            case 4:
                  calc(div);
                  break;
            case 0:
                  printf("退出程序\n");
                  breark;
            default:
                  printf( "选择错误\n" );
                  break;
        }
     } while (input);
    return 0;
}

        在主函数main中就能控制调用函数calc内的其他的被调用函数对象是什么(add/sub/mul/div),而且是根据主函数内不同情况要求下的响应。

        说实话,要是不用函数指针的话能写出这样的代码?

函数指针的数组

概念

        函数指针也是指针,而把函数指针放在数组中,就是函数指针数组。

        看看整型指针数组:

int *arr[10];//数组的每个元素是int*

        那函数指针数组长啥样呢?

        函数指针:

int(*p)(int, int)

        对应的数组:int(*parr[4])(int, int)

        parr先和 [] 结合,说明 parr是数组,数组的内容是什么呢?

        是 int (*)(int, int) 类型的函数指针。

函数指针数组的用途:转移表

例子(还是计算器)

        前面函数指针例子中用calc函数和函数指针传参的方法将四个运算函数统一了起来,而本例中不创建新的函数,而是使用函数指针数组和分支语句来对应不同情况下调用不同的函数进行运算,通过函数指针数组的值解引用来调用函数。

        这时候如果想增加计算器小程序的运算功能,比如增加&运算,^运算,|运算等等,只需要创建一个运算函数然后把地址放到函数指针数组里,改一下分支语句判断条件即可。

#include <stdio.h>
void menu()
{
    printf( "*************************\n" );
    printf( " 1:add           2:sub \n" );
    printf( " 3:mul           4:div \n" );
    printf( "*************************\n" );
    printf( "请选择:" );

}
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)
     {
	      menu();
          scanf( "%d", &input);
           //边界判断输入是否合法
          if ((input <= 4 && input >= 1))
          {
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = (*p[input])(x, y);//解引用数组元素调用函数
	          printf( "ret = %d\n", ret);
          }
	      else if(input == 0)
	      {
		      printf("退出计算器\n");
	      }
          else
              printf( "输入有误\n" );
     }
      return 0;
}

        为什么要把函数指针数组叫做转移表?通过函数指针调用函数实际上是跳转到对应函数,程序执行位置从一个地方转移到了另一个地方,也就是每个函数指针提供一次跳转机会,把这些指针放在一块的数组就是转移表。

函数指针数组的指针

         说实话,理论上可以无限“套娃”下去,指针放数组里,指针又可以指向数组......师傅别念了😭

         我们把上个例子的函数指针数组拿过来看看它的指针该怎么写:

int(*p[5])(int x, int y) = { 0, add, sub, mul, div };
int (*(*pfarr)[5])(int, int) = &pfarr;

image.png

         很多代码看起来花里胡哨,其实只要抓住本质特征就不容易混淆:

         (*parr)[ ]是数组指针,*parr[ ]是指针数组。

         其他的一堆东西都是附加的,如果本质是数组指针,那么其他的东西就是数组的元素类型;如果本质是指针数组,那么其他的东西就是指针的类型。


以上就是本文全部内容,感谢观看,你的支持就是对我最大的鼓励~

u=779924663,1900594976&fm=253&fmt=auto&app=120&f=JPEG.webp