1.volatile.md 4.5 KB

volatile

判别规则(核心,唯一要记的)

  • 变量可能被程序控制之外的上下文(中断 / 硬件寄存器 / 另一线程)修改 → 加 volatile
  • 否则(只在程序自己代码里改)→ 不加

一句话:"不是我这个函数亲自改的" = 加 volatile。

抽象描述(一句话本质)

volatile 是给编译器的警告:这个变量可能在我看不见的地方被改,每次用到都必须去内存重新读,不许缓存优化。

正反例(建立直觉)

  • 正例1:按键计数值 k,中断在按键按下时 k++,定时器/另一线程可能 k-- → 多个程序外角色会改,必须加(个人实际场景)
  • 正例2:DMA 完成中断置位的 DMA_Flag,main 等它置位再读缓冲区 → ISR 是程序外角色
  • 正例3:硬件寄存器 (*(volatile uint32_t*)0x40021000) → 硬件随时变,防编译器把两次读合并成一次
    • 语法拆解(从里往外读)0x40021000 是外设寄存器固定地址 → (uint32_t*) 强转成"指向 32 位无符号整数的指针" → volatile 标明指针指向的东西是 volatile(每次解引用必须真去内存读)→ * 解引用得到寄存器本体 → 外层 () 让它成为可读可写的左值。即 #define REG (*(volatile uint32_t*)0x40021000),把"裸地址"伪装成普通变量,但值由硬件控制
    • 为什么防"合并两次读"uint32_t a = REG; uint32_t b = REG; return a==b;——编译器看遍代码发现没人改地址 0x40021000,判定内容没变,把 b=REG 优化成复用 a 的值 → a==b 恒真被优化掉。但实际两次读之间硬件可能已更新寄存器,检测逻辑被编译器干掉
    • 更常见场景:轮询死等 while (REG & FLAG_BUSY);——不加 volatile,编译器把读取提循环外,while(1) 死循环;加 volatile 每次循环强制读内存,硬件一清位就退出
    • 类比:寄存器 = 门铃,硬件会随时按;编译器不知道有"看不见的手"在改,会合并读取来提速(内存读比寄存器慢上百倍)。volatile 告诉它:读两次可能得两个不同的值,别合并
  • 反例1int count 只在 main() 里自己加加减减 → 程序内完全控制,不加
  • 反例2:const 全局变量,无任何代码/硬件改它 → 没有外部修改,加 volatile 无意义

易混对比

易混点 A 易混点 B 关键区别
volatile const volatile 防编译器缓存;const 防程序员误改;可共存于只读寄存器
volatile 原子操作 volatile 管"每次读新值";原子管"读-改-写不被拆开";++ 分 LOAD/ADD/STORE 三步,可能被打断丢更新
volatile static static 管存储期/作用域;static 变量被中断/线程共享时照样要加 volatile

变体验证(3 题,全过=学会)

  1. int count 只在 main() 里自己加加减减 → 不加(程序内控制,无外部改写)
  2. DMA 完成中断里置位的 DMA_Flag,main 等它置位 → (ISR 是程序外角色;注意 flag 实际由 DMA 传输完成 ISR 置位,不是 DMA 硬件"顺便改",但"非 main 自己改"这条规则不变)
  3. 陷阱题:const 全局变量,无任何代码/硬件改它 → 不加(没有外部修改,加 volatile 无意义)

口述要点(面试怎么讲)

  • 结论先行:判断标准是"变量是否被程序控制之外的角色修改"
  • 原理:编译器只信它看到的代码。while(!flag) 循环里没人改 flag → 编译器把 flag 缓存进寄存器,循环变 while(1)。volatile 强制每次去内存读。读内存比寄存器慢上百倍,所以缓存优化本身是合理的——错在编译器不知道"看不见的角色"在改它
  • 易错点:volatile 不解决原子性。flag++ = LOAD(读内存)→ ADD(+1)→ STORE(写回)三步,中断可在 LOAD 后插入,导致丢更新(本应 2 实际 1)。解法:临界区(关中断)或硬件原子指令
  • 例子:硬件只读寄存器用 const volatile——const 防代码误写(防程序员),volatile 防缓存读到旧值(防编译器缓存)。两个防的敌人不同,所以只读寄存器两个都要

学习日期

2026-08-03


每主题一页,复习时只翻本目录。