C 语言内存模型
简介
梳理了 C 语言的内存模型与关键修饰符,包括栈/堆/全局/static 的存储与生命周期差异,以及 volatile 与 const 在编译器优化、硬件访问和指针语义中的核心作用。
栈内存 Stack
特点
- 自动分配/自动释放:进入函数分配,离开函数回收
- 生命周期:作用域结束就没了
- 访问快:通常由编译器/ABI 管
- 容量小且固定:嵌入式尤其明显,容易爆栈
典型放在栈上的东西
- 函数局部变量(非 static)
- 函数参数(实现相关,有的会走寄存器再 spill 到栈)
常见坑
- 返回局部变量地址:
return &local;→ 悬空指针 - 大数组放栈:
uint8_t buf[4096];→ 可能直接炸
堆内存 Heap
特点
- 动态分配/释放:
malloc/free(或 new/delete in C++) - 生命周期:需要手动释放(free),否则一直存在(直到进程结束/系统重启)
- 碎片化风险:长期运行 + 不规则分配释放 → 内存碎片
- 线程/中断安全性:很多 RTOS/裸机里堆分配不是“随便在 ISR 里就能用”的(
malloc/free本质不是“纯函数”,需要特殊实现malloc_from_isr)
典型场景
- 需要跨函数/跨模块延长生命周期
- 运行时才知道大小的数据结构
嵌入式建议
- 能不用堆就不用堆;要用也尽量:
- 统一从内存池/固定块分配
- 避免在 ISR 中 malloc/free
- 关注失败路径(malloc 返回 NULL)
- MISRA-C:2012 Rule 21.3(Required) The memory allocation and deallocation functions of <stdlib.h> shall not be used.
- malloc、calloc、realloc、free在MISRA标准中被要求不再使用
文件作用域变量 global variable
存储期(lifetime):静态存储期
- 程序开始就存在
- 程序结束才消失
作用域(scope):文件作用域
- 从声明位置到文件结束可见
链接性(linkage):默认 external linkage
-
同名符号可被别的 .c 文件通过 extern 引用:
int g = 1; // b.c extern int g; -
注意避免tentative definition导致的不确定性
// a.c int g; // b.c int g; /* 在链接阶段: 如果链接器严格遵循“一符号只能定义一次” → multiple definition error 但很多老式工具链会把它当作 “common symbol” → 合并成一个全局变量 */
static关键字 及其两种用法
(A) 文件作用域 static(internal linkage)
static int g_count;
- “只在本 .c 文件可见”(链接层面)
- 生命周期仍然是静态存储期(一直活着)
- 用来做“模块私有变量/私有函数”
(B) 函数内 static(保留值)
void foo(void) {
static int once = 0;
if (!once) { ...; once = 1; }
}
- 只在这个函数里可见(作用域)
- 但它的值会在多次调用之间保留(生命周期长)
- 常用:状态机、缓存、单例初始化(注意并发/中断重入)
volatile 关键字
volatile 的本质:
防止编译器 优化 对变量的 内存访问。
告诉编译器:这个变量的值可能在“你看不到的地方”发生变化,所以每次访问都必须真的去内存读写,不能缓存、不能省略。
经典案例
int flag = 0;
while(flag == 0) {
// wait
}
但编译器可能优化成:
int tmp = flag;
while(tmp == 0) { }
因为它认为:
“flag 在这个循环里没人改,读一次就够了”
于是程序变成:
- flag 永远不更新
- loop 永远出不去
此时需要添加volatile关键字,使其强制从内存中读取真实值
volatile int flag;
volatile 禁止的优化行为
-
寄存器缓存优化
volatile int x; int a = x; int b = x;必须生成两次 load:
load x load x不能优化成:
load x copy -
删除“无用写”
volatile int reg; reg = 0x01; reg = 0x02;编译器不能说:
“第一次写没意义,删掉”
因为硬件寄存器写入可能触发动作。
-
删除“无用读”
volatile int status; status; status;每次读都必须保留,因为:
- 读寄存器可能清 flag
- 读操作本身有副作用
-
重排 volatile 访问顺序(部分限制)
volatile int A, B; A = 1; B = 2;不可调换A和B的访问顺序(但是不影响CPU memory ordering)
volatile 不保证什么
- volatile 不保证原子性
- volatile 不保证同步
- volatile 不解决 cache 一致性
const 关键字
const 的真正含义是:
通过这个名字(lvalue)不能修改对象
它约束的是“访问权限”,不是对象的物理属性。
const int x = 10;
x = 20; // ❌ 编译错误
这里的含义不是“x 在内存里不可变”,而是:
- 编译器禁止你通过
x修改它
const 和指针
核心口诀:
const 修饰的是它左边的东西
如果左边没东西,就修饰右边
-
const int *pconst修饰指针,等价于
int const *p含义:
-
p是 const -
你不能通过 p 改值
*p = 5; // ❌ 不行 p = &y; // ✅ 可以换指向
-
-
int * const p含义:
-
p 本身是 const
-
指针不能改指向
p = &y; // ❌ 不行 *p = 5; // ✅ 可以改内容
-
-
const int * const p含义:
-
指向不能变
-
内容不能改
p = &y; // ❌ *p = 3; // ❌
-
-
const int **pp;类似第一条的情况,从里往外读:
pp是一个指针pp是一个int const **pp是一个const int
pp = &p; // ✅ 可以 *pp = &y; // ✅ 可以 **pp = 5; // ❌ 不行(int 是 const)