12. const 与指针:const int *、int * const、const int * const 的区别与应用场景
指针和 const 的组合,说实话,是很多 C 语言学习者的噩梦。我记得刚入行那会儿,面试官就爱问这个。我自己也在这上面栽过跟头——有一次在嵌入式固件里,因为 const 位置搞反,把一个本该只读的缓冲区给写穿了,结果系统跑着跑着就随机死机。查了两天才定位到问题。
今天咱们就把这几种组合彻底捋清楚。你只要记住一个核心原则:const 修饰的是它右边紧挨着的东西。嗯,就这么简单。
12.1 三种基本形式
先看代码,再讲道理。
const int *p1; // 方式一:指向 const int 的指针
int * const p2; // 方式二:指向 int 的 const 指针
const int * const p3; // 方式三:指向 const int 的 const 指针
怎么读?我教你一个技巧:从右往左读。
const int *p:p 是一个指针,指向一个 const int。也就是说,指针可以变,但指向的值不能变。int * const p:p 是一个 const 指针,指向一个 int。也就是说,指针本身不能变,但指向的值可以变。const int * const p:p 是一个 const 指针,指向一个 const int。也就是说,指针和指向的值都不能变。
核心口诀:const 在 * 左边,修饰的是指向的数据;const 在 * 右边,修饰的是指针本身。
12.2 逐个拆解
12.2.1 const int *p —— 指向常量的指针
这种写法最常见。我在项目中经常用它来传递只读缓冲区。比如你有一个传感器数据数组,只想让函数读取,不想让它修改。
int value = 42;
const int *p = &value;
// 可以修改指针本身
p = &another_value; // 合法
// 但不能通过指针修改指向的值
// *p = 100; // 编译错误!
你可能会问:那 value 本身能不能改?当然可以。value 本身不是 const 的,只是你通过 p 这个视角看过去是只读的。这就好比你把一份文件交给别人看,但告诉他「只能看,不能改」。
我的习惯:只要函数不需要修改传入的数据,参数一律写成 const int * 或 const char *。这既是文档,也是编译器帮你做的安全检查。
12.2.2 int * const p —— 常量指针
这种写法在嵌入式里很常见。比如你有一个固定的内存映射寄存器地址,指针本身不能变,但寄存器的值可以读写。
int value = 10;
int * const p = &value;
// 可以通过指针修改值
*p = 20; // 合法
// 但不能修改指针本身
// p = &another; // 编译错误!
我曾经在写 STM32 的寄存器映射时,就用 volatile uint32_t * const 来定义外设基地址。指针固定,但寄存器的值随时可能被硬件改变。
注意:常量指针必须在定义时初始化。因为一旦定义,就不能再指向别处了。这和数组名有点像——数组名本质上就是一个常量指针。
12.2.3 const int * const p —— 常量的常量指针
这个组合最严格:指针不能改,指向的值也不能改。一般用在全局常量表或者只读配置结构体上。
const int value = 100;
const int * const p = &value;
// 下面两行都编译不过
// *p = 200;
// p = &other;
说实话,这种写法在实际项目中用得不多。但面试官特别喜欢考,因为它能检验你对 const 和指针的理解是否透彻。
12.3 对比表格
| 声明 | 指针本身 | 指向的数据 | 典型场景 |
|---|---|---|---|
const int *p |
可修改 | 不可修改 | 函数参数(只读输入) |
int * const p |
不可修改 | 可修改 | 硬件寄存器地址 |
const int * const p |
不可修改 | 不可修改 | 全局只读配置表 |
12.4 函数参数中的 const 指针
这是实际开发中最常用的地方。我写函数时,有个铁律:如果函数不修改传入的数据,参数就用 const 指针。
// 好的写法:明确告诉调用者,我不会改你的数据
void print_data(const int *data, size_t len) {
for (size_t i = 0; i < len; i++) {
printf("%d ", data[i]);
}
}
// 不好的写法:调用者会担心你改了他的数据
void print_data(int *data, size_t len) {
// ...
}
你想想看,如果你调用一个函数,传进去一个指针,结果函数偷偷把你的数据改了,那得多坑?用 const 就是给调用者一个承诺。
避坑指南:我曾经在写一个通信协议栈时,把一个接收缓冲区的指针传给了下层处理函数。下层函数声明的是 int *,结果它把缓冲区内容改了,导致上层重传逻辑完全乱掉。从那以后,所有只读参数我都加 const。
12.5 const 指针与字符串
字符串常量在 C 语言中就是 const char * 类型。但很多老代码喜欢写成 char *,这其实是不规范的。
const char *msg = "Hello, world!"; // 正确
// char *msg = "Hello, world!"; // 不推荐,字符串常量是只读的
如果你用 char * 指向字符串常量,然后试图修改它,行为是未定义的。有些平台会崩溃,有些平台静默失败。嗯,别问我怎么知道的。
12.6 二级指针与 const
二级指针的 const 更绕一些。我简单说一下:
int value = 10;
int *p = &value;
const int **pp = &p; // 指向 const int * 的指针
这里 pp 是一个指针,它指向一个 const int * 类型的变量。也就是说,你不能通过 **pp 来修改 value,但可以修改 *pp 或 pp 本身。
说实话,二级指针的 const 在实际项目中用得很少。如果你遇到了,建议用 typedef 简化一下:
typedef const int * ConstIntPtr;
ConstIntPtr *pp; // 清晰多了
12.7 知识结构图
下面这张图帮你理清 const 与指针的完整关系:
12.8 总结
const 与指针的组合,说白了就是两件事:指针本身能不能改,指向的数据能不能改。你只要抓住这个核心,不管怎么组合都不会乱。
我个人建议:在写新代码时,尽量多用 const。它不仅是给编译器看的约束,更是给读代码的人看的文档。一个加了 const 的函数参数,比十行注释都管用。
最后一个小技巧:如果你不确定某个 const 声明是什么意思,就试着从右往左读。比如 const int * const p:p 是一个 const 指针,指向一个 const int。嗯,是不是清晰多了?
公众号:蓝海资料掘金营,微信deep3321