资讯动态

C语言指针进阶:const、数组指针与二级指针实战解析

发布时间:2026/10/22 6:16:41 来源:尧图企业网站定制
1. 为什么指针学到第三篇反而觉得前两篇白学了能坚持到C语言指针系列的第三篇说明前面基础部分的底子你已经大致铺开了。可奇怪的是很多人在这个阶段反而比刚入门时更没底声明、取地址、解引用单独拿出来都认识一旦代码里出现const char **、int (*p)[5]这种组合就得捧着书猜半天程序一崩溃第一反应不是查指针而是怀疑编译器出了毛病。C语言指针基础三这篇我想解决的不是指针是什么而是指针为什么能组合出这么多花样。const 加进来该怎么读字符串到底用char*还是char[]二维数组和二级指针是不是同一回事函数里想改外部的指针又该怎么传参如果你被这几个问题卡过这篇文章就是给你准备的。我见过不少学完基础篇的同学术语背得滚瓜烂熟一写真实代码就露怯。这不怪你——指针本来就不是靠背定义能拿下的它是一套需要反复对照内存去理解的心智模型。第三篇的内容就是用几组最容易碰到的组合场景把这些知识点真正拧在一起。下面按四条线展开const 与指针的搭配、字符指针和字符数组的区别、数组指针与指针数组的辨析、二级指针的出现时机。每条线后面都有实际工程里高频出现的例子以及我个人踩过之后总结出来的判读技巧。建议手边备一支笔遇到不明确的声明先在纸上画一画它到底指向哪里。2. const 与指针的四种组合别死背口诀先搞懂锁住谁很多教程喜欢把 const 指针声明总结成口诀背是能背换个场景就忘。我的建议是直接从修饰关系去理解const 始终修饰紧挨着它右侧的东西。当右侧是*时它修饰通过解引用得到的值当右侧是指针变量名时它修饰指针变量本身。看一段最直观的代码int a 10, b 20; const int *p a; // const 修饰 *p即被指数据只读 p b; // 合法p 本身可改 // *p 5; // 非法*p 不能写 int *const q a; // const 修饰 q即指针变量只读 *q 5; // 合法a 被改成 5 // q b; // 非法q 不能指向别处2.1 const 放在星号左边还是右边含义完全相反对齐上面代码看const int *p的 const 在*左边锁住的是解引用后的数据int *const q的 const 在*右边锁住的是指针变量自己。哪怕你什么都不记只要看到 const迅速问一句它右边跟的是什么跟的是*锁数据跟的是变量名锁指针。好多人不知道的是const int *p和int const *p其实完全等价因为把 const 挪到int前面并不会改变修饰关系。真正能改变含义的是 const 和星号之间的相对位置。这个细节在阅读旧代码时特别重要因为不同开发者写 const 的习惯不一样看懂等价的两种形式才不会把同一种声明理解成两回事。2.2 四种组合的判读速查表声明指针本身被指向的数据常见叫法const int *p或int const *p可改只读指向常量的指针int *const p只读可改常量指针const int *const p只读只读指向常量的常量指针int *p可改可改普通指针判读时最忌讳跳跃。先让标识符p站出来看它右边有没有 const再看解引用后的数据左边有没有 const。两次检查完结果就出来了。我平时读复合声明都按这个顺序来宁可慢一点也不靠感觉猜。2.3 const 在工程接口里为什么值得写写小项目时容易觉得 const 可有可无反正自己不会乱改。一旦代码变成多人维护const 的价值立刻显现它把这块数据不允许修改从注释里的口头约定变成了编译器的强制检查。调用方某天试图修改编译阶段就会报错省下大量联调时间。size_t my_strlen(const char *s) { size_t n 0; while (s[n] ! \0) n; return n; }函数内部只需要读取字符参数写成const char *等于给所有调用者一个明确承诺这个函数不会改你的字符串。反过来如果有人想传字符串常量进来这个签名也完全兼容。我在自己的项目里有一条硬性规定凡是函数不需要修改的外部数据参数一律加 const。前期的确多敲几个字符后期能省下一整晚排查谁把数据改坏了的时间。需要提醒的是const 是编译期约束它只拦截直接通过这个指针进行的修改挡不住恶意强转也管不了别的指针从旁边改。它解决的是工程规范问题不是安全墙。理解到这一层就比单纯背口诀的人高出一截了。3. 字符串这块char* 和 char[] 的差别能毁掉一整天不少初学者以为字符串就是char*随手写char *str hello。两种写法在很多操作上确实一模一样printf 能打印、strlen 能测长、下标能取字符。但它们的本质差异是 C 语言里最常触发野指针和段错误的隐藏陷阱之一。3.1 同样写着 hello内存里的命运完全不同char s[] hello; // 栈上一个可修改的数组长度 6 字节 char *p hello; // 指针变量指向只读的字符串字面量第一条语句定义了一个数组编译器把hello的 6 个字节复制到栈上以后s的使用都发生在那块独立内存上。数组名s被绑定在起始地址你不能写s但可以通过s[0]修改内容。第二条语句定义的只是一个指针变量里面保存的是字符串字面量在内存中的起始地址而字面量在多数系统上放在只读数据段一旦尝试通过p去写轻则没效果重则直接段错误。标准的说法是修改字符串字面量属于未定义行为。未定义的意思不是一定会崩也不一定不崩而是语言标准不保证任何结果。C 标准把字面量当作普通字符数组行为交给实现决定C 则更严格地把它当成 const 字符数组处理。两个语言的保护力度不同但结论一致别去改字面量。3.2 一个让新手头皮发麻的段错误场景很多人排查段错误时怀疑 malloc 用错、怀疑指针越界很少想到问题出在改了一个不该改的字符串常量。我帮 A 同学调试过一个字符串替换函数函数参数是char *origin逻辑很好直接在原字符串上做原地替换。日常传入的都是 malloc 出来的字符串一切正常。某次调用顺手传入一个字符串常量就出现随机崩溃而且崩溃位置还不固定特别难抓。最终定位下来就是函数内部对只读区域做了写操作。把参数类型改成const char *调用处如果还想传可变字符串原本没问题而任何试图修改的路径都会在编译期暴露。这就是 const 和字符串字面量规则叠加在一起的价值——它们不是孤立的语法点而是共同构成一套内存安全边界。3.3 到底该选 char[] 还是 char *我的三条标准判断依据其实很朴素字符串内容需要修改或者生命周期由当前栈管理用数组char s[]。只想保存一个指向某段字符串的指针将来想换一个地方指用char *p。需要一个真正可写的字符串缓冲区先malloc一块内存再往里面拷贝。char *p first; p second; // 合法p 换个地方指 char *buf malloc(1024); strcpy(buf, second); // 这才是可写字符串 free(buf);最后一条特别多人踩坑有人写下char *buf ...然后紧跟着strcpy(buf, ...)结果就是往只读区域写数据。缓冲区一旦需要被修改第一件事就是保证它指向的是一块可写内存而不是字面量地址。这个习惯建立起来之后字符串相关的崩溃能少掉一大半。4. 二维数组传参数为什么是 int (*p)[N]而不是 int **p二维数组的传参问题简直是每届初学者必掉的坑。int *a[5]和int (*a)[5]只差一对括号含义却从5 个指针组成的数组变成了指向一个含 5 个 int 的数组的指针。搞懂这一对才算真正摸到复合声明的门槛。4.1 [] 的优先级高于 *类型就完全不一样关键在运算符优先级[]的优先级高于取内容运算符*。所以没有括号时a先和[5]结合它首先是一个数组每个元素再去乘星号成为指针加了括号后(*a)先结合a首先是一个指针再借用[5]描述它指向的目标。int *a[5]; // 数组5 个元素每个是 int* int (*a)[5]; // 指针一个指针指向含 5 个 int 的数组这两个声明在内存布局上也不同。a[5]是连续存放的 5 个指针变量每个占 8 字节64 位系统总共占 40 字节(*a)[5]只是一个指针变量占 8 字节它指向的那 5 个 int 是另行分配的。画一下草图就非常清楚。4.2 用右左法则读复杂声明遇到更复杂的声明我推荐右左法则从标识符开始向右找[]或()再折返回左边找*一层层剥开。以int (*p)[5]为例从p开始右边是)没有别的向左看是*所以p是指针跳出这层括号p右边是[5]说明这个指针指向一个包含 5 个 int 的数组。再读int (*funcp)(int)funcp右边是)、左边是*是指针跳出括号后右边是(int)表示参数列表整个声明读作指向一个参数为 int、返回值为 int 的函数指针。右左法则应付不了 C 语言里 90% 的复合声明。遇到读不出来的先写出来推一遍而不是赌自己猜得对。读声明和读英文句子一样有固定语序练几次就有肌肉记忆了。4.3 二维数组名退化后的真实类型很多人想当然int arr[3][5]有行有列数组名退化成指针那不就是int*吗于是写成int **p arr。这里有一个必须正视的事实数组名作参数传递时退化的是指向第一个元素的指针。arr的第一个元素不是 int而是一个int[5]数组所以退化成int (*)[5]也就是上一节说的数组指针。那int **到底对应什么它是指针的指针一般用来指向一个int*变量或者指向一个指针数组。int **p想通过p[i][j]双重下标访问数据前提是p[i]拿到的是int*而二维数组arr[i]拿到的是整个int[5]类型不匹配。强行赋值编译器会毫不客气地报警告或报错。标准传参写法是void print_matrix(int (*mat)[5], int rows) { int i, j; for (i 0; i rows; i) { for (j 0; j 5; j) printf(%d , mat[i][j]); printf(\n); } }这里第二维大小 5 必须写清楚因为编译器只有知道每行多长才能算mat[i] j的步长。C99 之后还可以用变长数组写成void print_matrix(int rows, int cols, int mat[rows][cols])本质还是行指针只是把长度一并传进来灵活处理不同列数。4.4 指针数组和数组指针工程上分别用来干什么指针数组最经典的用途是装一组字符串例如菜单项、命令词表const char *cmds[] {add, delete, search, quit};字符串长短不一用二维字符数组会浪费空间还要约定最大长度换成指针数组每个元素只是 8 字节的char*各自指向对应的字符串常量轻巧又直观。遍历时只需要数数组元素个数。数组指针则适合固定形状的数据比如矩阵、图像像素、网格状态。处理这类数据通常要传int (*)[N]让函数清楚一行有多少列。如果行和列都是动态的我个人的建议是干脆用一维数组加手动下标换算idx i * width j很多底层库也偏爱这种紧凑的连续内存布局访问速度还更快。5. 二级指针的存在价值函数想改的不只是值而是指针本身二级指针让不少初学者望而生畏但实际上它的出现条件非常单一当你确实需要修改某个指针变量本身时自然就需要再往上一层把这个指针的地址传进来。5.1 一个经典问题函数里 malloc调用者却拿不到写封装函数时经常想把分配内存这个动作包起来。初学者最容易写成这样void alloc_buf(int *buf) { buf malloc(10 * sizeof(int)); }然后发现调用处buf仍然是 NULL。原因就藏在参数传递里int *buf传进来的是指针的值函数内部重新给buf赋值只是让局部副本指向了新内存调用者的指针变量没有受到任何影响。函数返回后那块新分配的内存既没有地址被保存也没有被释放白白泄漏。想修改调用者的变量本身就必须把这块变量的地址传进来。调用者的变量是指针int*它的地址自然就是int**void alloc_buf(int **buf) { *buf malloc(10 * sizeof(int)); }这里的*buf等价于调用者的那个指针变量。给*buf赋值等于直接修改调用方手里的指针。想通这一点二级指针的窗户纸就捅破了。5.2 链表插入的哨兵模式其实用的是同一个套路链表的头插、删除头节点这类操作也需要二级指针。比如头插一段新节点后链表的头指针本身要变如果函数只传Node *head内部让head指向新节点外面的头指针还是旧地址链表就断了。正确写法是传Node **head在函数内用*head new_node让外部头指针真正更新。单链表的删除、二叉树的指针调整、各种容器结构的修改接口全都是这个模式。可以说二级指针不是 C 语言里独立的神秘语法而是你要修改的东西本身是个指针时自然的推导结果。在判断要不要用二级指针时只问一句这个函数结束之后调用者的指针变量需不需要变需要就传地址不需要传值就够了。5.3 argv 这个每天都见的二级指针int main(int argc, char **argv)就是最顺手的二级指针实例。argv的类型是char**它指向一个指针数组的首元素那个指针数组的每个元素是char*装着启动参数字符串。平时遍历argv[1]到argv[argc-1]本质上就是在遍历一个字符串列表。int main(int argc, char **argv) { int i; for (i 1; i argc; i) printf(%s\n, argv[i]); return 0; }很多人分不清char *argv[]和char **argv其实在函数参数列表里二者等价因为数组形参会退化成指针。char *argv[]作为形参时真实类型就是char **argv。二级指针藏在这样一层等价关系后面所以看起来抽象实际每天都在用。5.4 解引用层级最容易看花眼使用二级指针前先在纸上分清两层p是二级指针*p是一级指针**p才是真正的数据。p的类型是int***逐层往下传每一次解引用就向数据靠近一步。出现类型不匹配的报错时多半是解引用层级没有对齐。我调试这类问题的习惯是在可疑代码前插临时打印printf(p%p *p%p **p%d\n, (void*)p, (void*)*p, **p);一次看清三个量。如果*p是0x0说明第一层还没初始化如果**p突然变成乱码多半是第二层越界。指针调试本质就是在打印地址和值的过程中把脑中的内存模型一点点校准。这一步熟练之后二级指针就不再是玄学而只是又一个需要小心对待的普通层级。6. 用几道判断题给这篇收个尾你的指针直觉准不准讲完这些概念我准备了六道小判断题全部围绕这篇的核心内容。每一道我都亲眼见过有人答错而且错法还各有各的精彩。建议你先别急着看答案拿纸写下自己的判断再往下对答案。答错不丢人答错之后的纠正往往比全对更有价值。// 题1 const int a 1; int *p a; // 能通过编译吗 // 题2 char s[] hello; char *p s; p; // 合法吗 printf(%c\n, *p); // 输出什么 // 题3 char *str hello; str[0] H; // 安全吗 // 题4 int a[3] {1, 2, 3}; a; // 合法吗 // 题5 int *p[5]; // p 到底是什么 int (*q)[5]; // q 到底是什么 // 题6 void set_ptr(int *p) { p NULL; } int main(void) { int v 42; int *x v; set_ptr(x); // 这里 x 等于什么 }题1答案不能。int*指向const int会丢弃 const 限定符编译器至少给出警告严谨的工程环境会直接报错。正确写法是const int *p a;也就是第 2 节讲的锁数据。题2答案合法。p让指针移动到数组第二个元素输出是字符e。数组名s本身不能自增但它赋值给指针后指针是自由变量移动完全合法。题3答案不安全属于未定义行为。str指向字符串字面量修改str[0]就是在写只读区域。正确做法是定义成char str[] hello;让编译器在栈上复制一份可写副本。题4答案不合法。数组名不是可修改的左值a编译不过。记住数组名在大多数表达式中会退化成指针但它本身并不是变量不能被重新赋值。题5答案p是指针数组包含 5 个int*元素共 40 字节q是数组指针指向一个包含 5 个 int 的数组。区别全部来自[]优先级高于*加不加括号决定先和谁结合。题6答案x仍然是v函数内p NULL修改的只是形参副本。想让外部x变成 NULL必须把参数写成int **p函数内执行*p NULL。这正是第 5 节的核心。如果你把上面六道题都弄明白了这篇的基础就已经内化得差不多。我始终觉得指针需要的不只是记忆而是一套稳定的判断流程先分清它是数组还是指针再确认 const 锁住的是哪一层最后看传参时地址走了几层。多年之后回看 C 语言里的指针真正困难的从来不是语法而是识别这个地址到底能不能改、这块内存到底归谁管。写代码之前多问自己这两个问题比背一百个技巧都管用。基础三走到这里下一步无论是深入函数指针还是研究链表结构脚下这块地基都已经踩实了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑