ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C/C++关键字volatile

C/C++关键字volatile 目录提纲1、 它到底做了什么2. 最大误区volatile 不是线程同步工具3. 和 const 的区别4. 修饰指针时的写法5. volatile 类对象只能调用 volatile 成员函数且属性会传播到成员volatile 类对象只能调用 volatile 成员函数且属性会传播到成员6、volatile修饰变量的引用问题1. 可以写 volatile int2. 不能写 int volatile 或 int const3. 普通引用不能绑定 volatile 对象4. 通过 volatile 引用访问才算 volatile 访问5. 实际开发中的建议总结提纲1、它到底做了什么volatile 的类型限定符含义、编译器约束、可观测副作用。2. 最大误区volatile 不是线程同步工具澄清 volatile 不能保证原子性、跨线程可见性、指令重排和互斥。3. 和 const 的区别对比 const 与 volatile 的约束对象和典型场景。4. 修饰指针时的写法区分 volatile 修饰指向数据还是指针本身。5. volatile 类对象只能调用 volatile 成员函数且属性会传播到成员成员函数限定、属性传播、拷贝/赋值特殊处理。6、volatile 修饰变量的引用问题volatile T 的写法、引用本身不可限定、普通引用不能绑定 volatile 对象、实际开发建议。总结核心作用、适用场景、常见误区、与 const 的区别、引用写法要点。volatile在 C 里是一个类型限定符核心含义是这个对象的值可能在程序正常控制流之外被改变所以编译器不能对它做“看起来无害”的优化。它主要解决的是编译器优化带来的可见性问题不是线程安全问题。1、 它到底做了什么对一个volatile对象进行读写时编译器通常要遵守这些约束每次读取都要从内存重新取值不能把值长期缓存在寄存器里每次写入都要真正写回内存不能随意合并、消除不能把volatile访问优化掉(如下图所示如果没有volatile编译器在优化时可能认为循环体内没有修改isr_flag就把它的值缓存起来导致死循环。volatile int isr_flag 0; void interrupt_handler() { isr_flag 1; } int main() { while (isr_flag 0) { // 轮询等待 } }在单线程语义下volatile访问不能和其他“可见副作用”随意重排C 标准规定对 volatile 对象的每次读/写都属于可观测副作用编译器必须按照抽象机语义真实执行不能优化掉也不能在同一线程内把 volatile 访问和其他“可观测副作用”随意重排。volatile int flag; int data; data 42; // 普通写不是可见副作用 flag 1; // volatile 写是可见副作用在标准 C 里编译器不能把两次 volatile 访问彼此重排也不能把 volatile 访问和其他可见副作用越过彼此。 但data 42本身不是可见副作用所以编译器在不影响单线程可观测行为的前提下可能对普通变量做更多优化。也就是说volatile 保证的是“volatile 访问本身不被删、不被合并、彼此不乱序”并不等于给所有普通内存访问加上了完整内存屏障。volatile做不到这些。它只能保证每次读都从对象存储位置读每次写都真实写回多核之间缓存何时同步CPU 是否对普通内存访问重排其他线程能否立即看到修改是否存在数据竞争。可观测副作用就是编译器优化不能改变、删除或重排的那些对外部世界有影响的操作。它本 质上是编译器优化的“红线”——编译器可以随便优化内部计算但一旦涉及能被外部观测到的行为就必须老老实实按源码顺序执行。标准定义的可观测副作用包括对 volatile 对象的读写每次访问都必须真实执行不能优化掉。文件 I/O 和标准输出printf、std::cout等输出顺序不能乱。原子操作的同步行为C11 起std::atomic的 load/store 带有内存序语义。浮点环境修改舍入模式、浮点异常状态等需#pragma STDC FENV_ACCESS ON。调用会触发以上行为的函数。为什么 volatile 访问算可观测副作用因为 volatile 通常对应硬件寄存器、中断共享变量等程序外部的实体。编译器无法知道硬件会怎么响应一次写入所以必须把每次读写都当作真实发生的事件。换句话说volatile告诉编译器这个变量可能随时被硬件、中断、信号处理程序等外部因素修改你不能假设它稳定2. 最大误区volatile 不是线程同步工具很多人误以为volatile int counter 0; counter; // 以为这样就线程安全这是错误的。volatile不能保证原子性、跨线程可见性、CPU指令重排、互斥。3. 和const的区别关键字含义典型场景const程序不应修改它常量、只读接口volatile值可能被外部修改硬件寄存器、ISR、信号处理const volatile程序不写但外部可能改只读硬件状态寄存器const是对程序行为的约束volatile是对编译器优化的约束。4. 修饰指针时的写法volatile int* p1; // 指向 volatile int 的指针 int* volatile p2; // 指针本身是 volatile volatile int* volatile p3; // 指针和指向的数据都是 volatile这和const指针的规则类似看volatile修饰的是“指向的数据”还是“指针本身”。5. volatile 类对象只能调用 volatile 成员函数且属性会传播到成员volatile 重排限制主要约束编译器保证 volatile 访问不被删、不被合并、彼此不乱序volatile 类对象只能调用 volatile 成员函数volatile 限定会传播到成员拷贝/赋值也需要特殊处理。volatile修饰类对象时相当于给这个对象加上了“可能被外部修改”的限定。编译器会据此限制你能调用的接口。class Device { public: int read(); // 普通成员函数 int read() volatile; // volatile 成员函数 }; volatile Device dev; dev.read(); // 只能调用 volatile 版本普通成员函数默认假设对象状态相对稳定而volatile成员函数表示即使对象可能被外部因素改变这个函数仍然能安全读取或操作它。“传播到成员”的意思是通过 volatile 对象访问其数据成员时这些成员也会被当作 volatile 看待。struct S { int x; }; volatile S obj; obj.x; // 这个访问被视为对 volatile int 的访问同样volatile 对象不能直接使用合成的拷贝/赋值运算符因为默认拷贝构造和赋值运算符的参数是非 volatile 引用。如果确实需要拷贝 volatile 对象类必须自己提供接受volatile引用的拷贝/赋值接口。这三个问题其实都围绕同一件事volatile 到底约束的是谁答案是它主要约束编译器优化不约束 CPU 硬件、缓存、线程调度也不建立 C 标准意义上的跨线程 happens-before。volatile 类对象只能调用 volatile 成员函数且属性会传播到成员volatile修饰类对象时相当于给这个对象加上了“可能被外部修改”的限定。编译器会据此限制你能调用的接口。class Device { public: int read(); // 普通成员函数 int read() volatile; // volatile 成员函数 }; volatile Device dev; dev.read(); // 只能调用 volatile 版本普通成员函数默认假设对象状态相对稳定而volatile成员函数表示即使对象可能被外部因素改变这个函数仍然能安全读取或操作它。“传播到成员”的意思是通过 volatile 对象访问其数据成员时这些成员也会被当作 volatile 看待。struct S { int x; }; volatile S obj; obj.x; // 这个访问被视为对 volatile int 的访问同样volatile 对象不能直接使用合成的拷贝/赋值运算符因为默认拷贝构造和赋值运算符的参数是非 volatile 引用。如果确实需要拷贝 volatile 对象类必须自己提供接受volatile引用的拷贝/赋值接口。总结一下volatile 重排限制主要约束编译器保证 volatile 访问不被删、不被合并、彼此不乱序跨线程可见性C 里靠std::atomic、锁等建立 happens-beforevolatile 不承担这个责任volatile 类对象只能调用 volatile 成员函数volatile 限定会传播到成员拷贝/赋值也需要特殊处理。6、volatile修饰变量的引用问题可以兼容但有两个前提引用本身不能被 volatile 限定而且引用的目标类型必须带 volatile。1. 可以写volatile intvolatile int v 0; volatile int ref v; // OK这里ref是一个绑定到 volatile int 的引用。通过ref读取或写入时仍然属于 volatile 访问编译器不能随意优化。2. 不能写int volatile或int constC 里引用本身不能被 const 或 volatile 限定int x 0; int volatile r x; // 错误引用本身不能加 volatile int const r2 x; // 错误引用本身不能加 const如果看到int const这种写法通常是想写const int也就是“指向 const int 的引用”。3. 普通引用不能绑定 volatile 对象volatile int v 0; int r v; // 错误非 volatile 引用不能绑定 volatile 对象因为int是非 volatile 引用而v是 volatile 对象。要绑定它引用类型必须带 volatilevolatile int r v; // OK4. 通过 volatile 引用访问才算 volatile 访问创建引用本身不一定触发 volatile 读真正触发的是后续读写volatile int v 0; volatile int ref v; // 创建引用 int x ref; // 这里才是 volatile 读5. 实际开发中的建议在 C11 之后硬件寄存器、中断共享变量、信号处理可以用volatile和volatile线程间共享变量不要用volatile应使用std::atomic或锁。一句话总结引用可以和 volatile 兼容但只能写成volatile T不能写成T volatile而且只有引用目标带 volatile 时通过它的读写才会保持 volatile 语义。总结volatile的核心作用它是一个类型限定符告诉编译器对象的值可能在程序正常控制流之外被改变因此编译器不能对它做“看起来无害”的优化。它主要约束编译器优化保证 volatile 访问不被删、不被合并、彼此不乱序。适用场景硬件寄存器、中断共享变量、信号处理等程序外部实体。常见误区volatile不是线程同步工具不能保证原子性、跨线程可见性、CPU 指令重排和互斥。线程间共享变量应使用std::atomic或锁。与const的区别const约束程序行为volatile约束编译器优化。引用写法引用可以和 volatile 兼容但只能写成volatile T不能写成T volatile只有引用目标带 volatile 时通过它的读写才会保持 volatile 语义。
返回列表