C语言 volatile用法

本文详细介绍了C/C++中volatile关键字的使用场景与原理,包括如何防止编译器优化、确保变量值的稳定获取以及在多线程和中断环境中的应用。

 

嵌入式编程中经常用到 volatile这个关键字,在网上查了下他的用法可以归结为以下两点:

一:告诉compiler不能做任何优化

   比如要往某一地址送两指令:  

   int   *ip   =...;   //设备地址  

   *ip   =   1;   //第一个指令  

   *ip   =   2;   //第二个指令  

   以上程序compiler可能做优化而成:  

   int   *ip   =   ...;  

   *ip   =   2;  

   结果第一个指令丢失。如果用volatile, compiler就不允许做任何的优化,从而保证程序的原意:  

   volatile   int   *ip   =   ...;  

   *ip   =   1;  

   *ip   =   2;  

   即使你要compiler做优化,它也不会把两次付值语句间化为一。它只能做其它的优化。这对device   driver程序员很有用。

二:表示用volatile定义的变量会在程序外被改变,每次都必须从内存中读取,而不能把他放在cache或寄存器中重复使用。

   如  volatile   char   a;   

       a=0;  

       while(!a){  

          //do   some   things;   

      }   

      doother();  

   如果没有   volatile   doother()不会被执行

 

   volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改。

用volatile关键字声明的变量i每一次被访问时,执行部件都会从i相应的内存单元中取出i的值。

没有用volatile关键字声明的变量i在被访问的时候可能直接从cpu的寄存器中取值(因为之前i被访问过,也就是说之前就从内存中取出i 的值保存到某个寄存器中),之所以直接从寄存器中取值,而不去内存中取值,是因为编译器优化代码的结果(访问cpu寄存器比访问ram快的多)。

以上两种情况的区别在于被编译成汇编代码之后,两者是不一样的。之所以这样做是因为变量i可能会经常变化,保证对特殊地址的稳定访问。

volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

使用该关键字的例子如下:

int volatile nVint;

当要求使用volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。

例如:

volatile int i=10;

int a = i;

...

//其他代码,并未明确告诉编译器,对i进行过操作

int b = i;

volatile 指出 i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在b中。而优化做法是,由于编译器发现两次从i读数据的代码之间的代码没有对i进行过操作,它会自动把上次读的数据放在b中。而不是重新从i里面读。这样以来,如果i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile可以保证对特殊地址的稳定访问。

注意,在vc6中,一般调试模式没有进行代码优化,所以这个关键字的作用看不出来。下面通过插入汇编代码,测试有无volatile关键字,对程序最终代码的影响:

首先,用classwizard建一个win32 console工程,插入一voltest.cpp文件,输入下面的

代码:

#include <stdio.h>

void main()

{

int i=10;

int a = i;

printf( "i= %d/n",a);

//下面汇编语句的作用就是改变内存中i的值,但是又不让编译器知道

__asm {

mov dword ptr [ebp-4], 20h

}

int b = i;

printf("i= %d/n",b);

}

然后,在调试版本模式运行程序,输出结果如下:

i = 10

i = 32

然后,在release版本模式运行程序,输出结果如下:

i = 10

i = 10

输出的结果明显表明,release模式下,编译器对代码进行了优化,第二次没有输出正确的i值。

下面,我们把 i的声明加上volatile关键字,看看有什么变化:

#include <stdio.h>

void main()

{

volatile int i=10;

int a = i;

printf( "i= %d/n",a);

__asm {

mov dword ptr [ebp-4], 20h

}

int b = i;

printf("i= %d/n",b);

}

分别在调试版本和release版本运行程序,输出都是:

i = 10

i = 32

这说明这个关键字发挥了它的作用!

 

源文档 <http://my.oschina.net/xiaoyunhu/blog/6272>

 

 

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:

1) 并行设备的硬件寄存器(如:状态寄存器) 

2)  一个中断服务子程序中会访问到的非自动变量(Non-automatic variables) 

3)  多线程应用中被几个任务共享的变量 

回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。嵌入式系统程序员经常同硬件、中断、RTOS等等打交道,这些都要求volatile变量。不懂得volatile内容将会带来灾难。 

假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是不是直正完全懂得volatile的重要性。

1) 一个参数既可以是const还可以是volatile吗?解释为什么。

2) 一个指针可以是volatile 吗?解释为什么。

3) 下面的函数有什么错误:

int square(volatile int *ptr) 

   return *ptr * *ptr; 

}

下面是答案:

1) 是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。

2) 是的。尽管这并不很常见。一个例子是当一个中断服务子程序修改指向一个buffer的指针时。

3) 这段代码里有个恶作剧。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:

int square(volatile int *ptr) 

{

  int a,b;

  a = *ptr;

  b = *ptr;

  return a * b;

}

由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返回值不是你所期望的平方值!正确的代码如下:

long square(volatile int *ptr)

{

  int a;

  a = *ptr;

  return a * a;

}

================================================================================ 

volatile的本意是“易变的”

由于访问寄存器的速度要快过RAM,所以编译器一般都会作减少存取外部RAM的优化。比如:

 

static int i=0; 

 

int main(void) 

  ... 

  while (1) 

  { 

    if (i) dosomething(); 

  } 

 

/* Interrupt service routine. */ 

void ISR_2(void) 

  i=1; 

 

程序的本意是希望ISR_2中断产生时,在main当中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远也不会被调用。如果将将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中i也应该如此说明。

 

一般说来,volatile用在如下的几个地方:

1、中断服务程序中修改的供其它程序检测的变量需要加volatile;

2、多任务环境下各任务间共享的标志应该加volatile;

3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能有不同意义;

 

另外,以上这几种情况经常还要同时考虑数据的完整性(相互关联的几个标志读了一半被打断了重写),在1中可以通过有关中断来实现,2中可以禁止任务调度,3中则只能依靠硬件的良好设计了。

 

volatile 的含义

volatile总是与优化有关,编译器有一种技术叫做数据流分析,分析程序中的变量在哪里赋值、在哪里使用、在哪里失效,分析结果可以用于常量合并,常量传播等优化,进一步可以使代码消除。但有时这些优化不是程序所需要的,这时可以用volatile关键字禁止做这些优化,volatile的字面含义是易变的,它有下面的作用:

 

1、不会在两个***作之间把volatile变量缓存在寄存器中。在多任务、中断、甚至setjmp环境下,变量可能被其他的程序改变,编译器自己无法知道,volatile就是告诉编译器这种情况。

 

2、不做常量合并、常量传播等优化,所以像下面的代码:

volatile int i = 1; 

if (i > 0) ... 

 

if的条件不会当作无条件真。

 

3、对volatile变量的读写不会被优化掉。如果你对一个变量赋值但后面没用到,编译器常常可以省略那个赋值***作,然而对Memory Mapped IO的处理是不能这样优化的。

 

前面有人说volatile可以保证对内存操作的原子性,这种说法不大准确,其一,x86需要LOCK前缀才能在SMP下保证原子性,其二,RISC根本不能对内存直接运算,要保证原子性得用别的方法,如atomic_inc。

 

对于jiffies,它已经声明为volatile变量,我认为直接用jiffies++就可以了,没必要用那种复杂的形式,因为那样也不能保证原子性。

 

你可能不知道在Pentium及后续CPU中,下面两组指令:

 

inc jiffies

;;

mov jiffies, %eax

inc %eax

mov %eax, jiffies

 

作用相同,但一条指令反而不如三条指令快。

 

 

================================================================================ 

 

关键在于两个地方: 

 

1、编译器的优化

 

在本线程内,当读取一个变量时,为提高存取速度,编译器优化时有时会先把变量读取到一个寄存器中;以后,再取变量值时,就直接从寄存器中取值;当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以便保持一致;当变量在因别的线程等而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致;当该寄存器在因别的线程等而改变了值,原变量的值不会改变,从而造成应用程序读取的值和实际的变量值不一致。

 

举一个不太准确的例子:

发薪资时,会计每次都把员工叫来登记他们的银行卡号,一次会计为了省事,没有即时登记,用了以前登记的银行卡号,刚好一个员工的银行卡丢了,已挂失该银行卡号,从而造成该员工领不到工资。

 

员工 --原始变量地址 

银行卡号 --原始变量在寄存器的备份 

 

 

2、在什么情况下会出现

 

1) 并行设备的硬件寄存器(如:状态寄存器)

2) 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)

3) 多线程应用中被几个任务共享的变量

 

 

补充:volatile应该解释为“直接存取原始内存地址”比较合适,“易变的”这种解释简直有点误导人;

“易变”是因为外在因素引起的,象多线程,中断等,并不是因为用volatile修饰了的变量就是“易变”了,假如没有外因,即使用volatile定义,它也不会变化;

而用volatile定义之后,其实这个变量就不会因外因而变化了,可以放心使用了;大家看看前面那种解释(易变的)是不是在误导人。

 

 

volatile对应的变量可能在你的程序本身不知道的情况下发生改变,比如多线程的程序,共同访问的内存当中,多个程序都可以操纵这个变量,你自己的程序,是无法判定这个变量会发生变化;还比如,他和一个外部设备的某个状态对应,当外部设备发生操作的时候,通过驱动程序和中断事件,系统改变了这个变量的数值,而你的程序并不知道。

对于volatile类型的变量,系统每次用到他的时候都是直接从对应的内存当中提取,而不会利用cache当中的原有数值,以适应它的未知何时会发生的变化,系统对这种变量的处理不会做优化——显然也是因为它的数值随时都可能变化的情况。

 

--------------------------------------------------------------------------------------

 

典型的例子:

for ( int i=0; i<100000; i++);

这个语句用来测试空循环的速度的

但是编译器肯定要把它优化掉,根本就不执行

如果你写成:

for ( volatile int i=0; i<100000; i++);

它就会执行了

 

源文档 <http://gyy.javaeye.com/blog/166678>

 

<think>嗯,用户想了解C语言volatile关键字的详细解释和使用方法。这个问题很专业,需要系统性地整理volatile的概念、语法、使用场景和注意事项。 用户提供的引用资料很有价值,特别是引用[1]和引用[2]详细说明了volatile的作用机制和使用场景。引用[1]从基础概念入手,解释了volatile如何防止编译器优化;引用[2]则重点强调了多线程和硬件编程中的注意事项;引用[3]虽然主要讲char类型,但也展示了C语言关键字的具体用法示例。 准备从这几个维度组织回答:先精确定义volatile的作用原理,用寄存器缓存优化的例子说明必要性;然后展示标准语法格式;接着分类讨论硬件寄存器访问、中断服务程序、多线程共享变量等典型场景;最后列举"中断标志位未声明volatile导致程序错误"等常见陷阱。所有代码示例都会用$...$格式标注行内变量,比如$volatile int flag$。 用户可能是嵌入式开发者或系统程序员,因为volatile在应用层编程中很少见。ta特意要求"详解",说明需要实践指导而不仅是理论定义。考虑在回答末尾添加"如何调试volatile相关错误"等实用问题,帮助ta深入理解。 特别注意要区分volatile和原子操作的区别——这是很多初学者容易混淆的点。引用[2]提到"不能保证原子性"这点很重要,要在注意事项里强调。另外用户提供的示例代码中有个字符串初始化错误(char b="a"应为char *b="a"),不过这与volatile无关就不展开了。</think>好的,我们来详细解析C语言中的`volatile`关键字,包括其概念、作用、使用方法和关键注意事项。 **`volatile` 关键字详解** 1. **概念与作用** * `volatile` 是C语言中的一个类型修饰符(Type Qualifier),它告诉编译器:**被它修饰的变量的值可能会在意料之外的情况下被改变**。 * 主要目的是**防止编译器进行可能影响程序正确性的优化**。 * 编译器优化通常假设程序在单线程环境中顺序执行,变量值的变化完全由当前线程内的代码控制。因此,编译器可能会: * **缓存变量到寄存器**:如果发现某个变量在代码块内被多次读取且没有写入操作,编译器可能将其值加载到寄存器中,后续读取直接使用寄存器值而不是每次都去内存读取。这能提高速度。 * **省略看似冗余的读写**:如果发现代码对变量进行了写入操作,但后续没有读取就再次写入,或者读取后没有使用,编译器可能会省略掉一些它认为“冗余”的读写操作。 * **问题所在**:对于某些特殊变量,其值的变化并非由当前线程内的代码直接控制,而是由: * **硬件**:映射到内存地址的硬件寄存器(如状态寄存器、数据寄存器),其值会随外部事件(按键、数据到达、中断发生)而改变。 * **其他线程/进程**:在多线程/多任务环境中,一个线程修改了共享变量,另一个线程需要感知这个变化。 * **信号处理程序/中断服务程序(ISR)**:主程序(或另一个线程)中的变量可能被ISR修改。 * **`volatile` 的作用**:告知编译器,**每次需要访问这个变量时,都必须直接从其内存地址读取最新的值;每次修改这个变量时,都必须立即将其值写回内存地址**。编译器不能假设其值不变,也不能缓存它或省略对其的读写操作。 2. **语法和用法** * `volatile` 在声明变量时使用,放在类型说明符之前或之后均可(但通常放在前面)。它可以与其他类型修饰符(如`const`)组合使用。 * **基本语法**: ```c volatile int sensor_value; // 声明一个易变的整型变量 volatile char *status_register; // 声明一个指向易变字符的指针 char volatile *data_port; // 同上,volatile位置可互换(修饰指向的对象) int *volatile ptr_to_int; // 声明一个易变的指针(指针本身易变,指向的内容不一定) volatile const int read_only_reg; // 声明一个只读的易变整型变量(硬件常量寄存器) ``` * **关键点**: * `volatile` 修饰的是变量本身或其指向的对象(取决于`*`的位置)。 * `volatile` 影响的是编译器如何生成访问该变量的代码(强制内存访问),而不是变量本身的存储位置(它仍然在内存中)。 3. **典型使用场景** * **硬件寄存器访问 (Memory-Mapped I/O)**: ```c #define PORT_A (*(volatile unsigned char *)0x10000000) void main() { unsigned char data; PORT_A = 0xFF; // 向地址0x10000000写入数据 data = PORT_A; // 从地址0x10000000读取数据 } ``` 硬件寄存器的值随时可能被外部设备改变(如串口接收数据),编译器绝不能缓存或优化掉对其的读写。`volatile` 确保每次`PORT_A`的读取都真实访问硬件地址。 * **中断服务程序(ISR)修改的全局变量**: ```c volatile int data_ready = 0; // 主循环和ISR共享的标志 void UART_ISR(void) { // ... 接收数据 ... data_ready = 1; // ISR中设置标志 } int main() { while(1) { if (data_ready) { // 主循环检测标志 process_data(); data_ready = 0; } // ... 其他任务 ... } } ``` 如果`data_ready`没有声明为`volatile`,编译器优化可能认为`while(1)`循环内`data_ready`的值不会变化(因为主循环内没有修改它的代码),从而将`if (data_ready)`优化成只检查一次或使用寄存器中的旧值(很可能是0),导致程序无法感知ISR设置的标志。 * **多线程共享变量**: ```c volatile int shared_counter = 0; void thread_func1() { while (1) { shared_counter++; // 线程1修改共享变量 } } void thread_func2() { while (1) { printf("Counter: %d\n", shared_counter); // 线程2读取共享变量 } } ``` 在多线程环境中,如果没有同步机制(如互斥锁),`volatile` **不能替代锁来保证原子性**(`shared_counter++`不是原子操作)。但`volatile`能确保线程2每次读取`shared_counter`时都去内存获取最新值,而不是使用可能过时的寄存器缓存值。*注意:多线程共享数据通常需要锁或原子操作,`volatile`只解决可见性问题的一部分。* * **信号处理程序修改的全局变量**:类似于ISR修改全局变量的情况。 4. **重要注意事项** * **`volatile` 不保证原子性**:`volatile` **只强制内存访问,不提供任何互斥或同步机制**。像`x++`(读-改-写)这样的操作在多线程或多核环境中,即使`x`是`volatile`的,仍然可能被打断,导致数据竞争(Data Race)。解决原子性问题需要使用锁(互斥量)、信号量或编译器/平台提供的原子操作(如C11的`_Atomic`或GCC的`__atomic_*`内置函数)。 * **`volatile` 与 `const`**:可以组合使用。`volatile const`表示变量本身是只读的(程序不能修改它),但其值可能被外部因素改变(如只读的状态寄存器),编译器仍需每次都读取其值。`const volatile int *p`表示`p`指向一个`volatile const int`。 * **指针的 `volatile`**: * `volatile int *p;`:指针`p`指向一个`volatile int`(指向的对象是易变的)。 * `int *volatile p;`:指针`p`本身是易变的(指针变量的值可能被意外改变,比如指向不同的硬件寄存器地址),但其指向的`int`对象不一定易变。 * `volatile int *volatile p;`:指针`p`本身和它指向的对象都是易变的。 * **编译器优化屏障**:`volatile`变量访问本身会阻止编译器对内存访问顺序进行某些重排优化,但**它不能阻止CPU层面的指令重排**。在需要严格内存顺序保证的低级编程(如设备驱动、无锁编程)中,可能需要使用CPU提供的**内存屏障(Memory Barrier/Fence)指令**(如`__sync_synchronize()` in GCC)配合`volatile`使用。 * **不要滥用**:不必要的使用`volatile`会阻止编译器进行有益的优化,降低程序性能。只在确实需要防止编译器优化导致程序逻辑错误时才使用它。 * **初始化**:`volatile`变量可以像普通变量一样初始化。`volatile int x = 10;`是合法的。 5. **示例代码 (结合场景)** ```c #include <stdio.h> #include <signal.h> #include <unistd.h> volatile sig_atomic_t flag = 0; // 用于信号处理程序的标志,使用volatile和sig_atomic_t void handle_signal(int sig) { flag = 1; // 信号处理程序修改全局标志 } int main() { signal(SIGINT, handle_signal); // 注册SIGINT (Ctrl+C) 信号处理函数 printf("程序运行中... 按Ctrl+C终止。\n"); while (!flag) { // 主循环检查volatile标志 // 模拟工作 sleep(1); printf("等待信号...\n"); } printf("收到终止信号,程序退出。\n"); return 0; } ``` **说明**: * 信号处理函数`handle_signal`异步地(在主程序执行`while`循环时随时可能被调用)修改全局变量`flag`。 * 如果`flag`没有声明为`volatile`,编译器优化可能将`while (!flag)`优化成`while (true)`(如果它检测到主循环内没有修改`flag`的代码),或者使用寄存器中的旧值(0),导致程序无法响应Ctrl+C信号。 * `volatile`确保每次循环条件检查都从内存读取`flag`的最新值。 * `sig_atomic_t`是一个整数类型,保证在信号处理程序中进行原子读写(通常是一个`int`),**配合`volatile`使用**是处理异步信号标志的常见安全做法[^1][^2]。 6. **总结** `volatile`关键字是C语言中用于处理特殊变量的重要工具,其核心作用是**告知编译器不要对被修饰变量的读写操作进行优化,强制每次访问都直接与内存交互**。它主要用于以下场景: * 访问映射到内存地址的**硬件寄存器**。 * 被**中断服务程序(ISR)**修改的全局变量。 * 被**信号处理程序**修改的全局变量。 * 在特定情况下(需谨慎),**多线程环境**中共享的、作为简单标志的全局变量(但需理解其局限性,不能替代锁或原子操作)。 使用`volatile`的关键是理解编译器优化的行为以及特定场景下变量值被“意外”修改的可能性。正确使用`volatile`能避免隐蔽的程序逻辑错误,尤其是在底层系统编程和嵌入式开发中至关重要[^1][^2]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值