volatile关键字在C语言中该放哪里?

99ANYc3cd6
预计阅读时长 16 分钟
位置: 首页 C语言 正文

volatile 的位置非常灵活,它可以放在几乎所有与变量声明相关的位置,但其核心思想是修饰变量,告诉编译器:“这个变量可能会被意料之外的因素改变,请不要对它进行任何假设和优化。”

c语言 volatile位置
(图片来源网络,侵删)

volatile 的核心作用(为什么需要它)

在深入位置之前,必须理解 volatile 的目的,否则会乱用。

主要目的:防止编译器过度优化。

编译器为了提高代码效率,会进行很多优化,

  1. 删除“无用”的代码:如果一个变量的值在读取后没有被修改,编译器可能会认为后续再次读取它的代码是“无用”的,直接使用第一次读取的缓存值。
  2. 将变量缓存到寄存器:频繁访问的全局变量或静态变量,编译器可能会将其加载到寄存器中,后续访问直接读写寄存器,而不是内存,以加快速度。

这些优化在普通变量上非常有效,但对于某些特殊变量,却是致命的。

c语言 volatile位置
(图片来源网络,侵删)

典型的需要 volatile 修饰的变量场景:

  1. 内存映射的外设寄存器:控制 LED 灯的寄存器、读取 ADC 值的寄存器,硬件会直接在内存地址上改变这些值,而 CPU 的 C 代码可能只是偶尔读取一次,如果编译器优化,它可能会把第一次读到的值一直留在寄存器里,导致程序无法感知到硬件的实际变化。

    // 假设 0x40001000 是一个状态寄存器,硬件会改变它的值
    volatile uint32_t * const STATUS_REGISTER = (uint32_t *)0x40001000;
    void check_status() {
        if (*STATUS_REGISTER == 0x01) { // 编译器不会优化掉这次读取
            // do something
        }
    }
  2. 共享的全局变量(多线程/中断):当一个全局变量被一个线程修改,同时被另一个线程或中断服务程序 读取时,为了确保读取到的是最新的值,而不是被缓存到寄存器里的旧值,需要使用 volatile

    volatile int shared_data = 0;
    void thread_A() {
        shared_data = 100; // 线程A写入
    }
    void thread_B() {
        int data = shared_data; // 线程B读取,必须从内存中读取最新值
    }
  3. 被中断服务程序 修改的全局变量:ISR 会修改一个变量,而主循环会读取这个变量,为了保证主循环每次读取的都是 ISR 修改后的最新值,该变量必须是 volatile

    c语言 volatile位置
    (图片来源网络,侵删)
    volatile uint8_t flag = 0;
    void main() {
        while(1) {
            if (flag) { // 每次循环都会检查内存中的 flag 值
                // do something
                flag = 0; // 清除标志
            }
        }
    }
    void ISR_Handler() {
        flag = 1; // 中断发生时修改 flag
    }

volatile 的合法位置(语法)

volatile 是一个类型修饰符,它的位置可以灵活地放在变量类型声明中的多个地方,以下都是合法且等价的写法。

基本用法:修饰单个变量

这是最常见的用法。

// 位置:类型关键字前
volatile int counter;
// 位置:类型关键字后(更常见)
int volatile counter;
// 位置:指针符号 * 前(修饰指针指向的变量)
volatile int * p_data; // p_data 是一个指向 volatile int 类型的指针
                      // 它告诉编译器:*p_data 是 volatile 的,不要优化对它的读写
                      // 但指针 p_data 本身不是 volatile 的
// 位置:指针符号 * 后(修饰指针变量本身)
int * volatile p_data; // p_data 是一个 volatile 的指针
                      // 它告诉编译器:不要优化对指针 p_data 本身的读写(防止它被意外改变)
                      // 但它指向的 int 变量不是 volatile 的
// 位置:同时修饰指针和指针指向的变量(最复杂但明确)
volatile int * volatile p_data; // p_data 是一个 volatile 的指针,
                                 // 并且它指向的 int 变量也是 volatile 的

总结指针与 volatile 的组合:

  • volatile int * p; -> 数据是 volatile,指针本身不是。
  • int * volatile p; -> 指针是 volatile,数据本身不是。
  • volatile int * volatile p; -> 数据和指针都是 volatile

修饰结构体或联合体

当结构体或联合体的成员可能被硬件或中断直接修改时,整个结构体都应该被声明为 volatile

typedef struct {
    uint8_t  status;
    uint16_t value;
    uint8_t  error_code;
} DeviceStatus;
// 告诉编译器,任何对 dev_stat 的成员的访问,都必须从内存中进行,不能优化
volatile DeviceStatus dev_stat;
// 使用时,访问成员自然也是 volatile 的
void update_status() {
    if (dev_stat.status == 0x01) { // 编译器不会优化这次读取
        dev_stat.value = 0;
    }
}

修饰全局变量和局部变量

volatile 可以修饰任何作用域的变量,只要其生命周期和访问方式符合 volatile 的使用场景。

// 全局变量
volatile int global_counter;
// 局部变量(不常见,但合法)
// 一个指向硬件寄存器的局部指针
void function() {
    volatile uint32_t * const REG = (uint32_t *)0x40000000;
    *REG = 0x01; // 写入
    uint32_t val = *REG; // 读取,确保从内存获取
}

修饰数组

// 告诉编译器,数组 buffer 中的每个元素都是 volatile 的
volatile uint8_t buffer[128];
void process_data() {
    uint8_t first_byte = buffer[0]; // 必须从内存中读取
    buffer[1] = 0xFF;              // 必须写入内存
}

volatile 的常见误区

  1. volatile 不等于原子操作 volatile 只保证每次访问都从内存中读写,不保证访问的原子性,在 32 位系统上,int volatile my_var;my_var++ 操作(读-改-写)不是原子的,如果两个线程同时执行它,仍然会发生数据竞争,对于原子操作,需要使用 C11 标准引入的 <stdatomic.h> 库或特定平台的原子操作指令。

  2. volatile 不能保证同步 volatile 不能解决多线程环境下的同步问题,它只是禁止了编译器优化,但无法阻止 CPU 缓存一致性带来的问题,真正的线程同步需要使用互斥锁、信号量等同步机制。

  3. 不要滥用 volatile 只在确实需要防止硬件、中断或并发修改导致优化错误的情况下使用 volatile,滥用 volatile 会降低代码的执行效率,因为它让编译器放弃了许多有效的优化机会。


修饰对象 示例 含义
普通变量 volatile int a;
int volatile a;
变量 avolatile 的,每次读写都直接访问内存。
指针指向的数据 volatile int *p; p 指向的数据是 volatile 的,*p 的访问是原子的(相对于优化而言),指针 p 本身不是。
指针变量本身 int * volatile p; 指针 p 本身是 volatile 的,不能被优化(防止其被意外重定向),它指向的数据不是。
数据和指针 volatile int * volatile p; 指针 p 和它指向的数据都是 volatile 的。
结构体/联合体 volatile Struct s; 结构体 s 的所有成员都是 volatile 的。
数组 volatile int arr[10]; 数组 arr 的所有元素都是 volatile 的。

核心思想:把 volatile 想象成一个“请勿缓存”的标签,当你把标签贴在一个变量上时,你就在告诉编译器:“这个变量的世界你不懂,它随时可能被我不知道的力量改变,所以每次要用它的时候,都直接去它的大本营(内存)问一下,别用你手里的缓存记录。”

-- 展开阅读全文 --
头像
dede上传图片为何报500错误?
« 上一篇 今天
织梦如何获取当前点击的图片地址?
下一篇 » 今天

相关文章

取消
微信二维码
支付宝二维码

目录[+]