深入解析C语言中volatile关键字的含义与多线程应用
C语言丨深入理解volatile关键字

这篇文章详细分析介绍了 C 语言中关键字 volatile 的含义,希望能在学习方面帮助大家。
volatile 是一种类型修饰符。它的作用是用于修饰那些会被不同线程访问和修改的变量。如果没有 volatile ,可能会出现两种情况:一是无法编写多线程程序;二是编译器会失去大量优化的机会。
1.原理作用
“Volatile 的意思是‘易变的’,将其解释为‘直接存取原始内存地址’更为恰当。之所以是‘易变的’,是因为会受到外在因素的影响,例如多线程、中断等情况。”
C语言书籍这样定义volatile关键字:
volatile 会提醒编译器,后面所定义的变量随时都有可能发生改变。所以,在编译后的程序每次需要存储或读取这个变量时,编译器会被告知不对该变量进行优化,而是直接从变量的内存地址中读取数据。这样就能提供对特殊地址的稳定访问。
如果没有 volatile 关键字,编译器有可能会对读取和存储进行优化。它有可能会暂时使用寄存器中的值。如果这个变量被别的程序更新了,就会出现不一致的现象。简洁来说,volatile 关键词会对编译器的编译结果产生影响。被 volatile 声明的变量意味着该变量随时可能发生变化。对于与该变量有关的运算,不要进行编译优化,否则可能会出错。
2.一般用处
一般说来,volatile用在如下的几个地方:
1)并行设备的硬件寄存器(如:状态寄存器)
存储器映射的硬件寄存器通常需要加上 volatile 。因为每次对它进行读写操作时,其意义可能都不相同。
假设要对一个设备进行初始化,这个设备有一个寄存器,它的值是 0xff800000。
定义一个 IO 端口,其地址为 (unsigned int *)0xff800000,用 int *output 来表示;在 init 函数中,进行如下操作:定义一个整型变量 i,然后通过一个 for 循环,从 i = 0 开始,当 i 小于某个条件时执行循环体,循环体为空。< 10;i++){ *output = i; } }
编译器经过优化后,认为前面的循环毫无意义,对最终结果没有影响。因为最终只是将 output 这个指针赋值为 9,所以编译器最后编译的代码结果就相当于:
*output 被赋值为 9 ;
如果你对该外部设备进行初始化的过程必须按照上面代码那样依次赋值,那么显然优化过程无法达到目的。反之,如果你不是对该端口进行反复写操作,而是反复进行读操作,结果是相同的。编译器在优化后,或许你的代码对该地址的读操作只进行了一次。但从代码角度来看,是没有任何问题的。这时候就该使用 volatile 。它的作用是通知编译器这个变量是不稳定的。在遇到此变量时,编译器不要进行优化。
中断服务程序中进行修改的、供其他程序检测的变量,需要加上 volatile 这个关键字;
变量在触发某中断程序时被修改,编译器判断主函数中未修改该变量,所以可能仅执行一次从内存到某寄存器的读操作,之后每次就只会从该寄存器中读取变量的副本,这样就导致中断程序的操作被短路了。
在多任务环境下,各任务之间共享的标志,应当加上 volatile 这个关键字。
在本次线程中,读取一个变量时,编译器优化有时会先将变量读取到一个寄存器里;接着,再取变量值时,会直接从寄存器中获取;若内存变量或寄存器变量因其他线程等原因而改变了值,该寄存器的值不会随之改变,进而导致应用程序读取的值与实际的变量值不一致。
存储器映射的硬件寄存器通常需要加 volatile 说明。因为每次对它进行读写操作时,其意义可能都不相同。
假设要对一个设备进行初始化操作。这个设备有一个特定的寄存器,其地址为 0xff800000。接着,通过一个循环来进行相关操作,循环变量为 i,从 0 开始,当 i 小于某个条件时执行循环体,在循环体中进行与该寄存器相关的一些处理步骤。< 10;i++) *output = i;前面循环半天都是废话,对最后的结果毫无影响,因为最终只是将output这个指针赋值为9,省略了对该硬件IO端口反复读的操作。
这是一个最基本的问题,用于区分 C 程序员和嵌入式系统程序员:嵌入式系统程序员常常会与硬件、中断、RTOS 等进行交互,而所有这些情况都需要使用 volatile 变量。如果不了解 volatile 的内容,将会引发灾难。
3.volatile 问题和总结
volatile 常见的几个面试题:
1)一个参数既可以是const还可以是volatile吗?
可以有只读的状态寄存器。这种状态寄存器是 volatile 的,原因在于它有可能被意外地改变。同时,它又是 const 的,这是因为程序不应该试图对其进行修改。
2) 一个指针可以是volatile 吗?
可以,当一个中服务子程序修改一个指向buffer的指针时。
4.下面的函数有什么错误?
函数 square 接收一个 volatile 类型的 intptr 参数,其返回值为 ptr 乘以 ptr 的结果。
该程序的目的是返回指针 ptr 所指向值的平方。然而,因为 ptr 指向一个 volatile 型参数,所以编译器会生成类似以下的代码。
定义一个函数 square,它接收一个指向 volatile 整型的指针 ptr。在函数内部,定义两个整型变量 a 和 b。将指针 ptr 所指向的值赋给变量 a,再将指针 ptr 所指向的值赋给变量 b。最后,返回 a 与 b 的乘积。
因为*ptr 的值有可能被意外地更改,所以 a 和 b 有可能是不一样的。这样一来,这段代码有可能返回的不是你所期望的平方值!正确的代码如下:
最后返回 a 与 a 的乘积
频繁使用 volatile 很有可能会致使代码尺寸增大并且降低性能,所以应当合理地运用 volatile 。
总结:
volatile 关键字属于类型修饰符。用它来声明的类型变量,意味着这类变量可能会被某些编译器未知的因素所更改。它能提醒编译器,其后面所定义的变量随时都有改变的可能。所以,编译后的程序在每次需要存储或读取这个变量时,都会直接从变量地址中去读取数据。如果没有 volatile 关键字,编译器可能会优化读取和存储操作。它可能会暂时使用寄存器中的值。如果这个变量被别的程序更新了,就会出现不一致的现象。所以,当遇到用这个关键字声明的变量时,编译器就不会对访问该变量的代码进行优化,这样就能提供对特殊地址的稳定访问。