C & C++ 中的关键字 volatile 之详解

目录

[1. volatile 本身的含义](#1. volatile 本身的含义)

[2. volatile 在计算机领域中的含义](#2. volatile 在计算机领域中的含义)

[3. volatile 在C和C++ 中的含义](#3. volatile 在C和C++ 中的含义)

[4. volatile 与多线程](#4. volatile 与多线程)

[5. C语言中内存映射I/O的示例](#5. C语言中内存映射I/O的示例)

[6. C中编译对比](#6. C中编译对比)


1. volatile 本身的含义

从字典词义上来说,volatile 词义为"易变的,易失的。"

2. volatile 在计算机领域中的含义

在计算机编程中,如果一个变量的值能够被当前执行线程以外的实体异步读取或修改,则称该变量为"易变"变量(volatile variable)。易变变量的值可能会因多种原因而发生自发变化,例如:与其他线程共享数据;与异步信号处理程序共享数据;或者通过内存映射 I/O(Memory-Mapped I/O)访问硬件设备(即通过读写内存来接收和发送来自外设的消息)。在支持 volatile 关键字的各种编程语言中,对这些应用场景的支持程度各不相同变量的易变性可能会影响函数调用约定,以及变量的存储、访问和缓存方式

3. volatile 在C和C++ 中的含义

在 C 和 C++ 中,volatile 是一种类型限定符(类似于 const),并且是类型(例如变量或字段的类型)的一部分。( 注:这一点无疑,两种语言中均有此关键字 。)

C 和 C++ 中 volatile 关键字的行为,有时会被描述为抑制优化编译器的某些优化操作:(1) 不移除现有的 volatile 读写操作;(2) 不添加新的 volatile 读写操作;(3) 不重排 volatile 读写操作。然而,这种定义仅是为了便于初学者理解而给出的近似说法,在编写实际生产代码时不应依赖这一近似定义

在 C 语言(以及随后的 C++)中, volatile 关键字旨在:

允许访问内存映射 I/O 设备(内存变量对应设备寄存器)。

允许在执行 longjmp 时保留变量值。

允许通过 volatile sig_atomic_t 对象在信号处理程序与程序的其余部分之间共享变量值。

C 和 C++ 标准允许编写可移植代码,利用 volatile 对象在 longjmp 操作期间共享数据,也允许利用 volatile sig_atomic_t 对象在信号处理程序与程序的其余部分之间共享数据。除此之外,在 C 和 C++ 中使用 volatile 关键字均属于不可移植或不正确的做法 。特别是,针对内存映射 I/O 设备使用 volatile 关键字编写代码,本质上是不可移植的,且始终需要对特定的目标 C/C++ 实现及平台有深入的了解。

4. volatile 与多线程

一个常见的误解是认为 volatile 关键字在 C 和 C++ 的可移植多线程代码中很有用实际上,在任何多线程场景下,C 和 C++ 中的 volatile 关键字从未作为一种有用且可移植的工具发挥过作用 。与 Java 和 C# 编程语言不同,C 和 C++ 中针对 volatile 变量的操作并非原子操作也不具备足够的内存排序保证 (即内存屏障)。大多数 C 和 C++ 编译器、链接器及运行时环境根本不提供必要的内存排序保证,使得 volatile 关键字无法在任何多线程场景中发挥作用。在 C11 和 C++11 标准出台之前,程序员编写多线程代码时,不得不依赖特定实现或平台(如 POSIX 和 WIN32)所提供的保证。随着现代 C11 和 C++11 标准的引入,程序员现在可以使用诸如 std::atomic<T> 模板等新的可移植构造来编写可移植的多线程代码

5. C 语言中内存映射I/O的示例

在本例中,代码将存储在 foo 中的值设为 0。随后,它开始反复轮询该值,直到其变为 255 (注:在内存映射I/O中,foo的值由其它外部系统写到I/O口,从而映射到变量foo,即其值由外部赋予,而不是程序中改变,比如上位机写入下位机寄存器值)。

cpp 复制代码
static int foo;

void bar(void) {
    foo = 0;

    while (foo != 255) {}
}

优化编译器会发现,没有任何其他代码能够改变存储在 foo 中的值,并据此假定该值将始终保持为 0。因此,编译器会将函数体替换为一个类似如下的无限循环:

cpp 复制代码
void bar_optimized(void) {
    foo = 0;

    while (true) {}
}

若要防止编译器进行此项优化,可以使用 volatile 关键字

cpp 复制代码
static volatile int foo;

void bar(void) {
    foo = 0;

    while (foo != 255) {}
}

volatile 关键字阻止编译器将读取操作移出循环,因此代码能够检测到变量 foo 的预期变化。

**(1)**下面,我们在 Windows X64平台下用 VS2026 来编译该程序,看看有什么变化,在VS2026 的配置选项中,我们配置为最大优化,并生成Debug版。

在不加 volatile 关键字的情况下:

static int foo;

void bar(void) {

foo = 0;

while (foo != 255) {}

}

调用:

bar();

我们看看生成的反汇编代码:

bar();

00007FF6821F6E98 call bar (07FF6821D37BAh)

void bar(void) {

00007FF6821F2FB0 nop

foo = 0;

while (foo != 255) {}

00007FF6821F2FB2 jmp bar+2h (07FF6821F2FB2h)

从以上代码可以看出,编译器会认为,在本程序中,foo 的值不会改变,甚至都没有对foo初始化生成代码,认为 foo != 255 总是成立,因此都没有去检测 foo 的值是否有变化,直接生成一个死循环语句

在加 volatile 关键字的情况下:

bar();

00007FF6F6267474 mov dword ptr foo (07FF6F64A2A38h),0

00007FF6F626747E mov eax,dword ptr foo (07FF6F64A2A38h)

00007FF6F6267484 cmp eax,0FFh

00007FF6F6267489 jne wmain+0Eh (07FF6F626747Eh)

汇编代码说明:

00007FF6F6267474 mov dword ptr foo (07FF6F64A2A38h),0

这一句是对变量 foo的初始化,可以看出并没有把它优化掉。

00007FF6F626747E mov eax,dword ptr foo (07FF6F64A2A38h)

这一句将 foo 的值移入寄存大 eax 。

00007FF6F6267484 cmp eax,0FFh

将eax 的值与 0FFh (255) 的值进行比较,从此可以看出,所生成的代码会去检测 foo的值,看其是否发生变化。

00007FF6F6267489 jne wmain+0Eh (07FF6F626747Eh)

如果值不相等,则继续检测 foo 的值。

从以上可以看出,编译器不会对涉及变量 foo 的操作做本质上的优化。

(2) 现在看看在 CentOS 8.1(Linux) 中使用 gcc编译的情况。

没有加 volatile 的情况下,生成的汇编文件。

static int foo;

void bar(void) {

foo = 0;

while (foo != 255) {}

}

我们将文件命名为 example.c

gcc -S -O3 -masm=intel example.c -o example.s

生成的汇编文件为:

.file "example.c"

.intel_syntax noprefix

.text

.p2align 4,,15

.globl bar

.type bar, @function

bar:

.LFB22:

.cfi_startproc

.p2align 4,,10

.p2align 3

.L2:

jmp .L2

.cfi_endproc

.LFE22:

.size bar, .-bar

.ident "GCC: (GNU) 8.3.1 20190507 (Red Hat 8.3.1-4)"

.section .note.GNU-stack,"",@progbits

.L2:

jmp .L2

这句可以看出,直接被优化掉了,没有判断 foo 的值。

volatile 的情况下,生成的汇编文件。

.file "example.c"

.intel_syntax noprefix

.text

.p2align 4,,15

.globl bar

.type bar, @function

bar:

.LFB22:

.cfi_startproc

mov DWORD PTR foorip, 0

.p2align 4,,10

.p2align 3

.L2:

mov eax, DWORD PTR foorip

cmp eax, 255

jne .L2

ret

.cfi_endproc

.LFE22:

.size bar, .-bar

.local foo

.comm foo,4,4

.ident "GCC: (GNU) 8.3.1 20190507 (Red Hat 8.3.1-4)"

.section .note.GNU-stack,"",@progbits

注意生成的汇编代码:

mov eax, DWORD PTR foorip

cmp eax, 255

jne .L2

以上汇编代码可以看出,编译器没有对 foo 进行优化,每一次执行都判断了 foo的值。

6. C 中编译对比

以下 C 语言程序及对应汇编代码片段展示了 volatile 关键字如何影响编译器的输出结果。

观察汇编代码可以明显发现,涉及 volatile 对象的代码更为冗长,这是为了满足 volatile 对象的特性要求。volatile 关键字会阻止编译器对相关代码进行优化,从而确保对 volatile 变量的每一次赋值和读取操作都对应一次实际的内存访问(注:这种表述很流行,但这种说法是不正确的,因为 volatile 阻止优化,因此变量一定会占用内存栈,而不是优化为某个立即数,因此对变量的赋值和访问就老初按语句来,没有被优化当然是访问内存) 。若不使用 volatile 关键字,编译器便会认为无需在每次使用变量时都从内存重新读取,因为此时通常不存在其他线程或进程向该变量所在的内存位置进行写入操作的情况**( 注:这种表述同样很流行,但此说不正确的,只需牢记** volatile 修饰的变量必占用内存栈,任何对其操作都不会优化即可,不宜过度解读,以免引起混淆)。

(1) 在 Windows X64 平台。

不使用 volatile 关键字:

cpp 复制代码
void FunNoVolatile()
{
	// 永远不会在栈上创建这些变量
	int a = 10;
	int b = 100;
	int c = 0;
	int d = 0;

	/*
		`printf` 将使用参数 `"%d"` 和 `110`(编译器会计算 `a+b` 的和)进行调用,因此不会产生运行时执行加法运算的开销。
	*/
	printf("\n no valiatile a + b = %d \n", a + b);

	/*
	这段代码会因优化而被移除,但在调用 `printf` 时,仍能观察到 `c` 和 `d` 变为 100 产生的影响。
	*/
	a = b;
	c = b;
	d = b;

	/*
	编译器将生成调用 printf 的代码,传入的参数为 "%d" 和 200。
	*/
	printf("\n no volatile: c + d = %d \n", c + d);
}

对应汇编代码:

void FunNoVolatile()

{

00007FF70F4A81E0 sub rsp,28h

// 永远不会在栈上创建这些变量

int a = 10;

int b = 100;

int c = 0;

int d = 0;

/*

`printf` 将使用参数 `"%d"` 和 `110`(编译器会计算 `a+b` 的和)进行调用,因此不会产生运行时执行加法运算的开销。

*/

printf("\n no valiatile a + b = %d \n", a + b);

00007FF70F4A81E4 mov edx,6Eh

00007FF70F4A81E9 lea rcx,string "\\n no valiatile a + b = %d \\n" (07FF70F69F7A0h)

00007FF70F4A81F0 call printf (07FF70F492950h)

/*

这段代码会因优化而被移除,但在调用 `printf` 时,仍能观察到 `c` 和 `d` 变为 100 产生的影响。

*/

a = b;

c = b;

d = b;

/*

编译器将生成调用 printf 的代码,传入的参数为 "%d" 和 200。

*/

printf("\n no volatile: c + d = %d \n", c + d);

00007FF70F4A81F5 mov edx,0C8h

00007FF70F4A81FA lea rcx,string "\\n no volatile: c + d = %d \\n" (07FF70F69F860h)

}

00007FF70F4A8201 add rsp,28h

00007FF70F4A8205 jmp printf (07FF70F492950h)

说明:

从以上汇编代码可以看出,4个变量并没有占用内存栈。

00007FF70F4A81E4 mov edx,6Eh

6Eh = 110

编译器将 a + b = 110 直接优化成 110,然后作为立即数生成一条语句,即将 110 移入寄存器 edx ,作为参数传递给调用函数 printf 。

同理,下面的那句 c + d = C8 作为立即数生成一个语句:

00007FF70F4A81F5 mov edx,0C8h

使用 volatile 关键字:

cpp 复制代码
void FunWithVolatile()
{
	// 这些变量会使用栈存储
	volatile int a = 10;
	volatile int b = 100;
	volatile int c = 0;
	volatile int d = 0;

	printf("volatile: \n a + b = %d \n", a + b);

	a = b;
	c = b;
	d = b;

	printf("volatile: \n c+d = %d \n", c + d);
}

生成的汇编代码:

void FunWithVolatile()

{

00007FF71CEA8234 sub rsp,28h

// 这些变量会使用栈存储

volatile int a = 10;

00007FF71CEA8238 mov dword ptr rsp+38h,0Ah

volatile int b = 100;

volatile int c = 0;

volatile int d = 0;

printf("volatile: \n a + b = %d \n", a + b);

00007FF71CEA8240 lea rcx,string "volatile: \\n a + b = %d \\n" (07FF71D09F768h)

00007FF71CEA8247 mov dword ptr rsp+30h,64h

00007FF71CEA824F mov dword ptr rsp+48h,0

00007FF71CEA8257 mov dword ptr rsp+40h,0

00007FF71CEA825F mov edx,dword ptr b

00007FF71CEA8263 mov eax,dword ptr a

00007FF71CEA8267 add edx,eax

00007FF71CEA8269 call printf (07FF71CE92950h)

a = b;

00007FF71CEA826E mov eax,dword ptr b

c = b;

d = b;

printf("volatile: \n c+d = %d \n", c + d);

00007FF71CEA8272 lea rcx,string "volatile: \\n c+d = %d \\n" (07FF71D09F788h)

00007FF71CEA8279 mov dword ptr a,eax

00007FF71CEA827D mov eax,dword ptr b

00007FF71CEA8281 mov dword ptr c,eax

00007FF71CEA8285 mov eax,dword ptr b

00007FF71CEA8289 mov dword ptr d,eax

00007FF71CEA828D mov edx,dword ptr d

00007FF71CEA8291 mov eax,dword ptr c

00007FF71CEA8295 add edx,eax

}

00007FF71CEA8297 add rsp,28h

c = b;

d = b;

printf("volatile: \n c+d = %d \n", c + d);

00007FF71CEA829B jmp printf (07FF71CE92950h)

说明:从以上可以看出,4 个变量都占用了内存栈,而且每一次计算都老老实实从内存读取。

(2) 在 Linux X64 平台(CentOS 8.1,gcc)。

不使用 volatile 关键字:

.file "example.c"

.intel_syntax noprefix

.text

.section .rodata.str1.1,"aMS",@progbits,1

.LC0:

.string "\n no valiatile a + b = %d \n"

.LC1:

.string "\n no volatile: c + d = %d \n"

.text

.p2align 4,,15

.globl FunNoVolatile

.type FunNoVolatile, @function

FunNoVolatile:

.LFB22:

.cfi_startproc

sub rsp, 8

.cfi_def_cfa_offset 16

mov esi, 110

mov edi, OFFSET FLAT:.LC0

xor eax, eax

call printf

mov esi, 200

xor eax, eax

add rsp, 8

.cfi_def_cfa_offset 8

mov edi, OFFSET FLAT:.LC1

jmp printf

.cfi_endproc

.LFE22:

.size FunNoVolatile, .-FunNoVolatile

.ident "GCC: (GNU) 8.3.1 20190507 (Red Hat 8.3.1-4)"

.section .note.GNU-stack,"",@progbits

使用 volatile 关键字:

.file "example.c"

.intel_syntax noprefix

.text

.section .rodata.str1.1,"aMS",@progbits,1

.LC0:

.string "volatile: \n a + b = %d \n"

.LC1:

.string "volatile: \n c+d = %d \n"

.text

.p2align 4,,15

.globl FunWithVolatile

.type FunWithVolatile, @function

FunWithVolatile:

.LFB22:

.cfi_startproc

sub rsp, 24

.cfi_def_cfa_offset 32

mov edi, OFFSET FLAT:.LC0

mov DWORD PTR rsp, 10

mov DWORD PTR rsp+4, 100

mov DWORD PTR rsp+8, 0

mov DWORD PTR rsp+12, 0

mov esi, DWORD PTR rsp

mov eax, DWORD PTR rsp+4

add esi, eax

xor eax, eax

call printf

mov eax, DWORD PTR rsp+4

mov edi, OFFSET FLAT:.LC1

mov DWORD PTR rsp, eax

mov eax, DWORD PTR rsp+4

mov DWORD PTR rsp+8, eax

mov eax, DWORD PTR rsp+4

mov DWORD PTR rsp+12, eax

mov esi, DWORD PTR rsp+8

mov eax, DWORD PTR rsp+12

add rsp, 24

.cfi_def_cfa_offset 8

add esi, eax

xor eax, eax

jmp printf

.cfi_endproc

.LFE22:

.size FunWithVolatile, .-FunWithVolatile

.ident "GCC: (GNU) 8.3.1 20190507 (Red Hat 8.3.1-4)"

.section .note.GNU-stack,"",@progbits

相关推荐
c238563 小时前
Bug 猎手入门指南
c++·算法·bug
玖玥拾5 小时前
C++ 数据结构 八大基础排序算法专题
数据结构·c++·算法·排序算法
YYYing.5 小时前
【C++大型项目之高性能服务器框架 (七) 】Socket与ByteArray模块
服务器·c++·后端·框架·高性能·c/c++
从零开始的代码生活_6 小时前
C++ list 原理与实践:双向链表、迭代器与简化实现
开发语言·c++·后端·学习·算法·链表·list
无相求码6 小时前
罪魁祸首:for 括号后面那个"隐形分号"
c语言
hy.z_7776 小时前
【C语言】11. 深入理解指针1
c语言·开发语言
charlie1145141916 小时前
现代C++工程实践——WeakPtr 第二关:核心骨架与控制块
开发语言·c++
2401_869769596 小时前
内容9 string 1
c++
XH华8 小时前
C++语言第二章类和对象(下)
开发语言·c++