文章目录
- 字符指针变量
- 数组指针变量
- 数组指针变量是什么?
- 数组指针变量怎么初始化
- 二维数组与指针
- 二维数组传参的本质
- 二维数组中的暗含的退化
- 函数指针变量
- 函数指针变量的创建
- &Add和Add
- 函数指针变量的使用
- 函数指针是否要\*才能调用函数?
- 两段代码帮你更好理解函数指针
- typedef 关键字
- 函数指针数组
- 函数指针数组的使用场景:转移表
这里是think的博客
希望可以一起交流知识,一起think
今天我们来学习指针(4)吧
一起来think吧
今天我们将迎来指针模块里难度偏高的知识点,放平心态,一起稳步开启学习之旅。
字符指针变量
指针中有一种指针是字符指针,即char*,
一般这样使用:
intmain(){charch='w';char*pc=&ch;*pc='a';return0;}但是还有一种使用方法:
intmain(){constchar*pstr="hello world.";printf("%s\n",pstr);很多人下意识认为这里是把一个字符串放到pstr指针变量,实际上不是的,因为指针中只能存放一个地址,那么这个"hello world."存放在哪里呢?
实际上它存放在内存中的常量区中,这个字符串是常量字符串,pstr拿到的是这个常量字符串的首元素地址。
接下来又会有一个问题,字符指针和字符数组有什么差别呢?,接下来我们通过一个例子来明晰它们之间的差别。
#include<stdio.h>intmain(){charstr1[]="hello bit.";charstr2[]="hello bit.";constchar*str3="hello bit.";constchar*str4="hello bit.";if(str1==str2)printf("str1 and str2 are same\n");//1elseprintf("str1 and str2 are not same\n");///2if(str3==str4)printf("str3 and str4 are same\n");//3elseprintf("str3 and str4 are not same\n");//4return0;}答案就是2,4。
我们来分析一下,首先str1和str2是两个不同的字符数组,其中在编译期间编译器就用常量字符串"hello bit."来初始化字符数组了,而这两个字符数组在内存中栈区的所开辟的空间是不同的,那么它们首元素的地址也不一样。
其中,常量字符串如果内容是一样的,之后只会在常量区中存一份,因为常量区里的数据是不能修改的,故而str3和str4存的"hello bit."的首元素地址是一样的。
数组指针变量
数组指针变量是什么?
我们讲过一个词的本质是最后一个那个名词,同样类比可知,字符指针和整型指针都是指针,字符指针存放的是字符的地址,指向的内容是字符,整型指针存放的整型的地址,指向的内容是整型,那么数组指针也是指针,存放的是数组的地址,指向的内容是数组。
怎么定义数组指针变量的类型?
int (*p2)[10];
分析一下结构,*说明的是p2是指针变量,后面的[10]说明这个指针变量指向的是一个有10个元素的数组,其中int说明这10个元素都是整型
我们对比一下和指针数组对比一下,int * p1[10];
分析一下,[ ]的优先级是比*高的,所以p1先与[ ]结合,说明这是一个有10个元素的数组,然后再到前面的,int*,说明了数组元素的类型是整型指针。
所以我们明白了为什么数组指针要加(),就是因为[ ]优先级高,为保证*先与p2结合,先确定它的本质是指针,确定了本质后,其他的都是它的属性了。
数组指针变量怎么初始化
&arr的时候,arr不会退化,它的意义依旧是整个数组,那么&后就得到了整个数组的地址,用&arr初始化即可
int arr[10] = {0};
int(*p)[10] = &arr;
VS中这个的调试中类型是int[10]*,是优化过的,实际上的数组指针的类型是去变量名后剩下的部分,即int(*)[10]。
二维数组与指针
二维数组传参的本质
#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;}arr我们知道是首元素的地址,而在二维数组又可以看作每个元素是一维数组的数组,所以二维数组的首元素就是第一行的一维数组,所以首元素的地址就是一维数组的地址。
而数组指针就是指向一维数组的指针,像上面的那个例子,arr的类型就是
int(*)[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;}这就是二维数组传参的本质,传的是一个数组指针。
二维数组中的暗含的退化
二维数组中化为指针的本质的时候其实是有退化现象存在的,如果理解了这个现象,也就可以很轻松理解二维数组的索引了。
我们知道
arr[i][j]就是*(*(arr+i)+j),那么其中arr就是第一行(下标为0)一维数组的地址,那么加i就指向了下标为i的那一行数组,那么解引用就得到了对应的那一行数组,此时就退化为了那一行数组的首元素地址,那么接下来的操作就是一维数组中的操作了。
其中退不退化看表达式的逻辑是需要整个数组还是首元素地址,明显这里需要首元素地址继续去找元素的值。
函数指针变量
函数指针变量的创建
#include<stdio.h>voidtest(){printf("hehe\n");}intmain(){printf("test: %p\n",test);printf("&test: %p\n",&test);return0;}我们发现test和&test都是地址,说明函数是有地址的,并且两个地址是一样的,说明取函数地址可取&也可以不取&。
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写上或者省略都是可以的我们知道函数指针就是指向函数的指针,其中类比数组指针,我们可以知道函数指针的写法,中间有个*来说明pf1是指针,后面写函数形参类型(变量可以不写),前面写返回值。
函数指针变量的类型就是去掉变量名,如:int (*) (int x, int y),函数类型是int (int x,int y)。
&Add和Add
我们知道&Add和Add都可以得到函数的地址,有&的时候Add是整个函数的意思,&后就得到了整个函数的地址,
而为什么单独的Add都可以得到地址呢?
可以类比到数组中退化,但是在函数中我们一般会说这是隐式转换(我们口头也说是退化,但是不是很准确),我们会说函数隐式转换为函数指针,也是可以类比数组知道它的例外情况(即不隐式转换的),其中不能sizeof(Add),因为默认函数是没有体积,有些编译器为了防止报错会设置函数体积默认为1。
那么编译器是怎么实现这个隐式转换的?
虽然写的是 Add,但在编译阶段的表达式语义分析中,Add 在大多数情况下会自动转换为函数指针,其效果等价于 &Add。
函数指针变量的使用
#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;}答案是5,8。
函数指针是否要*才能调用函数?
很明显上面一个例子中,函数指针是否解引用都是可以调用函数的,那么它们的逻辑是一样的吗?
并不是的,我们看到pf3是函数指针,解引用调用了函数,看起来非常符合我们的惯性,因为整型,字符指针解引用可以找到对应的值,并且函数指针解引用就是整个函数,整个函数去调用这个函数好像合理。
实际上并不是,如果看过我写的函数栈帧的创建和销毁 的话,你会知道函数的调用实际上就是call 函数的地址就可以跳过去执行指令了,所以这里我们调用函数只需要函数指针(地址)即可,那么pf3直接调用是正确的。
那么对pf3*之后为什么还能调用?不是应该*之后就没有函数地址了吗?
实际上编译器会进行隐式转换,把函数转换为函数指针,所以这样的代码也是可以的,(***pf3)(2, 3),语法上我们认为是这样的,*之后编译器将函数转换为函数指针,然后又*在转换,一直循环转换,但是实际执行中编译器很聪明,有*的时候它会直接会忽略,一直保持函数指针这个意义。
两段代码帮你更好理解函数指针
(*(void (*)())0)();
我们会发现0前面有个括号,里面有个函数指针变量的类型,很显然这里是强制类型转换,这里的意思就是将0强制类型转换为这个函数指针类型,那么0这个地址指向一个函数,表示call 0这个地址处可以调用类型为void ()的函数。
我们看到*这个地址,表层意义上我们拿到了函数,然后去调用了这个函数(实际上去看我上面写的那个函数指针是否要*才能调用函数?就知道了)
void ( *signal(int , void(*)(int)) )(int);
我们先来看里面那一部分,显然
signal(int , void(*)(int))中,void(*)(int)是一个函数栈帧,其中显然正在声明一个函数,那么声明还要有返回值,那么其中剩下的void(*)(int)就是返回值
由此又可以引出一个知识点,在函数声明或者定义的时候,如果返回值是函数指针类型,那么变量名和参数要写在*旁边,并且要用()括起,不能写出这样,这是语法规定的,
void(*)(int) signal(int , void(*)(int)),可以类比函数指针来理解记忆。
typedef 关键字
但是这样不太好看,并且使用的时候也不太好理解,有什么解决办法吗?
typedef unsigned int uint;
//将unsigned int 重命名为uinttypedef int(*parr_t)[5];
//新的类型名必须在*的右边typedef void(*pfun_t)(int);
//新的类型名必须在*的右边
typedef就成功的补救了理解困难的问题
typedef void(*pfun_t)(int);pfun_t signal(int, pfun_t);
函数指针数组
同样的函数指针数组就是数组,里面存的元素是函数指针。
定义形式:
int (*parr1[3])();
经过了多轮输入,我们知道只要有…(*)…这种形式的,*旁边一定是有变量名的其他附带在变量名周围的都要写在()内部,哪怕返回值是函数指针,也要把变量名和附带的参数写在括号的的*旁边。
函数指针数组的使用场景:转移表
计算器的一般实现:
#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);printf("输入操作数:");scanf("%d %d",&x,&y);switch(input){case1:ret=add(x,y);break;case2:ret=sub(x,y);break;case3:ret=mul(x,y);break;case4:ret=div(x,y);break;case0:printf("退出程序\n");break;default:printf("选择错误\n");break;}printf("ret = %d\n",ret);}while(input);return0;}我们发现这个代码其中一连串的ret=…(x,y)和case1,2都是相似重复代码,是可以省略不要的。
还有一点是最重要的,就是这个代码可维护性太差了,如果以后要加&,|等等运算符呢?那么还要再往下写,越写越长,这样是不行的。
#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;}这里有一个好处就是直接通过下标去调用对应的函数而不是说重复写固定的函数,方便后期维护。
转移表中的表就是函数指针数组,数组将函数指针统一管理成表,转移就是jump,所以可以理解为跳转,从mian函数通过下标索引转移,跳转到add,sub函数。