C/C++ volatile让你看的更明白

news/2024/6/29 12:16:18 标签: 编译器, 汇编, 多线程, 优化, compiler, 测试

转载一篇有关C/C++中volatile修饰符的文章,自己用VC++ 10.0测试了一下,与原文有一些不同,特论述如下,让你也让自己看的更明白。
链接:http://blog.sina.com.cn/s/blog_4e345ce70100rsc7.html

1. 为什么用volatile?

C/C++ 中的 volatile 关键字和 const 对应,用来修饰变量,通常用于建立语言级别的 memory barrier。这是 BS 在 "The C++ Programming Language" 对 volatile 修饰词的说明:

A volatile specifier is a hint to a compiler that an object may change its value in ways not specified by the language so that aggressive optimizations must be avoided.

volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。声明时语法:int volatile vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。例如:

//...
volatile int i = 10;
int a = i;
//...
// 其他代码,并未明确告诉编译器,对 i 进行过操作
int b = i;

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

下面编写程序并通过插入汇编代码,测试有无volatile关键字,对程序最终代码的影响

输入下面的代码:

#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);
}
发现上面的程序在Debug,Release模式下的输出都是:


这个程序在Debug,Release模式下的输出一致,说明默认情况下VC++ 10.0在两种模式下都做了代码优化,都造成了编译无法识别变量的改变

再输入下面的代码:

#include <stdio.h>

void main()
{
	volatile 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);
}

发现上面的程序在Debug模式下的输出也是:


但是在Release模式下的输出是:


上面这个程序在不同模式下的输出说明这个 volatile 关键字只在 Release 模式下才发挥了它的作用,在 Debug 模式下并没有达到想要的结果。这也说明,在使用 volatile 修饰符修饰变量时,需要注意程序在 Debug 模式和 Release 模式下的不同行为。


其实不只是“内嵌汇编操纵栈”这种方式属于编译无法识别的变量改变,另外更多的可能是多线程并发访问共享变量时,一个线程改变了变量的值,怎样让改变后的值对其它线程 visible。一般说来,volatile 用在如下的几个地方:
1) 中断服务程序中修改的供其它程序检测的变量需要加 volatile;
2) 多任务环境下各任务间共享的标志应该加 volatile;
3) 存储器映射的硬件寄存器通常也要加 volatile 说明,因为每次对它的读写都可能由不同意义。

自己的注解:自己在之前的 MCU 程序的编写,以及在多线程程序的编写中,从来没有用到 volatile 修饰符修饰变量。比如在单片机的 C51 程序设计中似乎身边的人都没有用过该修饰符,查阅《单片机的 C 语言应用程序设计》一书也没有讲解关于 volatile 的内容,这不禁让我怀疑是不是 Keil C51 编译器对这一块的默认支持,即 Keil C51 编译器就不会有上面的优化过程,这一点,自己还需要进一步考证。

2.volatile 指针

和 const 修饰词类似,const 有常量指针和指针常量的说法,volatile 也有相应的概念:

  • 修饰由指针指向的对象、数据是 const 或 volatile 的:

    const char* cpch;
    volatile char* vpch;

    注意:对于 VC,这个特性实现在 VC 8 之后才是安全的。

  • 指针自身的值——一个代表地址的整数变量,是 const 或 volatile 的:

    char* const pchc;
    char* volatile pchv;

注意:

(1) 可以把一个非 volatile int 赋给 volatile int,但是不能把非 volatile 对象赋给一个 volatile 对象

(2) 除了基本类型外,对用户定义类型也可以用 volatile 类型进行修饰。

(3) C++ 中一个有 volatile 标识符的类只能访问它接口的子集,一个由类的实现者控制的子集。用户只能用 const_cast 来获得对类型接口的完全访问。此外,  volatile 向 const 一样会从类传递到它的成员。

3. 多线程下的 volatile

有些变量是用 volatile 关键字声明的。当两个线程都要用到某一个变量且该变量的值会被改变时,应该用 volatile 声明,该关键字的作用是防止优化编译器把变量从内存装入  CPU 寄存器中。如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。volatile 的意思是编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值,如下:

volatile BOOL bStop = FALSE; 
(1)  在一个线程中: 
while( !bStop ) { ... } 
bStop = FALSE; 
return; 
(2)  在另外一个线程中,要终止上面的线程循环: 
bStop = TRUE; 
while( bStop ); //等待上面的线程终止

如果 bStop 不使用 volatile 申明,那么这个循环将是一个死循环,因为 bStop 已经读取到了寄存器中,寄存器中 bStop 的值永远不会变成 FALSE,加上 volatile,程序在执行时,每次均从内存中读出 bStop 的值,就不会死循环了。
这个关键字是用来设定某个对象的存储位置在内存中,而不是寄存器中。因为一般的对象编译器可能会将其的拷贝放在寄存器中用以加快指令的执行速度,例如下段代码中: 

... 
int nMyCounter = 0; 
for(; nMyCounter<100;nMyCounter++) 
{ 
... 
} 
... 
在此段代码中,nMyCounter 的拷贝可能存放到某个寄存器中(循环中,对 nMyCounter 的测试及操作总是对此寄存器中的值进行),但是另外又有段代码执行了这样的操作:nMyCounter  -=  1;这个操作中,对 nMyCounter 的改变是对内存中的 nMyCounter 进行操作,于是出现了这样一个现象:nMyCounter 的改变不同步。



http://www.niftyadmin.cn/n/1738569.html

相关文章

LLVM 与 Clang 介绍

晚上听国软徐纪元学弟给我说起了他刚装上的 Mac Lion 操作系统&#xff0c;也说到了最近 Apple 推出的新版本的 Xcode 4.2 在 iOS 4、iOS 5 程序开发中&#xff0c;加入了 automatic reference counting 功能&#xff0c;也即是开发者不再需要手动的做内存管理了&#xff0c;不…

程序员常用字符编码 ——从ASCII编码谈起...

1、ASCII-128编码 我们知道&#xff0c;在计算机内部&#xff0c;所有的信息最终都表示为一个二进制的字符串。每一个二进制位&#xff08;bit&#xff09;有0和1两种状态&#xff0c;因此八个二进制位就可以组合出256种状态&#xff0c;这被称为一个字节&#xff08;byte&…

最合适写代码的字体

最合适写代码的字体 每天盯着屏幕写代码&#xff0c;自然需要寻找一种看得舒服的字体&#xff0c;能让自己的代码赏心悦目&#xff0c;一般来说&#xff0c;我们选择用于显示代码的字体&#xff0c;有如下几个要求&#xff1a; 字母的宽度一致 或称为等宽字体&#xff0c;由于代…

Linux--exec函数族及system函数

自己有一段 Linux 下面的代码&#xff0c;需要用到 exec 函数和 system 函数&#xff0c;在 CSDN 上看到这样一篇还不错的帖子&#xff0c;就转载而来。 原文网址&#xff1a;http://blog.csdn.net/cnctloveyu/article/details/4129520 exec函数族包括6个函数&#xff1a; #in…

Linux system 函数返回值

又见不错的文章&#xff0c;原文地址&#xff1a;http://blog.csdn.net/cheyo/article/details/6595955 例&#xff1a; status system("./test.sh");1、先统一两个说法&#xff1a;&#xff08;1&#xff09;system返回值&#xff1a;指调用system函数后的返回值&a…

Linux 下监测指定路径下指定时间间隔内是否有指定的文件的生成

题目很拗口&#xff0c;感觉自己有必要说明一下&#xff0c;O(∩_∩)O~ 在 Liunx 程序设计中&#xff0c;有时我们需要写这样一个程序&#xff0c;当指定目录下有相应的新文件生成时&#xff0c;触发程序动作&#xff0c;这个触发的动作可能是解析新生成的文件异或其他行为。 …

JNA——JNI终结者

转载缘由&#xff1a; 今天&#xff0c;一位河海大学的学长和我讨论起 Java 代码调用本地 C/C DLL/so 的问题。以前我只知道 Java 中的 JNI 机制&#xff0c;但是 JNI 存在一个弊端&#xff1a;如果你有一个已经写的 DLL/so&#xff0c;简单的采用 JNI 机制调用 &#xff0c;是…

求解非齐次线性方程组算法

1. 非齐次线性方程组有解的条件 如下非齐次线性方程组&#xff1a; 由系数矩阵和常数列向量构成的增广矩阵如下&#xff1a; 无解情况&#xff1a; 唯一解情况&#xff1a; 无穷解情况&#xff1a; 2. 高斯消元法求解 步骤&#xff1a; 1&#xff09; 消元法 …