3、静态内存分配:全局变量、静态变量、常量区,优缺点分析
说到静态内存分配,我脑子里第一个蹦出来的词就是「稳」。
为什么?因为这部分内存从程序启动到结束,地址纹丝不动。你不用担心它被谁抢走,也不用考虑什么时候释放。说白了,它就是嵌入式系统里的「钉子户」。
3.1 静态内存都藏在哪?
先理清楚一个概念。静态内存分配,对应的是三个不同的存储区域:
- 全局变量区——你在函数外面定义的变量,比如
int g_counter; - 静态变量区——加了
static关键字的变量,不管在函数内还是函数外 - 常量区——用
const修饰的只读数据,还有字符串字面量
这三兄弟,都在程序编译链接时就确定了地址。运行时,它们就在那里,不增不减。
核心区别:
- 全局变量和静态变量:可读可写,生命周期贯穿整个程序
- 常量区:只读,你敢写它就敢崩(硬件异常)
3.2 代码里长什么样?
我直接贴一段典型的嵌入式代码,你一看就明白:
#include <stdint.h>
// 全局变量 —— 放在 .data 或 .bss 段
uint32_t system_tick = 0; // 已初始化,放 .data
uint8_t rx_buffer[1024]; // 未初始化,放 .bss
// 常量 —— 放在 .rodata 段(只读)
const char* const VERSION = "v2.1.3";
const float PI = 3.1415926f;
void timer_handler(void) {
// 静态局部变量 —— 也在静态区,但作用域只在函数内
static uint32_t call_count = 0;
call_count++;
// 普通局部变量 —— 在栈上,跟静态区没关系
uint32_t temp = call_count;
system_tick++;
}
嗯,这里要注意:static uint32_t call_count 虽然写在函数里,但它不占栈空间。它的内存地址在编译时就定死了。我第一次带团队时,有个小伙子非说 static 变量在栈上,结果排查一个栈溢出问题查了半天……后来我让他打印地址,他才服气。
3.3 优点:为什么我偏爱静态分配?
说实话,在资源紧张的 MCU 上,我个人的习惯是:能用静态,尽量不用动态。原因如下:
- 确定性极强——地址固定,访问速度快。没有 malloc/free 的开销,也没有堆碎片问题。
- 生命周期可控——程序启动就存在,程序结束才消失。不会出现「野指针」或「悬空引用」。
- 调试友好——我在调试器里直接看全局变量的地址,随时可以修改值。动态分配的内存,有时候断点一停,地址都找不着。
- 实时性有保障——分配动作在编译期完成,运行时零延迟。这对中断服务程序来说,简直是救命稻草。
我的小技巧:
在 FreeRTOS 或裸机系统中,我习惯把任务栈、消息队列、信号量都声明为全局静态数组。这样既避免了堆分配的不确定性,又方便在链接脚本里精确控制内存布局。
3.4 缺点:静态分配也有坑
但是,别以为静态分配就是银弹。我曾经在一个项目中吃过亏,差点翻车。
第一个问题:内存浪费。
静态分配是「一次申请,终身占用」。你声明了一个 4KB 的缓冲区,哪怕 99% 的时间只用 100 字节,那 4KB 也占着。我做过一个传感器采集项目,为了兼容最大数据包,静态分配了 8KB 的 buffer。结果后来发现,99% 的包只有 200 字节。那 7.8KB 就这么白白浪费了。
第二个问题:可移植性差。
全局变量满天飞的代码,想移植到另一个平台?难。因为静态分配的大小和布局,跟具体芯片的 RAM 大小强相关。你换个 RAM 小一半的芯片,可能链接阶段就报错了。
第三个问题:多线程/多任务下的竞争。
全局变量是所有任务都能访问的。不加保护的话,一个任务在写,另一个任务在读,数据就乱套了。我见过最离谱的 bug,是两个中断服务程序共用一个全局标志位,结果互相踩踏,系统直接死机。
警告:
千万不要在中断服务函数里修改全局变量而不加保护!除非你能保证该变量只在中断中访问,或者使用了原子操作。否则,你会体验到什么叫「随机崩溃」。
3.5 常量区的特殊之处
常量区,也就是 .rodata 段,在大多数 MCU 上会被放到 Flash 里。这意味着:
- 不占宝贵的 RAM
- 掉电不丢失
- 访问速度比 RAM 慢一点(取决于 Flash 等待周期)
我个人的建议是:所有查表用的数据、固件版本字符串、校准参数,都声明为 const 并放到常量区。这样既省 RAM,又安全——代码想改都改不了。
举个例子,一个 256 字节的 sin 查找表:
// 放在 Flash 里,不占 RAM
const uint16_t sin_table[256] = {
0, 402, 804, 1206, ... // 实际数据省略
};
如果你不小心写成了 uint16_t sin_table[256],那这 512 字节就进了 RAM。对于只有 8KB RAM 的芯片来说,这差别太大了。
3.6 什么时候该用,什么时候不该用?
我总结了一个简单的判断原则,你参考一下:
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 系统配置参数(固定不变) | const 常量 | 放 Flash,省 RAM |
| 中断服务程序用的数据 | 全局/静态变量 | 避免动态分配的不确定性 |
| 大块缓冲区(如 DMA 用) | 全局静态数组 | 地址固定,DMA 方便配置 |
| 临时计算用的中间变量 | 栈上局部变量 | 用完即释放,节省静态区 |
| 大小不确定的数据 | 动态分配(谨慎使用) | 避免静态分配浪费 |
3.7 避坑指南
最后,分享几个我踩过的坑:
- 别在头文件里定义全局变量——多个 .c 文件包含同一个头文件,链接时会报重复定义。正确做法是在 .c 里定义,在 .h 里用
extern声明。 - 静态局部变量不是线程安全的——两个任务同时调用同一个函数,里面的 static 变量会被互相覆盖。我建议在 RTOS 环境下,慎用函数内的 static 变量。
- 常量区不是绝对安全——虽然硬件上标记为只读,但如果你的芯片有 Flash 控制器,某些异常情况(比如电源波动)可能导致 Flash 内容被篡改。关键常量建议加 CRC 校验。
好了,静态内存分配就聊这么多。说白了,它就像嵌入式世界里的「老黄牛」——踏实、可靠,但不够灵活。用得好,它是系统的基石;用不好,它就是内存浪费的元凶。下一章我们聊聊栈上的自动变量,那又是另一番天地了。