C语言指针系列(四):字符指针、数组指针与函数指针数组
文章目录前言一、字符指针变量二、数组指针变量1.数组指针变量是什么2.数组指针变量怎么初始化三、二维数组传参的本质四、函数指针变量1. 函数指针变量的创建2. 函数指针变量的使用3. 两段有趣的代码4. typedef关键字五、函数指针数组六、转移表总结前言在掌握了指针的基本概念、const限定与安全使用后本篇将进一步探索指针在字符串、数组和函数中的高级应用。我们将学习字符指针变量的两种使用方式理解数组指针的本质与初始化剖析二维数组传参的底层机制并逐步深入函数指针、函数指针数组以及利用转移表实现计算器。这些内容将帮助您真正理解“指针是C语言灵魂”这句话的含义。一、字符指针变量在指针的类型中我们通常知道一种指针类型叫字符指针char*使用方法如下这是单个字符的情况intmain(){charchw;char*pcch;*pcw;return0;}还有一种使用方法,这里char*指针作为字符串的首地址intmain(){constchar*pstrhello 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指向的数组元素类型三、二维数组传参的本质有了对数组指针的初步理解,我们就可以学习二维数组传参的本质了.过去我们有一个二维数组需要传参给一个函数时,我们是这样写的:#includestdio.hvoidtest(inta[3][5],intr,intc){inti0;intj0;for(i0;ir;i){for(j0;jc;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].那么二维数组传参本质也是传地址,传的是第一行这个一维数组的地址,那么传参形式也可以写成指针形式.如下:#includestdio.hvoidtest(int(*p)[5],intr,intc){inti0;intj0;for(i0;ir;i){for(j0;jc;j){printf(%d ,*(*(pi)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. 函数指针变量的创建那么什么是函数指针变量呢?根据前面的学习,我们可以大致类比知道:函数指针变量存放的是函数的地址,是一个能指向函数的指针.那么函数是否有地址呢?我们来做个测试:#includestdio.hvoidtest(){printf(hehe\n);}intmain(){printf(test: %p\n,test);printf(test: %p\n,test);return0;}打印结果test:005913CAtest:005913CA这里能打印出结果,说明函数是有地址的,函数名就是函数的地址,我们也可以通过函数名的方式获得函数的地址.如果我们要将函数地址存起来,那就得创建函数指针变量,其实函数指针变量写法和数组指针非常类似,如下:voidtest(){printf(hehe\n);}void(*pf1)()test;void(*pf2)()test;intAdd(intx,inty){returnxy;}int(*pf3)(int,int)Add;int(*pf3)(intx,inty)Add;//x和y写上或者省略都是可以的函数指针类型解析int(*pf3)(intx,inty)||-------------|||||pf3指向函数的参数类型和个数|函数指针的变量名 pf3指向函数的返回类型2. 函数指针变量的使用我们可以通过函数指针调用函数#includestdio.hintAdd(intx,inty){returnxy;}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 当作一个无参无返回值的函数并执行它”。代码2void(*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(*)()类型的函数指针六、转移表函数指针数组的用途转移表举例:计算器的一般实现:#includestdio.hintadd(inta,intb){returnab;}intsub(inta,intb){returna-b;}intmul(inta,intb){returna*b;}intdiv(inta,intb){returna/b;}intmain(){intx,y;intinput1;intret0;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);retadd(x,y);printf(ret %d\n,ret);break;case2:printf(输入操作数: );scanf(%d %d,x,y);retsub(x,y);printf(ret %d\n,ret);break;case3:printf(输入操作数: );scanf(%d %d,x,y);retmul(x,y);printf(ret %d\n,ret);break;case4:printf(输入操作数: );scanf(%d %d,x,y);retdiv(x,y);printf(ret %d\n,ret);break;case0:printf(退出程序\n);break;default:printf(选择错误\n);break;}}while(input);return0;}使⽤函数指针数组的实现#includestdio.hintadd(inta,intb){returnab;}intsub(inta,intb){returna-b;}intmul(inta,intb){returna*b;}intdiv(inta,intb){returna/b;}intmain(){intx,y;intinput1;intret0;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((input4input1)){printf(输入操作数: );scanf(%d %d,x,y);ret(*p[input])(x,y);printf(ret %d\n,ret);}elseif(input0){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);无论用户选择加、减、乘、除执行流都经过这一条核心指令。总结以上就是本篇博客的核心内容。本文介绍了字符指针指向字符串的用法区分了指针数组与数组指针揭示了二维数组传参的本质是指向一维数组的地址并讲解了函数指针的创建、使用及两段经典代码的含义最后通过函数指针数组实现转移表优化了计算器的分支结构。掌握这些您离精通指针又近了一大步。