指针与const:四种形态,一个核心

说实话,const 和指针的组合,是很多 C 语言学习者的噩梦。我记得刚入行那会儿,面试官问我「const int *p」和「int * const p」的区别,我当场就懵了。后来在嵌入式项目里被坑过几次,才真正搞明白。

今天咱们就把这四种形态彻底讲透。你想想看,其实就一个核心问题:const 到底修饰的是谁?

1. 指向常量的指针(const int *p)

这种写法,const 修饰的是 *p,也就是指针指向的那个值。说白了,你不能通过这个指针去修改它指向的内容。

const int a = 100;
const int *p = &a;   // p 指向 a

// *p = 200;  // 错误!不能通过 p 修改 a 的值
a = 200;             // 正确!a 本身不是 const,可以直接改

int b = 50;
p = &b;              // 正确!指针本身可以指向别处

我在项目中遇到过这种情况:某个传感器返回的数据是只读的,硬件寄存器映射过来后,我就用 const uint32_t * 来指向它。这样代码里谁想改这个值,编译器直接报错,省得我排查半天。

小技巧:读这种声明时,从右往左读——「p 是一个指针,指向一个 const int」。这样不容易搞混。

2. 常量指针(int * const p)

这次 const 修饰的是 p 本身。指针变量 p 一旦初始化,就不能再指向别处了。但指向的内容可以改。

int a = 10, b = 20;
int * const p = &a;   // p 必须初始化,且不能再指向别处

*p = 30;              // 正确!可以修改 a 的值
// p = &b;           // 错误!p 是常量,不能重新指向

printf("%d\n", *p);   // 输出 30

这种形态在嵌入式里特别常见。比如你有一个固定的缓冲区地址,用常量指针指向它,既保证了地址不会跑飞,又能自由读写缓冲区内容。

注意:常量指针必须初始化!不初始化的话,后面想赋值也赋不了,这个指针就废了。我曾经在代码审查时抓到过这种 bug,编译能过,运行时指针乱指,查了一下午。

3. 指向常量的常量指针(const int * const p)

这个就厉害了——指针本身不能改,指向的内容也不能改。双重锁定。

const int a = 100;
const int * const p = &a;   // 指针和内容都不可改

// *p = 200;  // 错误!内容不可改
// p = NULL;  // 错误!指针本身不可改

说实话,这种写法在实际项目中用得不多。但一旦用上,往往就是关键地方。比如系统启动时固化的配置参数表,你希望代码里谁都不能动它,连指针都不能乱指。

核心记忆法:看 const 和 * 的相对位置。
- const 在 * 左边 → 指向的内容是常量
- const 在 * 右边 → 指针本身是常量
- 两边都有 → 都是常量

4. const 修饰函数参数

这是实际项目中最常用的场景。函数参数加 const,说白了就是告诉调用者:「你放心传进来,我保证不改你的数据。」

// 不修改数据的函数,参数加 const
void print_data(const int *data, size_t len) {
    for (size_t i = 0; i < len; i++) {
        printf("%d ", data[i]);
        // data[i] = 0;  // 错误!不能修改
    }
}

// 需要修改数据的函数,参数不加 const
void reset_data(int *data, size_t len) {
    for (size_t i = 0; i < len; i++) {
        data[i] = 0;
    }
}

我个人习惯是:只要函数不修改指针指向的内容,参数一律加 const。这样做有几个好处:

  • 接口语义清晰,调用者一眼就知道这个函数会不会改数据
  • 编译器帮你做检查,防止手滑改了不该改的东西
  • 可以接受 const 和非 const 的实参,兼容性更好
避坑指南:我曾经在写驱动时,把一个 const 指针传给了另一个非 const 参数的函数。编译器报 warning,我没当回事。结果那个函数偷偷改了寄存器值,导致整个外设工作异常。从那以后,我对 const 的 warning 都是零容忍。

知识体系总览

下面这张图把四种形态的关系梳理清楚了。我建议你把它存下来,每次写代码前瞄一眼。

指针与const 四种形态 const 修饰谁? const int *p 指向常量的指针 内容不可改,指针可改 int * const p 常量指针 指针不可改,内容可改 const int * const p 指向常量的常量指针 指针和内容都不可改 const 修饰函数参数 保证不修改传入数据 接口语义清晰,编译器检查 记住:const 在 * 左边修饰内容,在 * 右边修饰指针

实际项目中的选择建议

场景 推荐写法 原因
只读数据(如传感器值) const int *p 防止意外修改,同时指针可灵活切换
固定缓冲区地址 int * const p 地址固定,内容可读写
系统配置参数表 const int * const p 双重保护,谁都不能动
函数参数(只读) const type *param 接口清晰,兼容性好

嗯,到这里四种形态就讲完了。其实你只要抓住那个核心——const 和 * 的位置关系——剩下的就是多写多练。我在项目中见过太多人在这上面栽跟头,但一旦搞懂了,你会发现 const 其实是你的好朋友,帮你省了不少调试时间。


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