news 2026/9/1 21:48:40

C语言指针常量与常量指针:从声明解析到实战应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言指针常量与常量指针:从声明解析到实战应用

1. 先搞清楚“指针常量”和“常量指针”到底卡住了谁

如果你在写C语言代码时,看到const int *pint * const p就发懵,分不清哪个能改指针、哪个能改数据,那你不是一个人。这几乎是每个C语言学习者都会卡住的一个点,而且一卡住,后面涉及到函数参数传递、字符串处理、结构体成员访问时,错误就会连环出现。

很多人试图靠“const离谁近”或者死记硬背口诀来区分,但一到实际代码里,稍微变个形式就又糊涂了。其实,区分它们的关键,不是去记两句话,而是理解声明语句的阅读顺序const修饰的到底是谁。一旦掌握这个核心,你就能自己推导出任何复杂指针声明中的读写权限。

这篇文章就是帮你彻底解决这个问题的。我会先带你用最直观的方法拆解声明,然后通过大量代码示例,让你看清楚在不同场景下(比如函数参数、数组、多级指针)它们的行为差异,最后总结出绝对不会错的判断流程。无论你是正在准备考试、面试,还是写项目时被编译器报错困扰,看完都能立刻上手用起来。

2. 核心原则:从标识符开始,向右看再向左看

要理解const int *pint * const p,必须抛弃从左到右的阅读习惯。C语言声明有一套固定的解析规则,对于指针和const组合,最有效的方法是“螺旋法则”的简化版:从变量名(标识符)开始,先向右看,再向左看

2.1 拆解“常量指针”(Pointer to Constant)

我们先看第一种:const int *p;。很多人叫它“常量指针”,更准确的叫法是“指向常量的指针”。

  1. 找到变量名p
  2. 向右看:看到*,说明p是一个指针。
  3. 继续向右看:右边没东西了(遇到分号)。
  4. 向左看:看到int,说明指针指向的是int类型。
  5. 继续向左看:看到const,这个const修饰的是它左边的int

所以,const int *p的完整意思是:p是一个指针,它指向一个const int(整型常量)。核心限制在于“指向的数据”是常量

这意味着什么?

  • p本身(这个指针变量,即它存储的地址值)是可以改变的,你可以让它指向别的整型变量。
  • 但是,你不能通过p这个指针,去修改它所指向的那个内存地址里存储的值。因为编译器认为它指向的是一个“常量”。
int a = 10; int b = 20; const int *p = &a; // 正确:p指向a // *p = 30; // 错误!不能通过p修改a的值 a = 30; // 正确:a本身不是常量,可以直接改 printf(“*p = %d\n”, *p); // 输出:30,p“看到”的值随着a改变而改变 p = &b; // 正确:p本身可以指向另一个地址 printf(“*p = %d\n”, *p); // 输出:20

关键点const int *pint const *p是完全等价的。constint左边还是右边,只要它紧挨着int,修饰的就是指向的数据类型。所以int const *p同样是指向整型常量的指针。我更喜欢写成int const *p,因为const直接跟在类型int后面,修饰关系更一目了然。

2.2 拆解“指针常量”(Constant Pointer)

再看第二种:int * const p;。这叫做“指针常量”或“常量指针”(注意中文歧义),意思是“指针本身是常量”。

  1. 找到变量名p
  2. 向右看:看到*,说明p是一个指针。
  3. 继续向右看:看到了const!这个const直接出现在*右边,它修饰的就是变量名p本身。

所以,int * const p的完整意思是:p是一个const指针,它指向一个int(整型)。核心限制在于“指针变量本身”是常量

这意味着什么?

  • p本身(存储的地址值)在初始化后就不能再改变了,它必须始终指向同一个地址。
  • 但是,你可以通过p这个指针,去自由地修改它所指向的那个内存地址里存储的值。
int a = 10; int b = 20; int * const p = &a; // 正确:p必须初始化,且永远指向a的地址 *p = 30; // 正确:可以通过p修改a的值 printf(“a = %d\n”, a); // 输出:30 // p = &b; // 错误!p本身是常量,不能再指向b

2.3 终极形态:指向常量的指针常量

把上面两种结合起来:const int * const p;int const * const p;

  1. 找到变量名p
  2. 向右看:看到*,说明是指针。
  3. 继续向右看:看到const,说明指针p本身是常量。
  4. 向左看:看到int,指向整型。
  5. 继续向左看:看到const,说明指向的整型也是常量。

结论:p是一个常量指针,并且它指向的数据也是常量。既不能修改指针的值(地址),也不能通过指针修改指向的数据

int a = 10; const int * const p = &a; // *p = 20; // 错误!不能通过p修改数据 // p = NULL; // 错误!不能修改指针本身 // 但是 a = 20; 仍然是合法的,因为a本身的const属性没变。

3. 为什么容易混淆?实战中的高频陷阱

理解了基本规则,为什么在实际编码中还是容易出错?因为下面这些场景会干扰你的判断。

3.1 函数参数传递中的误用

这是最常出问题的地方。当你希望一个函数“只读”地使用传入的数据时,应该用“指向常量的指针”。

错误示范

void print_string(char *str) { // 函数内部可能无意或有意修改了str指向的内容 // str[0] = ‘A’; // 编译能过,但可能违背调用者意图 printf(“%s\n”, str); }

调用者传进来一个字符串字面量或常量区数据,理论上你不该修改它。但上面的函数声明没有阻止你。

正确做法

void print_string(const char *str) { // 使用指向常量的指针 // str[0] = ‘A’; // 编译错误!保护了原始数据不被意外修改 printf(“%s\n”, str); }

在函数参数中使用const,是一种重要的“契约”和自我保护。它告诉调用者:“我保证不会修改你的数据”,同时也防止了函数内部的代码误操作。

3.2 与数组名的关系

数组名在大多数情况下会被转换为指向其首元素的指针。但这里有个微妙之处:

  • int arr[10];中,arr的类型可以看作是int * const(一个指向int的常量指针)。你不能执行arr = &some_other_int;,因为arr的地址是固定的。
  • 但是,arr并不是const int *(指向常量的指针),你可以通过arr[i]修改数组元素。

当你把数组传递给函数时:

void func(int *ptr); // ptr可以指向别处,也可以通过ptr修改数据 void func(const int *ptr); // ptr可以指向别处,但不能通过ptr修改数据 void func(int ptr[]); // 等价于 int *ptr void func(int ptr[const]); // C99起,表示ptr本身是常量(类似int * const ptr),但不能通过ptr修改数据?不,这只表示ptr指针是常量,数据仍可改。这是一个容易混淆的语法。

我建议在函数原型中明确使用const来表达意图,而不是依赖数组语法。

3.3 多级指针带来的复杂度

当遇到指针的指针时,const的位置会让人更加头疼。规则不变:从标识符开始,向右再向左。

  • const char **p;

    • p是指针(第一层)。
    • 向右:*,说明指向的是char *类型(另一个指针)。
    • 向左:const char?不,先看最近的。p是一个指针,指向char *。而char *前面有const吗?const修饰的是char。所以p指向的是一个“指向常量字符的指针”。
    • 含义:p可以指向不同的char *变量,并且可以通过p修改它存储的地址值(即让*p指向不同的字符串)。但是,不能通过**p来修改最终的那个字符,因为最终指向的char是常量。
    • 简化:p->*p(一个char *变量) ->**p(一个const char)。
  • char * const *p;

    • p是指针。
    • 向右:*,指向char * const(一个常量指针)。
    • 含义:p可以指向不同的char * const变量。但是*p(即p指向的那个指针)本身是常量,你不能通过*p = ...来改变它。然而,你可以通过**p来修改最终的那个字符,因为那个字符不是常量。
    • 简化:p->*p(一个char * const) ->**p(一个char)。
  • char ** const p;

    • p是常量指针。
    • 向右:*,指向char *
    • 再向右:*,指向char
    • 含义:p本身(存储的地址)是常量,初始化后不能再指向别的char **。但是,可以通过*p修改它指向的那个char *地址,也可以通过**p修改最终的字符。

看到这里可能有点晕。最好的办法是写个小程序验证。对于多级指针,我个人的经验是:除非必要,尽量避免使用超过两级的指针,并且用typedef来简化。

typedef char * pchar; // pchar 是 char* const pchar *p; // p 是指向 const pchar 的指针,即指向(常量指针)的指针?不对! // 注意:typedef 后,const pchar 中的 const 会修饰整个 pchar 类型,即 pchar 本身是常量。 // 所以 const pchar 等价于 char * const。 // 因此 `const pchar *p` 等价于 `char * const *p`。

使用typedef时,const的修饰对象会发生变化,这又是一个坑。所以对于复杂声明,老老实实用“从内向外”的规则去解析最可靠。

4. 一张表格和一套自检流程帮你彻底分清

说了这么多,我们来总结一个快速判断的方法。

4.1 对比表格

声明形式通俗叫法指针本身 (p)指向的数据 (*p)核心记忆点
const int *p
int const *p
指向常量的指针
(Pointer to Constant)
可以改变不可改变const*左边,修饰数据。
int * const p指针常量
(Constant Pointer)
不可改变可以改变const*右边,修饰指针变量。
const int * const p指向常量的指针常量不可改变不可改变两个const,两边都锁死。

记忆口诀(仅供参考,理解优先)const*左边,指针可以变向(指向别处),数据定值(不能改值)。const*右边,指针定址(不能变向),数据可变(能改值)。

4.2 自检流程:遇到声明不再慌

当你看到任何一个带const的指针声明时,按这个顺序问自己:

  1. 找到变量名:声明语句中哪个是变量名(p,ptr,arr等)?
  2. 向右探索:从变量名开始向右看,直到遇到结束(如分号、逗号或右括号)。
    • 如果先遇到*,说明它是指针。
    • 如果紧接着遇到const,那么这个const就是修饰指针变量本身的。记住这一点。
    • 继续向右,可能还有[ ](数组) 或( )(函数),按优先级处理。
  3. 向左解读:从变量名(及刚才向右分析完的部分)开始向左看。
    • 看到的const都是修饰它左边的类型的。
    • 最终确定指针指向的数据类型是什么(int,char,struct XXX等)。
  4. 得出结论
    • 如果第2步发现const紧挨着变量名(在*右边),则指针本身只读
    • 如果第3步发现const修饰了最终的数据类型(在*左边),则指向的数据只读
    • 两者可能同时存在。

实战演练char const * const *pp;

  1. 变量名是pp
  2. 向右看:*->pp是一个指针。右边没const紧挨着?继续,还有一个*?不对,我们看整体。pp右边是* const *?我们拆开看里层。实际上,声明是char const * const *pp。从pp向右:*,说明pp是指针。它指向什么?指向char const * const这个类型。所以pp本身不是const
  3. 向左看:pp指向的类型是char const * const。这是一个“指向常量字符的指针常量”。所以,*pp(即pp指向的那个东西)是一个指针常量,并且它指向常量字符。
  4. 结论:
    • pp本身可以改变(可以指向不同的char const * const变量)。
    • *pppp指向的那个指针)是常量,不能改变其值。
    • **pp(最终指向的字符)是常量,不能改变其值。

5. 在真实项目中如何应用与避坑

理解了理论,最终要落到代码上。下面是一些实实在在的建议。

5.1 何时使用“指向常量的指针”(const T *p

这是你应该最常使用的形式,尤其是:

  • 函数参数:当函数只需要读取参数指向的数据,而不修改它时。这是API设计的好习惯,能提高代码安全性和可读性。标准库函数如strlen,strcmp的参数就是const char *
  • 遍历只读数据:比如遍历一个字符串字面量或配置数组。
  • 指向其他进程或硬件的只读内存映射

避坑提示:即使你用了const T *p,也不意味着数据真的不可变。如果原始变量不是const,其他人通过其他非const指针依然可以修改它。const在这里主要是一种编译期的承诺和检查。

5.2 何时使用“指针常量”(T * const p

这种形式相对少用,但有其特定场景:

  • 寄存器映射:在嵌入式开发中,某个外设寄存器的地址是固定的,指向它的指针应该声明为指针常量。
  • 固定缓冲区:一个模块内部固定使用某个缓冲区,不希望指针意外指向别处。
  • C99的数组参数语法void func(int arr[const 10]),表示arr在函数内是一个指针常量。

避坑提示:指针常量必须初始化。因为之后就不能再给它赋值了。忘记初始化会导致编译错误,这是好事。

5.3 混合使用与类型转换

最安全的方式是保持const属性的一致性。但有时不得不进行转换。

  • 丢弃const(危险!):使用强制类型转换(T *)可以将const T *转换为T *。但这非常危险,如果原始数据真的是只读的(比如字符串字面量存储在只读段),尝试修改会导致运行时错误(如段错误)。除非你百分之百确定数据可写,否则不要这么做。
  • 添加const(安全):将T *赋值给const T *总是安全的。这相当于承诺“我接下来只读”。编译器允许这种隐式转换。
int a = 10; int *p1 = &a; const int *p2 = p1; // 安全:添加const限定 // *p2 = 20; // 错误 const int b = 100; // int *p3 = &b; // 错误:不能丢弃const限定 int *p3 = (int *)&b; // 强制转换,语法上可行,但修改*b是未定义行为!

5.4 调试与编译器是你的朋友

如果分不清,最简单粗暴的方法是写个小测试代码,让编译器告诉你

int main() { int a = 1, b = 2; // 测试第一种 const int *p1 = &a; // *p1 = 10; // 取消注释看编译器报错 p1 = &b; // 这行应该没问题 // 测试第二种 int * const p2 = &a; *p2 = 10; // 这行应该没问题 // p2 = &b; // 取消注释看编译器报错 return 0; }

当你对某个复杂声明的读写权限不确定时,就把它放到这样一个简单的环境里,尝试修改指针本身和指向的数据,看编译器报哪一行的错。这是最直接、最准确的学习方式。

最后记住,区分指针常量和常量指针,不是靠死记硬背,而是养成从标识符出发、向右再向左的阅读习惯。一开始可能慢,但练上几次就会成为本能。下次再看到const*纠缠在一起时,先别慌,按流程走一遍,你就能清晰地画出它的权限图谱了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/1 21:48:04

2026商场轨道灯厂家哪家强?TOP10靠谱推荐清单

2026商场轨道灯厂家哪家强?Top5靠谱清单,帮你避坑省几十万我跟你说,选商场轨道灯这事儿真的能把人逼疯。别不信。就在上个星期,一个做商场采购的老朋友跟我打电话说:“哥,我后悔死了。当初图便宜选了个杂牌…

作者头像 李华
网站建设 2026/9/1 21:46:14

LaTeX环境配置全攻略:从零搭建VSCode高效写作环境

很多同学在初次接触 LaTeX 时,往往被其强大的排版能力所吸引,但第一步的“环境配置”就足以劝退不少人。面对复杂的安装包、各种发行版的选择、编译器的配置以及与编辑器的集成,新手很容易感到迷茫,网上资料又常常版本过时或语焉不…

作者头像 李华
网站建设 2026/9/1 21:43:44

51单片机DHT11温湿度报警系统:Proteus仿真与源码全解析

简介:本资源是一套完整的基于51单片机的温湿度监测与报警系统开发资料,面向嵌入式初学者、课程设计学生及单片机实训教师,解决环境参数采集、阈值设定、越限报警与本地可视化显示等典型教学实践问题。压缩包共44个文件,801KB&…

作者头像 李华
网站建设 2026/9/1 21:41:50

基于MATLAB/Simulink的Boost电路电压单闭环PI控制设计与仿真

在电力电子和开关电源的设计与仿真中,Boost电路因其能够将输入电压升高而得到广泛应用。然而,一个稳定的输出电压是许多应用场景(如新能源发电、电动汽车、LED驱动等)的基本要求。直接使用固定占空比(D)的P…

作者头像 李华
网站建设 2026/9/1 21:37:41

Excel FILTER函数:告别VLOOKUP,掌握动态数组筛选新思维

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/1 21:32:42

从网络热词到代码实现:探索“紫色小果冻”视觉效果的Web图形技术

最近在技术社区看到不少关于“紫色小果冻”和“透明紫latex”的讨论,乍一看像是美食或娱乐话题,但作为一名开发者,我敏锐地察觉到这背后可能隐藏着某种技术现象、网络文化梗,甚至是特定软件或游戏的视觉特效。这类“黑话”或“梗”…

作者头像 李华