news 2026/10/5 7:39:37

C/C++关键字volatile

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C/C++关键字volatile

目录

提纲

1、 它到底做了什么?

2. 最大误区:volatile 不是线程同步工具

3. 和 const 的区别

4. 修饰指针时的写法

5. volatile 类对象只能调用 volatile 成员函数,且属性会传播到成员

volatile 类对象只能调用 volatile 成员函数,且属性会传播到成员

6、volatile修饰变量的引用问题

1. 可以写 volatile int&

2. 不能写 int& volatile 或 int& const

3. 普通引用不能绑定 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对象进行读写时,编译器通常要遵守这些约束:

  1. 每次读取都要从内存重新取值,不能把值长期缓存在寄存器里;

  2. 每次写入都要真正写回内存,不能随意合并、消除;

  3. 不能把volatile访问优化掉;(如下图所示,如果没有volatile,编译器在优化时可能认为循环体内没有修改isr_flag,就把它的值缓存起来,导致死循环。)

    volatile int isr_flag = 0; void interrupt_handler() { isr_flag = 1; } int main() { while (isr_flag == 0) { // 轮询等待 } }
  4. 在单线程语义下,volatile访问不能和其他“可见副作用”随意重排

C++ 标准规定:对 volatile 对象的每次读/写都属于可观测副作用,编译器必须按照抽象机语义真实执行,不能优化掉,也不能在同一线程内把 volatile 访问和其他“可观测副作用”随意重排。

volatile int flag; int data; data = 42; // 普通写,不是可见副作用 flag = 1; // volatile 写,是可见副作用

在标准 C++ 里,编译器不能把两次 volatile 访问彼此重排,也不能把 volatile 访问和其他可见副作用越过彼此。 但data = 42本身不是可见副作用,所以编译器在不影响单线程可观测行为的前提下,可能对普通变量做更多优化。也就是说,volatile 保证的是“volatile 访问本身不被删、不被合并、彼此不乱序”,并不等于给所有普通内存访问加上了完整内存屏障。

volatile做不到这些。它只能保证:

  1. 每次读都从对象存储位置读;
  2. 每次写都真实写回;
  3. 多核之间缓存何时同步;
  4. CPU 是否对普通内存访问重排;
  5. 其他线程能否立即看到修改;
  6. 是否存在数据竞争。

可观测副作用就是编译器优化不能改变、删除或重排的那些对外部世界有影响的操作。它本 质上是编译器优化的“红线”——编译器可以随便优化内部计算,但一旦涉及能被外部观测到的行为,就必须老老实实按源码顺序执行。

标准定义的可观测副作用包括

  1. 对 volatile 对象的读写:每次访问都必须真实执行,不能优化掉。
  2. 文件 I/O 和标准输出:printf、std::cout等,输出顺序不能乱。
  3. 原子操作的同步行为(C++11 起):std::atomic的 load/store 带有内存序语义。
  4. 浮点环境修改:舍入模式、浮点异常状态等(需#pragma STDC FENV_ACCESS ON)。
  5. 调用会触发以上行为的函数。

为什么 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-before,volatile 不承担这个责任;
  • volatile 类对象:只能调用 volatile 成员函数,volatile 限定会传播到成员,拷贝/赋值也需要特殊处理。

6、volatile修饰变量的引用问题

可以兼容,但有两个前提:引用本身不能被 volatile 限定,而且引用的目标类型必须带 volatile。

1. 可以写volatile int&

volatile int v = 0; volatile int& ref = v; // OK

这里ref是一个绑定到 volatile int 的引用。通过ref读取或写入时,仍然属于 volatile 访问,编译器不能随意优化。

2. 不能写int& volatile或int& const

C++ 里引用本身不能被 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 对象。要绑定它,引用类型必须带 volatile:

volatile int& r = v; // OK

4. 通过 volatile 引用访问才算 volatile 访问

创建引用本身不一定触发 volatile 读,真正触发的是后续读写:

volatile int v = 0; volatile int& ref = v; // 创建引用 int x = ref; // 这里才是 volatile 读

5. 实际开发中的建议

在 C++11 之后:

  • 硬件寄存器、中断共享变量、信号处理:可以用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 语义。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 7:39:20

VMware虚拟机显卡配置:3D加速、GPU直通与vGPU选型解析

简介:面向需要在VMware虚拟机中获得更好图形性能、运行Linux桌面环境或进行图形密集型应用开发的IT运维与开发用户,这份文档以Red Hat 7.3为例,系统讲解虚拟机中配置显卡的完整流程。内容依次覆盖加载VMware Tools的三种常用方式、通过mount命…

作者头像 李华
网站建设 2026/10/5 7:37:44

VMware Workstation快照恢复失败全排查:从原因定位到数据抢救

快照恢复失败这种事,落到新手头上基本就是“双击快照等半天,结果虚拟机直接罢工”。我之前也觉得快照就是个后悔药,随手建、随手回,直到某次恢复时整个虚拟机连启动器都打不开,日志一片飘红,才意识到自己对…

作者头像 李华
网站建设 2026/10/5 7:36:14

高通Camera调试实战:从CCI总线到CamX全栈排障指南

1. 项目概述:这不是“调通就行”,而是对高通Camera子系统的一次深度解剖“高通camera调试经验总结”——这八个字背后,藏着无数工程师在凌晨三点盯着logcat发呆的夜晚,也藏着产线良率从82%爬升到99.3%的关键转折。我干这行十年&am…

作者头像 李华
网站建设 2026/10/5 7:35:18

Kafka实战指南:消息队列原理、SpringBoot接入与高可靠架构

1. 整体思路与核心概念拆解做过几年后端的人,大概率都有过被业务系统“卡脖子”的经历:秒杀活动一上来,数据库连接池瞬间被打满;日志量稍微一涨,ES集群直接飙红;再严重点,上游接口抖动&#xff…

作者头像 李华
网站建设 2026/10/5 7:35:12

DeepSeek多模态协同开发:图像识别与文本生成的语义对齐实战

简介:本资源是一份面向AI开发者与多模态应用工程师的实战型技术文档,聚焦DeepSeek平台图像识别与文本生成API的协同开发方法,解决跨模态数据联动、语义对齐与系统集成等实际工程问题。文档共34页PDF,结构完整、图文并茂&#xff0…

作者头像 李华
网站建设 2026/10/5 7:35:03

硬件I2C与软件I2C深度对比:从时序原理到选型避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华