Bootstrap

【落羽的落羽 C语言篇】指针·之其四

在这里插入图片描述

文章目录

  • 一、字符指针变量
  • 二、数组指针变量
    • 1. 创建
    • 2. 数组指针类型
    • 3. 二维数组传参的本质
  • 三、函数指针变量
    • 1. 创建
    • 2. 函数指针类型
    • 3. 函数指针的使用
    • 4. 分析两句“有趣”的代码(doge)
    • 5. typedef关键字
  • 四、函数指针数组
    • 1. 创建
    • 2. 函数指针数组的用途——转移表

一、字符指针变量

在讲到指针变量的类型时,我们知道有一种指针类型是字符指针char*
最简单的使用方法是:

int main()
{
char ch = 'a';
char* p = &ch;
*p = 'b';
return 0;
}

还有一种使用方式:

int main()
{
char* p = "hello world";
return 0;
}

第三行char* p = "hello world";,并不是把字符串hello world放到字符指针里了,而是把它的首字符h的地址放到了p中。

知道了这个,我们就能尝试分析一下下面这道题:

#include<stdio.h>
int main()
{
char arr1[]="abcde";
char arr2[]="abcde";
char* parr1="abcde";
char* parr2="abcde";
if(arr1 == arr2)
  printf("arr1 and arr2 are same\n");
else
  printf("arr1 and arr2 are not same\n");
if(parr1 == parr2)
  printf("parr1 and parr2 are same\n");
else
  printf("parr1 and parr2 are not same\n");

return 0;
}

思考一下结果会是什么样的~
在这里插入图片描述

解析:arr1 == arr2比的是这两个数组首元素的地址,这两个数组是独立的,只是内容相同而已,数据存储的地址并不一样;而parr1 == parr2比的是两个指针变量中存放的地址,这两个指针指向的是同一个字符串abcde,所以是相同的

在这里插入图片描述
从本质上来讲:C语言会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串时,它们实际上会指向同一块内存区域。但是用相同的常量字符串去初始化不同的数组时就会开辟出不同的内存区域。
在这里插入图片描述

二、数组指针变量

1. 创建

在《指针·之其三》中,我们学习了指针数组。现在我们再来学习数组指针——指向数组的地址的指针变量。
数组指针变量的写法和前面的几种稍有不同,它的创建语法是数组元素类型(*指针名)[指向的数组的元素个数]比如:int(*p)[10],万万不能写成int* p[10]!因为[ ]的优先级高于*,所以必须写成(*p)来保证p先和*结合,以此说明p是指针变量,然后p指向的是一个大小为10的整形数组,p就是一个数组指针变量。

2. 数组指针类型

数组指针是用来存放数组的地址的,那怎么获得数组的地址呢?非常简单,使用我们之前提到的&数组名
在调试中也能看出来:&arr和p的类型是一样的

在这里插入图片描述那么,这个数组指针的具体类型又是什么呢?

我们知道,数组也有各种各样的类型,比如int arr1[10],char arr[8]等等,它们在元素类型和数组大小上都是不同的,所以数组指针变量也有不同的具体类型,怎么得到这些类型呢?

类比推理:
int* p1:p1的类型是int*,即是该句代码里去掉p1这个变量名;
char* p2:p2的类型是char*,即是该句代码里去p2这个变量名;

所以,int(*p)[10]:该句代码里去掉p这个变量名,int(*)[10]就是p的类型,p是一个存放10个整型的数组的数组指针。但虽然这就是p的类型,你也不能创建时就写成int(*)[10] p,这样程序会报错。王八的屁股——规定。

3. 二维数组传参的本质

有了数组指针的理解,我们就能学习二维数组传参的本质了。
在以前,我们想把二维数组传参给一个函数时,是这样写的:

#include<stdio.h>

void test(int arr[3][5],int r,int c)
{
for(int i=0 ; ;i<r ; i++)
  {
  for(int j=0 ; j<c ; j++)
    printf("%d ",a[i][j]);
  printf("\n");
  }
}

int main()
{
int arr[3][5]={{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}};
test(arr,3,5);
return 0;
}

实参是二维数组,形参也是二维数组的形式。

然而,我们以前讲过,二维数组可以看成是每个元素都是一维数组的数组。也就是说,二维数组的首元素就是第一行,是一个一维数组。所以,根据数组名是数组首元素的地址这个规则,二维数组的数组名表示的就是第一行的地址,也就是一维数组的地址。根据上面的例子,第一行的一维数组的类型就是int [5],第一行的地址的类型就是int(*)[5]。这意味着,二维数组传参本质上也是传递了地址,传递的是第一行这个一维数组的地址。

由此,上面的函数参数部分就可以改变一下:

#include<stdio.h>

void test(int(*p)[5],int r,int c)
{
for(int i=0 ; ;i<r ; i++)
  {
  for(int j=0 ; j<c ; j++)
    printf("%d ", *(*(p+i)+j) );
  printf("\n");
  }
}

int main()
{
int arr[3][5]={{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}};
test(arr,3,5);
return 0;
}

关于*(*(p+i)+j)的理解:p+i是纵下标为 i 的一维数组的地址,*(p+i)是找到纵下标为 i 的一维数组,*(p+i)+j是该一维数组里下标为 j 的元素的地址,*(*(p+i)+j)就是该元素了

在这里插入图片描述

三、函数指针变量

1. 创建

函数也是有地址的,我们来观察一下:

在这里插入图片描述如图,函数test也是有地址的。

根据前面整型指针变量、数组指针变量的学习,我们不难知道:函数的地址存放在函数指针变量里。和数组名类似,函数名就是函数的地址当然也可以通过 &函数名 的方式获得函数的地址。
如果我们要存放函数的地址,当然也需要创建函数指针变量,它的写法和数组指针变量十分相似:函数返回类型(*指针变量名)(指向函数的参数),参数名可以省略。比如int(*p)(int x,int y)(x和y可以不写),p就是一个指向“有两个整型参数,返回类型是整型”的函数的函数指针变量。

2. 函数指针类型

这部分也和刚刚讲到的数组指针类型道理相同,创建一个函数指针变量时去掉函数名,就是这个函数指针变量的具体类型了。
比如上面的int(*p)(int x,int y),p的类型是int(*)(int x,int y)
灰常简单

3. 函数指针的使用

我们可以通过函数指针调用其指向的函数,假如指针变量p指向了函数test,那么*p和p都能表示test函数:

#include<stdio.h>
int Add(int x,int y)
{
return x+y;
}
int main()
{
int(*p)(int,int) = Add;
printf("%d\n", Add(2,4));
printf("%d\n", p(2,4));
printf("%d\n", (*p)(2,4));
return 0;
}

在这里插入图片描述结果都没有问题

4. 分析两句“有趣”的代码(doge)

  • 代码1:(*(void(*)())0)();
    相信所有人第一次见到它时,都是一脸懵,但这真的不是乱码,且看我画图分析:
    在这里插入图片描述但实际上,地址0是无法使用的,这其实是一个非法操作,仅仅是用来展示函数指针概念的。

  • 代码2:void(*signal(int,void(*)(int)))(int);
    在这里插入图片描述

通过这两段代码,想必大家对函数指针变量和函数指针类型有了更深刻的了解了吧,嘻嘻。
这两段代码都出自《C陷阱和缺陷》这本书,感兴趣可以去阅读一下~

5. typedef关键字

刚刚我们分析两段代码的过程是十分费劲的,归根结底是因为那些变量类型写起来太复杂了。而关键字typedef正好能解决这个问题,它是用来给类型重命名的,可以将复杂的类型简单化。用法是typedef 原类型 重命名;

举个栗子,你觉得long long int写起来不方便,如果能改成lli就好了,那么就可以有:

typedef long long int lli;

一般的类型和指针类型都可以这样用。但是对于数组指针类型和函数指针类型,稍微有点区别:新的类型必须写在*的后面。比如我们想把int(*)[5]重命名为parr_t,应该写成:

typedef int(*parr_t)[5];

我们想把void(*)(int)重命名为pf_t,应该写成:

typedef void(*pf_t)(int);

有了typedef这个工具,我们就可以简化上面的代码2:void(*signal(int,void(*)(int)))(int);

typedef void(*pf_t)(int);
//代码2就可以写成:
pf_t signal(int,pf_t);

在这里插入图片描述

四、函数指针数组

1. 创建

我们已经学了指针数组、函数指针,那么试试把它们组合起来吧——将函数的地址存放到数组中,这就是函数指针数组,它该如何定义呢?答案是:将数组名和大小写在函数指针类型的*后。

比如,创建一个数组parr,能存放3个元素,元素类型是“有一个整型参数,无返回类型”的函数指针类型。就应该写成:void(*parr[3])(int)

2. 函数指针数组的用途——转移表

函数指针数组最常见的用途就是转移表。转移表是一种数据结构,它根据输入值来确定需要执行的函数或操作。
一个实例:实现一个计算器,要能根据你的选择进行加减乘除运算。
经过思考,我们可以分布实现:

  • 分别定义加减乘除的函数:
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(*p[5])(int,int)={ 0 , add , sub , mul , div };
//0的作用是让四个函数的下标能和下面的选项对应起来
  • 设定菜单选项
printf("----------------------\n");
printf("----1.add    2.sub----\n");
printf("----3.mul    4.div----\n");
printf("--------0.quit--------\n");
printf("----------------------\n");
printf("请选择:");
  • 根据输入的选项,决定执行函数
scanf("%d",&option);
if((option<=4)&&(option>=1))
{
printf("输入操作数:");
scanf("%d %d",&x,&y);
ret = (*p[option])(x,y);
printf("结果是%d\n",ret);
}
else if(option==0)
  return 0;
else
  printf("输入错误");

最后,把上面的所有东西结合起来,并且让它不退出一直可以选择计算:

#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(*p[5])(int,int)={ 0 , add , sub , mul , div };
int x,y;
int ret=0;
int option=0;

while(1)
{
printf("----------------------\n");
printf("----1.add    2.sub----\n");
printf("----3.mul    4.div----\n");
printf("--------0.quit--------\n");
printf("----------------------\n");
printf("请选择:");
scanf("%d",&option);
if((option<=4)&&(option>=1))
  {
  printf("输入操作数:");
  scanf("%d %d",&x,&y);
  ret = (*p[option])(x,y);
  printf("结果是%d\n",ret);
  }
else if(option==0)
  return 0;
else
  printf("输入错误\n");
}
//一个很巧妙的点是,我并没有将return 0放在最后,这样程序只会在option==0的情况下结束,结束前while(1)会让它一直运行
}

这样,程序就很完美了
在这里插入图片描述

欲知后事如何,且听下回分解~
在这里插入图片描述
本篇完,感谢阅读

;