17. const与内存管理:const指针、指向const的指针、const在内存管理中的应用
const 这个关键字,很多初学者觉得它就是「把变量变成常量」。嗯,这么说也没错,但太表面了。真正做嵌入式开发的人都知道,const 背后牵扯的是内存布局、编译器优化、还有代码的安全性。今天我就带你把这些东西彻底捋清楚。
17.1 先搞清楚:const 到底把数据放哪了?
很多人以为 const 变量就一定存在 ROM 或者 Flash 里。其实不一定。这取决于你定义的位置和编译器的处理方式。
| 定义位置 | 存储区域 | 说明 |
|---|---|---|
| 全局 const | .rodata(只读数据段) | 通常放在 Flash 或 ROM,不可修改 |
| 局部 const | 栈 | 实际上还是栈空间,只是编译器禁止你修改 |
| 字符串常量 | .rodata | "hello" 这种,通常放在只读区 |
我在项目中遇到过一个问题:局部 const 变量在栈上,但某些低端 MCU 的栈是映射到 RAM 的,这时候 const 其实是可以被指针强行修改的——只是编译器会警告你。所以别以为加了 const 就万事大吉,它更多是「契约」而不是「锁」。
17.2 指向 const 的指针 vs const 指针
这两个概念,我面试过的人十个有八个搞混。咱们一次说清楚。
指向 const 的指针
指针指向的数据是只读的,但指针本身可以变。
const int *p; // p 可以指向别处,但不能通过 p 修改指向的值
int const *p; // 同上,两种写法等价
说白了就是:你不能通过这个指针去改东西。但指针自己可以换个地方指。
const 指针
指针本身是只读的,但指向的数据可以变。
int *const p = &x; // p 一旦初始化,就不能再指向别处了
// 但可以通过 p 修改 x 的值
这个在嵌入式里很常见。比如你有一个固定的寄存器地址映射,你希望指针永远指向那个地址,但寄存器的值是可以读写的。
两者结合:指向 const 的 const 指针
const int *const p = &x; // 指针不能改,指向的值也不能改
这个在定义硬件只读寄存器的时候特别有用。比如某个状态寄存器,你只能读,不能写,而且地址是固定的。
const int *p —— p 是「指向 const int 的指针」
int *const p —— p 是「const 指针,指向 int」
17.3 const 在内存管理中的实战应用
我个人习惯把 const 用在三个地方:
1. 保护传入的缓冲区
写函数时,如果只是读取数据而不修改,参数一定要加 const。这既是文档,也是保护。
// 好的写法
void print_data(const uint8_t *data, size_t len);
// 不好的写法
void print_data(uint8_t *data, size_t len); // 别人以为你会改数据
我曾经接手过一个项目,有个函数叫 calculate_checksum,参数没加 const。结果后来有人在这个函数里偷偷改了传入的数组,导致整个系统的数据一致性出了问题。查了两天才找到原因。从那以后,我所有只读参数一律加 const。
2. 定义查表数据
嵌入式里经常用查表法,比如 CRC 表、正弦表。这些表一定要用 const 定义,让编译器把它们放到 .rodata 段,不占宝贵的 RAM。
// 正确:放在 Flash,不占 RAM
const uint16_t crc16_table[256] = {
0x0000, 0xC0C1, 0xC181, ...
};
// 错误:放在 RAM,浪费空间
uint16_t crc16_table[256] = {
0x0000, 0xC0C1, 0xC181, ...
};
你想想看,一个 256 个 uint16_t 的表,就是 512 字节。对于只有 2KB RAM 的小 MCU 来说,这 512 字节可能就是生死线。用 const 定义,直接省下来了。
3. 字符串常量和错误信息
很多嵌入式系统需要输出调试信息。把这些字符串定义为 const,可以避免它们被意外修改,同时节省 RAM。
const char * const error_msgs[] = {
"No error",
"Timeout",
"CRC mismatch",
"Buffer overflow"
};
这里用了两层 const:第一层保证字符串内容不可改,第二层保证指针数组本身不可改。双重保险。
17.4 避坑指南:const 和指针的常见陷阱
const int a = 10;
const int *p = &a;
int *q = p; // 编译警告!丢弃了 const 限定符
编译器会警告你,但如果你强制转换,就能绕过。我曾经见过有人这么干,然后通过 q 修改了 a 的值——在有些平台上,a 在 .rodata 段,写操作会导致硬件异常。嗯,那场面挺壮观的。
const char* get_version(void) {
return "v1.2.3";
}
调用方如果试图修改返回值,编译器会报错。但如果你用 (char*) 强制转换,那就自己承担后果吧。
17.5 知识体系图
下面这张图帮你理清 const 在内存管理中的角色:
17.6 总结
const 不是 C 语言的装饰品。它是内存管理的重要工具。用好 const,你的代码会更安全、更省内存、更容易维护。
我个人习惯在写嵌入式代码时,先问自己三个问题:
- 这个数据需要被修改吗?不需要 → 加 const
- 这个指针需要重新指向别处吗?不需要 → 加 const
- 这个函数的参数会被修改吗?不会 → 加 const
养成这个习惯后,你会发现很多内存相关的 bug 在编译阶段就被消灭了。这才是真正的「防患于未然」。
const volatile uint32_t *status_reg = (uint32_t *)0x40001000;。const 保证你不会误写,volatile 保证每次读取都是真实值。这个组合在嵌入式里非常实用。
公众号:蓝海资料掘金营,微信deep3321