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,但可以修改 *pppp 本身。

说实话,二级指针的 const 在实际项目中用得很少。如果你遇到了,建议用 typedef 简化一下:

typedef const int * ConstIntPtr;
ConstIntPtr *pp;  // 清晰多了

12.7 知识结构图

下面这张图帮你理清 const 与指针的完整关系:

const 与指针知识结构 const 与指针 const int *p 指向常量的指针 指针可改,指向的值不可改 int * const p 常量指针 指针不可改,指向的值可改 const int * const p 常量的常量指针 指针和值都不可改 应用:函数只读参数 字符串常量 应用:硬件寄存器地址 数组名 应用:全局只读配置表 安全关键数据 核心:const 在 * 左边修饰数据,在 * 右边修饰指针

12.8 总结

const 与指针的组合,说白了就是两件事:指针本身能不能改指向的数据能不能改。你只要抓住这个核心,不管怎么组合都不会乱。

我个人建议:在写新代码时,尽量多用 const。它不仅是给编译器看的约束,更是给读代码的人看的文档。一个加了 const 的函数参数,比十行注释都管用。

最后一个小技巧:如果你不确定某个 const 声明是什么意思,就试着从右往左读。比如 const int * const p:p 是一个 const 指针,指向一个 const int。嗯,是不是清晰多了?


公众号:蓝海资料掘金营,微信deep3321