这是一个类型修饰符,位置同const、static等。一个使用volatile修饰的变量,比如volatile int i; 每次对该变量的直接引用,都会访问内存,而不是从寄存器中读取(如果其已经在寄存器中)。这样一来,volatile似乎没什么用处,反倒会使数据的读取相对变慢很多。但是,如果没有volatile,编译器可能会优化你的程序,使得数据从寄存器中读取,从而加快程序的运行,但如果这个变量是同其它进程/线程共享的,就可能造成数据的不一致。多线程情况下,你可以使用互斥机制来保证对共享数据访问的原子性。但是,在单片机等嵌入式环境中,硬件经常不会有这种互斥机制的支持,这时某些共享的数据(比如端口)就可能会产生不一致的情况。而使用volatile就会使编译器不对代码进行优化,每次对该变量的访问都会从内存中读取。
下面通过观察使用volatile前后编译产生的汇编代码的不同,来加深对volatile关键词的理解。
对于下面的C代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #include <stdio.h> int foo(void) { int i = 0; while(i++ < 20); return i; } int main(int argc, char **argv) { int i = foo(); int j = i; int k = i; printf("%d, %d, %d\n", i, j, k); return 0; } |
1 | $ gcc -S -O1 main.c # 优化等级1 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | [ ... ] .LC0: .string "%d, %d, %d\n" [ ... ] main: [ ... ] call foo # 调用函数foo,返回值保存至寄存器eax movl %eax, 16(%esp) # 参数k入栈 movl %eax, 12(%esp) # 参数j入栈 movl %eax, 8(%esp) # 参数i入栈 movl $.LC0, 4(%esp) # 格式字符串地址入栈 movl $1, (%esp) call __printf_chk movl $0, %eax leave ret [ ... ] |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | [ ... ] main: [ ... ] call foo # 调用函数foo,返回值保存至寄存器eax movl %eax, 44(%esp) # 为i赋值 movl 44(%esp), %edx # 读取i值 movl 44(%esp), %ecx # 读取i值 movl 44(%esp), %eax # 读取i值 movl %ecx, 16(%esp) # 参数k入栈,使用i值 movl %edx, 12(%esp) # 参数j入栈,使用i值 movl %eax, 8(%esp) # 参数i入栈 movl $.LC0, 4(%esp) # 格式字符串地址入栈 call __printf_chk movl $0, %eax leave ret [ ... ] |
volatile vs. const
正像上面所描述的那样,volatile的意思是“易变的”。C/C++中还有另外一个关键词叫const,用来限定变量不可被改变。似乎volatile和const意思相反,水火不容。但volatile const int N = 0;这样的语句是允许存在的。为什么呢?其实,这个语句可以这样来理解:变量N在本程序(或者设备)中是不可(禁止)改变的,但它可能会被其它程序(或者设备)改变。你或许知道,const只是编译器的限定,而没有执行期的检查,因此const也只是限于单个进程,因此volatile和const这两个关键词并不冲突。另外,普通的整型const变量,比如const int N = 10; int a[N]; 中,变量N可能并不会被分配内存(除非程序里面有显式使用N地址的表达式)。但是,根据volatile变量的特性,volatile const int N = 10;中变量N是一定会占用内存的。这一点,亦可以通过观察相应的汇编代码来验证。
没有评论:
发表评论