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

C语言指针入门到理解:一篇文章系统梳理指针核心知识(3)

C语言指针入门到理解:一篇文章系统梳理指针核心知识(3)

前两篇文字我们已经把指针的基础和数组相关内容系统梳理过了,比如:

  • 指针的本质
  • 指针和数组的关系
  • 一维数组、二维数组的传参
  • 二级指针
  • 指针数组

这一篇继续往下走,进入指针相关的更进一步的内容:

  • 字符指针变量到底存的是什么?
  • 数组指针和指针数组怎么区分?
  • 二维数组传参为什么可以写成指针形式?
  • 函数指针到底是什么?
  • 函数指针数组有什么实际用途?
  • 什么是转移表?

这部分是 C 语言指针体系里看上去比较困难,但实际上只要抓住“类型决定意义”这条主线,其实并不难。


一、字符指针变量:它存的不是字符串本身

我们先看一个最常见的字符指针写法:

charch='w';char*pc=&ch;*pc='w';

这个很好理解:
pc是一个字符指针,里面存的是字符变量ch的地址。

但很多同学真正困惑的是下面这种写法:

constchar*pstr="hello world.";printf("%s\n",pstr);

很多人第一眼会误以为:

是把字符串"hello world."整体放进了指针变量pstr

其实不是。

这句代码的本质是:

把字符串常量"hello world."首字符地址,存放到字符指针pstr中。

也就是说,pstr里存放的是首字符'h'的地址,而不是整个字符串对象本身。

这里为什么要写const char*

因为字符串字面量通常存放在常量区,不应该通过指针去修改它,所以更合理的写法是:

constchar*pstr="hello bit.";

如果这里无法理解的话请联想一些常量的赋值是违规的操作,例如:

3=5//这种操作

二、一个经典面试题:为什么有的字符串地址相同,有的不同

看下面这段代码:

#include<stdio.h>intmain(){charstr1[]="hello world.";charstr2[]="hello world.";constchar*str3="hello world.";constchar*str4="hello world.";if(str1==str2)printf("str1 and str2 are same\n");elseprintf("str1 and str2 are not same\n");if(str3==str4)printf("str3 and str4 are same\n");elseprintf("str3 and str4 are not same\n");return0;}

运行结果通常是:

str1 and str2 are not same str3 and str4 are same

为什么会这样?

1.str1str2为什么不同

因为:

charstr1[]="hello world.";charstr2[]="hello world.";

这是用同样的内容初始化了两个不同的数组
数组初始化时,会各自开辟独立空间,所以str1str2不是同一块内存。

2.str3str4为什么相同

因为:

constchar*str3="hello world.";constchar*str4="hello world.";

这里不是创建数组,而是让两个指针都去指向同一个字符串常量
编译器通常会把相同的字符串常量放到同一块常量区内存中,所以str3str4很可能相等。

ps:这也同样可以进一步佐证前文为何要加const,因为这是一个常量,所以我们不希望它被修改。

总结

  • 数组名比较的是各自数组首元素地址
  • 字符指针比较的是它们指向的常量字符串地址

所以这个例子非常适合理解:

“字符数组”和“字符指针”虽然都能处理字符串,但底层模型并不一样。


三、数组指针变量:它是指针,不是数组

这一块是最容易和“指针数组”混掉的地方。

先看两个定义:

int*p1[10];int(*p2)[10];

很多人会懵:到底哪个是数组指针?

答案是:

int(*p2)[10];

才是数组指针变量


四、什么是数组指针

数组指针,本质上是:

一个指针变量,这个指针指向的是数组。

例如:

int(*p)[10];

怎么理解这句?

先看优先级

因为[]的优先级高于*,所以必须加括号:

(*p)

这表示先说明p是一个指针。

然后再看:

(*p)[10]

表示p指向一个有 10 个元素的数组。

如果数组元素类型是int,那最终它的含义就是:

p是一个指针,指向一个int[10]类型的数组。


五、数组指针怎么初始化

既然数组指针是“指向数组的指针”,那它存的就应该是数组的地址

而数组的地址怎么取?

用:

&数组名

例如:

intarr[10]={0};int(*p)[10]=&arr;

这里:

  • &arr是整个数组的地址
  • p是数组指针变量
  • p&arr类型一致

数组指针拆解理解

int(*p)[10]=&arr;

可以拆成三层:

  • p是变量名
  • *p说明p是指针
  • (*p)[10]说明它指向一个有 10 个int元素的数组

所以记忆方法很简单:

数组指针是指针,指针数组是数组。


六、二维数组传参的本质:传的是第一行的地址

先看一个常见写法:

#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;}

这段代码大家通常都见过,但很多人并没有真正理解:

二维数组传参,传过去的到底是什么?


七、二维数组本质上是什么

二维数组可以理解为:

每个元素都是一维数组的数组

比如:

intarr[3][5];

它可以理解成:

  • 整个数组有 3 行
  • 每一行是一个int[5]的一维数组

也就是说,二维数组的首元素不是一个int,而是第一行这个一维数组

所以根据“数组名表示首元素地址”的规则:

arr

表示的不是单个int的地址,而是第一行的地址

第一行的类型是:

int[5]

那么第一行地址的类型就是:

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;}

这里:

  • p + i表示走到第i
  • *(p + i)表示第i行这个一维数组
  • *(p + i) + j表示第i行第j个元素地址
  • *(*(p + i) + j)才是真正的元素值

ps:这里如果忘记了*操作符的作用请回顾我上篇文章

所以结论是:

二维数组传参时,形参既可以写成:

inta[3][5]

也可以写成:

int(*p)[5]

但无论哪种写法,本质上都是在接收第一行的地址


九、函数指针变量:它存放的是函数地址

前面学了很多种指针:

  • 整型指针:存整型变量地址
  • 字符指针:存字符地址
  • 数组指针:存数组地址

那自然就会想到:

能不能有一种指针,专门用来存函数地址?

答案当然是可以,这就是函数指针变量


十、函数真的有地址吗?

先看例子:

#include<stdio.h>voidtest(){printf("hehe\n");}intmain(){printf("test: %p\n",test);printf("&test: %p\n",&test);return0;}

你会发现:

  • test
  • &test

都能打印出函数地址,而且结果通常一样。

这说明:

函数名本身就表示函数地址,也可以用&函数名来取地址。


十一、函数指针怎么定义

例如有这样一个函数:

intAdd(intx,inty){returnx+y;}

那对应的函数指针可以写成:

int(*pf)(int,int)=Add;

也可以写成:

int(*pf)(int,int)=&Add;

这两种写法都可以。

如何理解这个定义

int(*pf)(int,int)

拆开看:

  • pf是变量名
  • *pf说明它是一个指针
  • (int, int)说明它指向的函数参数是两个int
  • 最前面的int说明该函数返回值类型是int

所以这句的完整含义是:

pf是一个函数指针,指向的函数形参是(int, int),返回值类型是int


十二、函数指针怎么调用函数

有了函数指针之后,可以通过它调用对应函数:

#include<stdio.h>intAdd(intx,inty){returnx+y;}intmain(){int(*pf)(int,int)=Add;printf("%d\n",(*pf)(2,3));printf("%d\n",pf(3,5));return0;}

输出:

58

这里两种调用方式都对:

(*pf)(2,3);pf(3,5);

因为对于函数指针来说,前面的* 在写的时候可以省略,所以平时更常写的是第二种,简洁一些。


十三、复杂函数指针看不懂怎么办?用 typedef

函数指针类型一复杂,代码可读性会迅速下降。

比如说:

void(*signal(int,void(*)(int)))(int);

这种写法一眼看过去确实头大。

这时候最好的解决方案就是:

用 typedef 给复杂类型起别名

例如:

typedefvoid(*pfun_t)(int);typedeflonglongLL;

这表示把:

void(*)(int)

重命名为:

pfun_t

那上面那句复杂定义就能简化为:

pfun_tsignal(int,pfun_t);

可读性一下就上来了。

除了函数指针,数组指针也可以这样简化

例如:

typedefint(*parr_t)[5];

以后看到parr_t,你就知道它是“指向int[5]的数组指针类型”。


十四、函数指针数组:数组里存的是函数地址

前面学过:

int*arr[10];

这是指针数组,表示数组中每个元素都是int*

那如果数组中每个元素都是“函数指针”,就得到了:

函数指针数组

例如:

int(*parr1[3])();

这个定义中:

  • parr1先和[]结合,说明它是数组
  • 数组元素类型是int (*)(),也就是函数指针

所以parr1是一个函数指针数组


十五、函数指针数组最典型的用途:转移表

函数指针数组最经典的应用场景,就是转移表

比如我们要实现一个简单计算器:

  • 1:加法
  • 2:减法
  • 3:乘法
  • 4:除法

传统写法通常会用switch-case

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;}

这种写法当然能用,但如果功能越来越多,switch-case会越来越臃肿。

这时就可以用函数指针数组优化。


十六、用函数指针数组实现计算器

#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])(int,int)={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>=1&&input<=4){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;}

这里最关键的一句

int(*p[5])(int,int)={0,add,sub,mul,div};

这就是一个函数指针数组,也就是转移表。

它的含义是:

  • p[1]指向add
  • p[2]指向sub
  • p[3]指向mul
  • p[4]指向div

之后只需要根据用户输入,直接调用:

(*p[input])(x,y);

这样就把“菜单选择 -> 函数跳转”的流程做成了表驱动结构。

这就是所谓的:

转移表

它的优势在于:

  • 结构更清晰
  • 扩展更方便
  • 代码更容易维护

十七、这一篇的几个易错点:

最后把本篇最容易出错的地方集中总结一下。

1. 字符指针不等于字符数组

constchar*p="hello";

这里p里存的是首字符地址,不是把整个字符串“装进了指针”。

2. 相同内容的字符数组不一定地址相同

charstr1[]="abc";charstr2[]="abc";

这是两个不同数组,地址不同。

3. 相同内容的字符串常量指针可能地址相同

constchar*p1="abc";constchar*p2="abc";

它们可能指向同一个常量区字符串。

4. 数组指针是指针,不是数组

int(*p)[10];

p是指针,指向一个int[10]数组。

5. 指针数组是数组,不是指针

int*arr[10];

arr是数组,元素类型是int*

6. 二维数组传参本质上传的是第一行地址

所以形参可以写成:

inta[][5]

也可以写成:

int(*p)[5]

7. 函数指针的关键是先看pf和谁结合

int(*pf)(int,int);

先看(*pf),说明pf是指针;再看后面的参数列表和前面的返回值类型。

8. 函数指针数组的本质还是数组

只是数组里的元素,不再是普通数据,而是“函数地址”。


十八、总结

这一篇我们的主要结论如下

  1. 字符指针存的是字符地址,字符串字面量本质上传递的是首字符地址。
  2. 数组指针是“指向数组的指针”,指针数组是“存放指针的数组”。
  3. 二维数组传参本质上传的是第一行的地址,所以形参可以写成数组指针。
  4. 函数名就是函数地址,函数指针变量就是用来存函数地址的。
  5. 函数指针数组可以构造转移表,让代码从分支驱动变成表驱动,从而更好的维护自己的项目。

实际上在学习指针这一块的时候,有一个很大的诀窍,那就是:

先认清变量是谁,再看它指向什么。

http://www.jsqmd.com/news/653193/

相关文章:

  • AI生成内容署名权与权利归属争议全解(2024最高法典型案例+5类合同条款陷阱预警)
  • 6个值得尝试的Claude Code扩展
  • 基于自指动力学的统一场论:从标准模型到宇宙学特征(世毫九实验室原创理论)
  • 生成式AI服务突然OOM崩溃?7类隐性依赖未追踪导致的级联故障,附可落地的Trace-Span增强模板
  • 如何快速搭建个人AI助手:Open WebUI完整实战指南
  • 一文搞懂近红外光谱学:原理、应用领域与常见问题......
  • 微软 MarkItDown 登顶 GitHub 热榜:108K Star,一键将任意文档转 Markdown,深度拆解它的技术野心
  • 从CVE到CAPEC:漏洞利用模式逆向分析实战(附BurpSuite插件配置)
  • 解锁Bootloader后,你的联想手机还能做什么?Magisk、LSPosed与自定义ROM入门指南
  • GPT-6 正式发布:200 万 Token、性能提升 40%,开发者必看(对比 GPT-5.4)
  • 我差点错过了Codex
  • 目前网站遇到最大的需要解决问题
  • 【8G显存福音】最新TX-2.3-22B-DISTILLED-1.1-VBVR 整合包文生视频、图生视频,支持首尾帧/单图无限时长,50系显卡全适配!
  • 生成式AI落地必过伦理关:SITS2026圆桌披露的5类隐性偏见、4步可审计治理框架及企业级实施模板
  • 2026年靠谱的徐州代理记账靠谱公司推荐 - 品牌宣传支持者
  • 词元时代,Token 如何重塑AI算力交易
  • 深入解析高通QSEE中qsee_stor_write_sectors函数在RPMB分区的安全存储机制
  • 生成式AI伦理治理不能再等下一版政策:SITS2026圆桌强制推荐——所有L3以上AI系统须嵌入实时伦理哨兵模块(开源SDK已上线GitHub Trending Top 1)
  • Ensemble-BioMart实战指南:快速获取非模式物种基因注释信息
  • 面向对象高级(枚举泛型)
  • 零门槛上手:OpenClaw 2.6.2 完整安装与使用教程(含报错解决)
  • AI 个性化推荐算法:重构民宿行业用户决策体验的核心引擎
  • [ecapture] eBPF hook gotls 收包乱序根因分析
  • 宝塔面板结合Docker:一站式网站部署实战指南
  • 别浪费你的SD卡了!实测Surface Pro外置运行Ubuntu:性能调优与避坑全记录
  • 千万级数据处理的架构演进:从瓶颈到突破
  • AI泡沫再现?从业者的理性生存指南
  • 指针进阶:回调函数精髓 + qsort 实战与模拟实现
  • 2026奇点智能技术大会AI翻译系统深度拆解(实时语义锚定技术首次公开)
  • 【限时解密】生成式AI数据回流机制的“暗数据”捕获术:绕过UI层直接抓取用户修正行为、停留热区、撤回序列的3种零侵入方案