- 上一篇文章学习了C/C++中的指针与数组的区别,点击链接进行查看:【软件开发底层知识修炼】二十七 C/C++中的指针与数组是不同的
- 本篇文章将学习volatile关键字在C/C++中的作用
1 实例代码分析
在讲解volatile关键字的作用之前,我们先来看一个代码的例子,代码如下:
- main.c
#include <stdio.h>
#include <pthread.h>
extern const int g_ready;
int main()
{
launch_device();
while( g_ready == 0 )
{
sleep(1);
printf("main() : g_ready = %d\n", g_ready);
}
printf("main() : g_ready = %d\n", g_ready);
return 0;
}
- device.c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int g_ready = 0;
void* th_fn(void* args)
{
sleep(5);
g_ready = 1;
printf("th_fn() : g_ready = %d\n", g_ready);
}
void launch_device()
{
pthread_t tid = 0;
pthread_create(&tid, NULL, th_fn, NULL);
}
上面的代码,是非常容易懂的,这里就不再多说。我们直接编译运行看看:
- gcc -pthread main.c device.c -o test.out
- ./test.out
运行结果如下:
这个结果对于我们来说其实挺容易懂的。就是main中的while循环先每隔一秒打印执行一次,然后5秒后th_fn函数开始执行,th_fn函数将g_ready变量变为1,然后打印一个语句。最后回到main中的while循环,由于while中上次最后一秒现在才执行完,打印一句,然后由于此时g_ready为1,所以退出循环,然后打印循环外的一个语句,然后结束程序的运行。这种结果,其实是比较容易理解的。也是比较普遍的结果。
但是如果我们再编译上述代码的时候,加上了-O3选项,该选项是优化选项,且级别比较高。编译运行结果如下:
- gcc -O3 -pthread main.c device.c -o test.out
- ./test.out
运行结果如下动态图:
由以上结果我们可以看到,程序陷入了死循环,g_ready没有被改变一直都是0,所以main中的while循环一直在执行。对于这个结果,可能并不是所有人都知道为什么。下面,我们就要来讲解为什么会产生上面的运行结果。
2 问题分析
-O3选项是让编译器对代码进行优化。
- 编译优化时,编译器根据当前文件进行优化。
- 为了效率上的提高,优化时编译器将变量值从内存中读取进入寄存器
- 每次要访问变量时直接从寄存器读取。毕竟寄存器的存取比内存的存取快很多。
那么,我们明白了优化对变量的影响。对于上述的代码,如果在编译时加了-O3选项。编译优化时,编译器会将变量g_ready的值放到寄存器中,以后每次使用该变量就从寄存器中取出g_ready的值使用即可。这样虽然是速度快了,但是当在th_fn函数中改变了g_ready的值后,在内存中确实g_ready的值已经变为1了,但是在刚刚那个寄存器中,最开始将g_ready的值也就是0存进去的,一直都没有改变过,但是呢,由于编译器的优化,每次在使用g_ready的变量的时候,都是从寄存器中直接取出值来使用…
说到这里,应该都能够明白了,此时从寄存器中取出的值就一直都是0.然后while一直循环。
3 解决方案
编译的时候使用-O3选项是很常用的。那么如何才能既使用这个-O3选项,又使得上述程序按照我们的意愿来执行呢?volatile就此出场。
使用volatile关键字修饰可能被意外修改的变量(内存),从而禁止编译器对该变量进行优化。
- volatile一般修饰的是一种易变的变量
- volatile可理解为一种编译器警告指示字,它告诉编译器必须每次都直接去内存中取变量值。
那么在上述的代码中,我们将main.c中的extern const int g_ready;
改为:extern volatile const int g_ready;
再重新进行优化的编译,然后运行,结果就是正确的运行。可以自己尝试做实验!
4 拓展: const和volatile
细心的朋友会发现,在main.c程序中,我们改完后,成这样了:extern volatile const int g_ready;
又是const又是volatile的。我们上面说过,volatile修饰的是意变的变量,而const修饰的变量是不能被修改的,有的人叫它常量,不可变的。而实际上在编程语言中的解释是这样的:const修饰的变量在当前文件中不能够出现在赋值符号的左边。 所以我们可以看到,在main.c这个文件中g_ready这个变量,并没有出现在赋值符号的左边,所以是没有问题。但是在device.c文件中g_ready是没有被const修饰的,它就可以出现在赋值符号的左边,可以被改变。这样一来,在device.c中修改g_ready这个变量,在main.c中的g_ready也间接被改变了。所以,我觉得说const修饰的是常量这个说法不够准确,说它是变量但是不能够出现在赋值符号的左边更加准确。
下面就总结一下const和volatile关键字:
- const表示修饰的变量在当前文件中不能出现在赋值符号的左边,不能直接被改变,但是可以间接被改变
- volatile表示修饰的变量每次使用的时候直接从内存中读取
- const和volatile同时修饰变量时互不影响。
4 总结
- 编译优化时,编译器仅根据当前文件进行优化
- 编译器的优化策略可能造成一些意外
- volatile强制编译器必须从内存中取变量值
- const和volatile同时修改变量时,互相不影响彼此的含义。