C语言指针系列(四):字符指针、数组指针与函数指针数组
文章目录
- 前言
- 一、字符指针变量
- 二、数组指针变量
- 1.数组指针变量是什么?
- 2.数组指针变量怎么初始化
- 三、二维数组传参的本质
- 四、函数指针变量
- 1. 函数指针变量的创建
- 2. 函数指针变量的使用
- 3. 两段有趣的代码
- 4. typedef关键字
- 五、函数指针数组
- 六、转移表
- 总结
前言
在掌握了指针的基本概念、const限定与安全使用后,本篇将进一步探索指针在字符串、数组和函数中的高级应用。我们将学习字符指针变量的两种使用方式,理解数组指针的本质与初始化,剖析二维数组传参的底层机制,并逐步深入函数指针、函数指针数组以及利用转移表实现计算器。这些内容将帮助您真正理解“指针是C语言灵魂”这句话的含义。
一、字符指针变量
在指针的类型中我们通常知道一种指针类型叫字符指针char*
使用方法如下
这是单个字符的情况
intmain(){charch='w';char*pc=&ch;*pc='w';return0;}还有一种使用方法,这里char*指针作为字符串的首地址
intmain(){constchar*pstr="hello world.";printf("%s\n",pstr);return0;}
这里的pstr实际指向的是这个字符串的首地址,即为首字符h的内存地址.
存储的是首字符h的内存地址
二、数组指针变量
1.数组指针变量是什么?
我们之前学过指针数组,就是用来存放指针变量的一个数组,本质上还是数组.那么我们今天要讲的数组指针变量是数组还是指针呢?
答案是:指针变量
我们之前学过
- 整型指针变量:
int * pint,存放的是整型变量的地址,是一个能够指向整型的指针. - 浮点型指针变量:
float * pf,存放的是浮点型变量的地址,是一个能够指向浮点型的指针.
类比可知,我们今天讲的数组指针,存放的就是数组的指针,能够指向数组的指针变量
我们来看一下下面两个代码:
int*p1[10];//代码1int(*p2)[10];//代码2这里我们要注意
p1是指针数组,本质还是一个数组,数组中的每一个元素都是一个指向int类型的指针.p2是数组指针,本质上是一个指针,指向一个包含10个元素的数组.
数组指针变量:
int(*p)[10];这里的*先和p结合,说明p是一个指针变量,然后指针再指向一个大小为10的一个数组.所以p是一个指针变量,指向一个数组,叫数组指针.
这里要注意:[]的优先级是大于*的,所以一定要加上()来保证 * 和p先结合,
2.数组指针变量怎么初始化
那么数组指针是如何获取并存放数组的地址的呢?这里就要用到我们之前讲过的取地址符&.
intarr[10]={0};&arr;//得到的是数组的地址如果要存放数组的地址,那就得存放在数组指针中,如下:
int(*p)[10]=&arr;下面我们来介绍一下数组指针的各个部分:
int(*p)[10]=&arr;||||||||p指向的数组的元素个数|p是数组指针的变量名 p指向的数组元素类型三、二维数组传参的本质
有了对数组指针的初步理解,我们就可以学习二维数组传参的本质了.
过去我们有一个二维数组需要传参给一个函数时,我们是这样写的:
#include<stdio.h>voidtest(inta[3][5],intr,intc){inti=0;intj=0;for(i=0;i<r;i++){for(j=0;j<c;j++){printf("%d ",a[i][j]);}printf("\n");}}intmain(){intarr[3][5]={{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}};test(arr,3,5);return0;}这里的实参是一个二维数组,形参也是,那么还有什么其他的写法吗?
在这里我们先了解一下二维数组的本质,二维数组其实可以看成每个元素都是一维数组的数组,也就是二维数组的每一个元素都是一维数组.二维数组的首元素就是第一行,是个一维数组.
如下图:
所以,根据数组名就是首元素地址我们可知,二维数组的数组名表示的是第一行的数组的地址,是一个一维数组的地址.根据上面的例子,第一行的一维数组类型就是int [5],所以第一行的数组的数组指针类型就是int (p)[5].那么二维数组传参本质也是传地址,传的是第一行这个一维数组的地址,那么传参形式也可以写成指针形式.如下:
#include<stdio.h>voidtest(int(*p)[5],intr,intc){inti=0;intj=0;for(i=0;i<r;i++){for(j=0;j<c;j++){printf("%d ",*(*(p+i)+j));}printf("\n");}}intmain(){intarr[3][5]={{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}};test(arr,3,5);return0;}总结:二维数组传参可以写成数组形式,也可以写成指针形式.
四、函数指针变量
1. 函数指针变量的创建
那么什么是函数指针变量呢?根据前面的学习,我们可以大致类比知道:
函数指针变量存放的是函数的地址,是一个能指向函数的指针.
那么函数是否有地址呢?
我们来做个测试:
#include<stdio.h>voidtest(){printf("hehe\n");}intmain(){printf("test: %p\n",test);printf("&test: %p\n",&test);return0;}打印结果
test:005913CA&test:005913CA这里能打印出结果,说明函数是有地址的,函数名就是函数的地址,我们也可以通过&函数名的方式获得函数的地址.
如果我们要将函数地址存起来,那就得创建函数指针变量,其实函数指针变量写法和数组指针非常类似,如下:
voidtest(){printf("hehe\n");}void(*pf1)()=&test;void(*pf2)()=test;intAdd(intx,inty){returnx+y;}int(*pf3)(int,int)=Add;int(*pf3)(intx,inty)=&Add;//x和y写上或者省略都是可以的函数指针类型解析
int(*pf3)(intx,inty)||-------------|||||pf3指向函数的参数类型和个数|函数指针的变量名 pf3指向函数的返回类型2. 函数指针变量的使用
我们可以通过函数指针调用函数
#include<stdio.h>intAdd(intx,inty){returnx+y;}intmain(){int(*pf3)(int,int)=Add;printf("%d\n",(*pf3)(2,3));printf("%d\n",pf3(3,5));return0;}输出结果:
58调用语法:
(*指针变量名)(参数列表)(*pf3)(3,5)3. 两段有趣的代码
代码1
(*(void(*)())0)()这段代码的作用是:调用首地址为0位置的子例程(函数)。
拆解步骤
0:这是一个普通的整数0void (*)():这是一个函数指针类型。它表示一个指向没有参数()且没有返回值void的函数的指针。(void (*)())0:这里使用了强制类型转换。将整数0强制转换为上述的函数指针类型。现在,0被编译器视为一个指向地址0的函数指针。*(void (*)())0:对刚才得到的函数指针进行解引用(使用*运算符)。获取地址0处的函数本身。(*(void (*)())0)():在最外层加上(),表示调用这个函数。
总结: 整体含义就是“将地址 0 当作一个无参无返回值的函数,并执行它”。
代码2
void(*signal(int,void(*)(int)))(int);这段代码的作用是:声明一个名为signal的函数。
拆解步骤
*signal(int , void(*)(int)):signal后面跟着括号,说明signal是一个函数。它接收两个参数:int类型 和void(*)(int)类型.这是一个函数指针,指向一个接收int参数且返回void的函数.*signal(...):signal函数调用的前面有一个*,说明signal函数的返回值是一个指针。void (* ... )(int):将内部的signal(...)作为一个整体来看,最外层的结构是void (*)(int)。这说明signal函数返回的那个指针,指向的是一个接收int参数且返回void的函数
总结:signal是一个函数,它接收一个int整型和函数指针,并且它的返回值也是函数指针
4. typedef关键字
typedef是用来类型重命名的,可以将复杂的类型,简单化。
比如,你觉得unsigned int写起来不方便,如果能写成uint就方便多了,那么我们可以使用:
typedefunsignedintuint;如果是指针类型,则可以这样写:
typedefint*parr_t;但是对于数组指针和函数指针稍微有点区别:
比如我们有数组指针类型int(*)[5],需要重命名为parr_t,那可以这样写:
typedefint(*parr_t)[5];//新的类型名必须在*的右边函数指针类型的重命名也是⼀样的,比如,将void(*)(int)类型重命名为pfun_t,就可以这样写:
typedefvoid(*pfun_t)(int);//新的类型名必须在*的右边如果要简化代码2,可以这样写:
typedefvoid(*pfun_t)(int);pfun_tsignal(int,pfun_t);五、函数指针数组
数组是一个存放相同类型数据的存储空间,我们已经学习了指针数组,
比如:
int*arr[10];//数组的每个元素都是int*那接下来我们要将函数的地址存入到一个数组,这个数组就叫函数指针数组
下面是它的定义:
int(*parr[10])();parr1先和[]结合,数组元素类型是int(*)()类型的函数指针
六、转移表
函数指针数组的用途:转移表
举例:计算器的一般实现:
#include<stdio.h>intadd(inta,intb){returna+b;}intsub(inta,intb){returna-b;}intmul(inta,intb){returna*b;}intdiv(inta,intb){returna/b;}intmain(){intx,y;intinput=1;intret=0;do{printf("*********************************\n");printf(" 1:add 2:sub \n");printf(" 3:mul 4:div \n");printf(" 0:exit \n");printf("*********************************\n");printf("请选择: ");scanf("%d",&input);switch(input){case1:printf("输入操作数: ");scanf("%d %d",&x,&y);ret=add(x,y);printf("ret = %d\n",ret);break;case2:printf("输入操作数: ");scanf("%d %d",&x,&y);ret=sub(x,y);printf("ret = %d\n",ret);break;case3:printf("输入操作数: ");scanf("%d %d",&x,&y);ret=mul(x,y);printf("ret = %d\n",ret);break;case4:printf("输入操作数: ");scanf("%d %d",&x,&y);ret=div(x,y);printf("ret = %d\n",ret);break;case0:printf("退出程序\n");break;default:printf("选择错误\n");break;}}while(input);return0;}使⽤函数指针数组的实现:
#include<stdio.h>intadd(inta,intb){returna+b;}intsub(inta,intb){returna-b;}intmul(inta,intb){returna*b;}intdiv(inta,intb){returna/b;}intmain(){intx,y;intinput=1;intret=0;int(*p[5])(intx,inty)={0,add,sub,mul,div};//转移表do{printf("*********************************\n");printf(" 1:add 2:sub \n");printf(" 3:mul 4:div \n");printf(" 0:exit \n");printf("*********************************\n");printf("请选择: ");scanf("%d",&input);if((input<=4&&input>=1)){printf("输入操作数: ");scanf("%d %d",&x,&y);ret=(*p[input])(x,y);printf("ret = %d\n",ret);}elseif(input==0){printf("退出计算器\n");}else{printf("输入有误\n ");}}while(input);return0;}- 1.消除冗余的分支结构
在代码2中,因为add,sub,mul,div这四个函数的参数类型和返回值类型完全一致,我们可以把它们放进同一个数组里.
int(*p[5])(intx,inty)={0,add,sub,mul,div};- 2.利用数据对齐索引(占位技巧)
int(*p[5])(intx,inty)={0,add,sub,mul,div};因为用户的输入(input)是从 1 开始对应add的,而数组下标是从0开始。通过在首位放置一个0进行占位,使得输入的数字直接等于对应的数组下标(例如input == 1对应p[1]即add),实现了用户输入与内存地址的直接映射。
- 3.统一调度接口
代码将原本分散在各个 case 中的调用逻辑,收拢为单行表达式:
ret=(*p[input])(x,y);无论用户选择加、减、乘、除,执行流都经过这一条核心指令。
总结
以上就是本篇博客的核心内容。本文介绍了字符指针指向字符串的用法,区分了指针数组与数组指针,揭示了二维数组传参的本质是指向一维数组的地址,并讲解了函数指针的创建、使用及两段经典代码的含义,最后通过函数指针数组实现转移表,优化了计算器的分支结构。掌握这些,您离精通指针又近了一大步。
