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 禁止的优化行为

  1. 寄存器缓存优化

    volatile int x;
    
    int a = x;
    int b = x;
    

    必须生成两次 load:

    load x
    load x
    

    不能优化成:

    load x
    copy
    
  2. 删除“无用写”

    volatile int reg;
    
    reg = 0x01;
    reg = 0x02;
    

    编译器不能说:

    “第一次写没意义,删掉”

    因为硬件寄存器写入可能触发动作。

  3. 删除“无用读”

    volatile int status;
    
    status;
    status;
    

    每次读都必须保留,因为:

    • 读寄存器可能清 flag
    • 读操作本身有副作用
  4. 重排 volatile 访问顺序(部分限制)

    volatile int A, B;
    
    A = 1;
    B = 2;
    

    不可调换A和B的访问顺序(但是不影响CPU memory ordering)

volatile 不保证什么

  1. volatile 不保证原子性
  2. volatile 不保证同步
  3. volatile 不解决 cache 一致性

const 关键字

const 的真正含义是:

通过这个名字(lvalue)不能修改对象

它约束的是“访问权限”,不是对象的物理属性。

const int x = 10;
x = 20; // ❌ 编译错误

这里的含义不是“x 在内存里不可变”,而是:

  • 编译器禁止你通过 x 修改它

const 和指针

核心口诀:

const 修饰的是它左边的东西

如果左边没东西,就修饰右边

  1. const int *p

    const修饰指针,等价于int const *p

    含义:

    • p 是 const

    • 你不能通过 p 改值

      *p = 5;   // ❌ 不行
      p = &y;   // ✅ 可以换指向
      
  2. int * const p

    含义:

    • p 本身是 const

    • 指针不能改指向

      p = &y;   // ❌ 不行
      *p = 5;   // ✅ 可以改内容
      
  3. const int * const p

    含义:

    • 指向不能变

    • 内容不能改

      p = &y;   // ❌
      *p = 3;   // ❌
      
  4. const int **pp;

    类似第一条的情况,从里往外读:

    1. pp 是一个指针
    2. pp 是一个 int const *
    3. *pp 是一个 const int
    pp = &p;   // ✅ 可以
    *pp = &y;  // ✅ 可以
    **pp = 5;  // ❌ 不行(int 是 const)
    
卡片 ID: c-memory-keywords