指针与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 * 来指向它。这样代码里谁想改这个值,编译器直接报错,省得我排查半天。
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
这种形态在嵌入式里特别常见。比如你有一个固定的缓冲区地址,用常量指针指向它,既保证了地址不会跑飞,又能自由读写缓冲区内容。
3. 指向常量的常量指针(const int * const p)
这个就厉害了——指针本身不能改,指向的内容也不能改。双重锁定。
const int a = 100;
const int * const p = &a; // 指针和内容都不可改
// *p = 200; // 错误!内容不可改
// p = NULL; // 错误!指针本身不可改
说实话,这种写法在实际项目中用得不多。但一旦用上,往往就是关键地方。比如系统启动时固化的配置参数表,你希望代码里谁都不能动它,连指针都不能乱指。
- 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 int *p |
防止意外修改,同时指针可灵活切换 |
| 固定缓冲区地址 | int * const p |
地址固定,内容可读写 |
| 系统配置参数表 | const int * const p |
双重保护,谁都不能动 |
| 函数参数(只读) | const type *param |
接口清晰,兼容性好 |
嗯,到这里四种形态就讲完了。其实你只要抓住那个核心——const 和 * 的位置关系——剩下的就是多写多练。我在项目中见过太多人在这上面栽跟头,但一旦搞懂了,你会发现 const 其实是你的好朋友,帮你省了不少调试时间。