当前位置: 首页 > news >正文

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:这是一个普通的整数0
  • void (*)():这是一个函数指针类型。它表示一个指向没有参数()且没有返回值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);

无论用户选择加、减、乘、除,执行流都经过这一条核心指令。


总结

以上就是本篇博客的核心内容。本文介绍了字符指针指向字符串的用法,区分了指针数组与数组指针,揭示了二维数组传参的本质是指向一维数组的地址,并讲解了函数指针的创建、使用及两段经典代码的含义,最后通过函数指针数组实现转移表,优化了计算器的分支结构。掌握这些,您离精通指针又近了一大步。

http://www.cnnetsun.cn/news/2489027.html

相关文章:

  • 别急着升级Android Studio!手把手教你降级AGP 8.3.0-alpha01到8.1.3,解决版本不兼容报错
  • 浏览器里的微信:当网页版不再只是传说
  • 终极指南:30天重置JetBrains IDE试用期的完整解决方案
  • 国内使用Claude Code免登录使用企业级功能,实现模型全兼容
  • 开源智慧树刷课插件:三分钟搞定自动学习,让在线课程不再烦人
  • Fansly Downloader:3分钟掌握离线收藏创作者内容的完整解决方案
  • 30天学会AI工程师|Day 15:当工具不止一个时,AI 工程的问题就不只是“能不能用”
  • 快速安装 Ollama三种方法(本地模型管理器)
  • 从模型文件到孪生场景:一个Three.js三维模型管理系统的完整产品化思考
  • 别再用错电位器了!聊聊那个带‘神秘第四脚’的电动双联电位器(附Python仿真)
  • 论文写得像流水账?资深教授推荐这几个AI写作辅助软件
  • 深入eDP协议栈:从PSR SDP发送到Main Link开关,一次搞懂屏幕自刷新的完整信令流程
  • Scroll Reverser:彻底解决Mac设备滚动方向冲突的终极方案
  • 机器学习神经网络激活函数知识点选型:从ReLU到Sigmoid全解析
  • 如何在macOS上免费导出微信聊天记录:WeChatExporter完整指南
  • 从ICM42688P到MPU6000:详解Betaflight/iNav飞控中那些‘奇怪’的IMU旋转配置
  • 发票OCR识别总是失败?一文解决90%的常见问题(附Python/Java调试指南)
  • 百度网盘提取码一键获取:3分钟学会的免费智能查询方案
  • 文字识别提取工具怎么选?2026 年免费和付费工具完整测评对比
  • C语言知识点与题库
  • 从零到一:手把手教你用ESP32和Arduino IDE配置BLE的GAP广播与GATT服务
  • 【为风光储一体化系统注入精准“心跳”的隐形力量】
  • InfluxDB Studio终极指南:免费图形化管理InfluxDB的完整解决方案
  • 告别调参玄学:在ISPRS Vaihingen数据集上复现Swin-UNet分割模型的完整流程与避坑指南
  • 新手避坑指南:在Windows上从零配置Xray被动扫描环境(含证书安装与浏览器代理设置)
  • 龙芯2K0500核心板开发实战:从硬件设计到Linux系统构建
  • 快速上手ncmdumpGUI:3步解锁网易云音乐NCM文件,免费畅享高品质音乐
  • 在RK3588开发板上折腾Qt 5.15.0带OpenGL ES2:一次本地编译的完整踩坑与配置实录
  • 从按键消抖到I2C通信:手把手拆解STM32 HAL库GPIO的8个核心函数实战
  • 用STM32C8T6做个智能衣柜,除了温湿度还能语音和蓝牙控制(附完整代码和PCB)