囫囵C语言(四):static 和 volatile
这两个关键字其实并不沾边。只是从英文上看他们似乎是一对儿。下面分别解释这两个关键字。
一:static
这个关键字的语法问题不想多说,讨论这个关键字的文章非常多。我们先看下面的一个小程序:
////////////////////////////////////////////////////////////////
/*
* 使用 Visual C++ 6.0
* 编译器:Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 12.00.8168 for 80x86
* 链接器:Microsoft (R) Incremental Linker Version 6.00.8168
*
* 程序是 release 版本
*/
int global_a;
static int global_b;
int main(int argc, char* argv[])
{
int local_a;
static int local_b;
printf("local_b is at /t%p/n", &local_b);
printf("global_b is at /t%p/n", &global_b);
printf("global_a is at /t%p/n", &global_a);
printf("local_a is at /t%p/n", &local_a);
return 0;
}
编译运行的结果是:
local_b is at 00406910
global_b is at 00406914
global_a is at 00406918
local_a is at 0012FF80
///////////////////////////////////////////////////////////////
从结果我们可以很直观地看到 global_a,global_b,local_b 在可执行程序中的位置是连续的,根据前面的介绍,section是页面对齐的,可以得知,这三个变量处于同一个 section。这就是static 局部变量具有“记忆功能”的根本原因,因为编译器将local_b 当作一个全局变量来看待的。而变量 local_a 才是在栈上分配的,所以他的地址离其他几个变量很远。
那么它们的差别又是什么呢?
我们使用 Visual C++ 6.0 中自带的 dumpbin 程序察看一下 obj 文件的符号表:
D:/test000/Release> dumpbin /SYMBOLS test000.obj
结果如下:(我只摘出了和论题有关系的部分)
External | _global_a
Static | _global_b
Static | _?local_b@?1??main@@9@9
从中可以看出:
1. static 变量同样出现在符号表中。
2. static 变量的属性和普通的全局变量不同。
3. local_b 似乎变成了一个很奇怪的名字 _?local_b@?1??main@@9@9
第一点,只有全局变量才会出现在符号表中,所以毫无疑问 local_b 被编译器看作是全局变量。
第二点,External属性表明,该符号可以被其他的 obj 引用(做链接),static 属性表明该符号不可以被其他的 obj 引用,所以所谓 static 变量不能被其他文件引用不仅仅是在 C 语法中做了规定,真正的保证是靠链接器完成的。
第三点,_?local_b@?1??main@@9@9 就是 local_b,链接器为什么要换名字呢?很简单,如果不换名字,如果有一个全局变量与之重名怎么办,所以这个变量的名字不但改变了,而且还有所在函数的名字作为后缀,以保证唯一性。
二:volatile
说道 volatile 这个关键字,恐怕有些人会想,这玩意儿是 C 语言的关键字么?你老兄不会忽悠俺吧?嘿嘿,这个关键字确实是C语言的关键字,只不过比较少用,他的作用很奇怪:编译器,不要给我优化用这个关键字修饰的符号所涉及的程序。有看官会说这不是神经病么?让编译器优化有什么不好,我巴不得自己用的编译器优化算法世界第一呢。
来看几个例子:(Microsoft Visual C++ 6.0, 程序编译成 release 版本)
例一:
/*
* 第一个程序
*/
int main(int argc, char* argv[])
{
int i;
for (i = 0; i < 0xAAAA; i++);
return 0;
}
/*
* 汇编码
*/
_main:
00401011 xor eax,eax
00401013 ret
通过观察这个程序的汇编码我们发现,编译器发现程序的执行结果不会影响任何寄存器变量,就将这个循环优化掉了,我们在汇编码里面没有看到任何和循环有关的部分。这两句汇编码仅仅相当于 return 0;
/*
* 第二个程序
*/
int main(int argc, char* argv[])
{
volatile int i;
for (i = 0; i < 0xAAAA; i++);
return 0;
}
/*
* 汇编码
*/
_main:
00401010 push ebp
00401011 mov ebp,esp
00401013 push ecx
00401015 mov dword ptr [ebp-4],0
0040101C mov eax,0AAAAh
00401021 mov ecx,dword ptr [ebp-4]
00401024 cmp ecx,eax
00401026 jge _main+26h (00401036)
00401028 mov ecx,dword ptr [ebp-4]
0040102B inc ecx
0040102C mov dword ptr [ebp-4],ecx
0040102F mov ecx,dword ptr [ebp-4]
00401032 cmp ecx,eax
00401034 jl _main+18h (00401028)
00401036 xor eax,eax
00401038 mov esp,ebp
0040103A pop ebp
0040103B ret
我们用 volatile 修饰变量 i,然后重新编译,得到的汇编码如上所示,这回好了,循环回来了。这有什么意义呢?在通常的应用程序中,这个小小的延迟循环通常没有用,但是写过驱动程序的朋友都知道,有时候我们写外设的时候,两个命令字之间是需要一些延迟的。
例二:
/*
* 第一个程序
*/
int main(int argc, char* argv[])
{
int *p;
p = 0x100000;
*p = 0xCCCC;
*p = 0xDDDD;
return 0;
}
/*
* 汇编码
*/
_main:
00401011 mov eax,100000h
00401016 mov dword ptr [eax],0DDDDh
0040101C xor eax,eax
0040101E ret
这个程序中,编译器认为 *p = 0xCCCC; 没有任何意义,所以被优化掉了。
/*
* 第二个程序
*/
int main(int argc, char* argv[])
{
volatile int *p;
p = 0x100000;
*p = 0xCCCC;
*p = 0xDDDD;
return 0;
}
/*
* 汇编码
*/
_main:
00401011 mov eax,100000h
00401016 mov dword ptr [eax],0CCCCh
0040101C mov dword ptr [eax],0DDDDh
00401022 xor eax,eax
00401024 ret
重新声明这个变量,*p = 0xCCCC; 被执行了,同样,这主要用于驱动外设,有的外设要求连续向一个地址写入多个不同的数据,才能外成一个完整的操作。Intel 的CPU 外设地址和内存地址分开,访问外设的时候使用特殊指令,比如 inb, outb。但有一些 CPU 比如 ARM,外设是和内存混合编址的,访问外设看上去就像读写普通的内存。具体细节请参考 Intel 和 ARM 的手册。
一个精彩的细节:
在例二中编译器发现一个寄存器 eax 可以完成整个函数,所以并没有给变量 p 在栈上分配内存。省略了栈的操作,节省了不少时间,通常栈的操作使用 5 条以上的汇编码。
上面两个例子,都是写驱动程序用到的,写应用程序是不是就没必要知道了呢?请看下面的例子:
例三:
int run = 1;
void t1()
{
run = 0;
}
void t2()
{
while (run)
{
printf("error ./n");
}
}
int main(int argc, char* argv[])
{
t1();
t2();
return 0;
}
这个程序乍看没什么问题,在某些编译器,或者某些优化参数下,while (run)会被优化成一个死循环,是因为编译器不知道会有外部的线程要改变这个变量,当他看到 run 的定义时,认为这个循环永远不会退出,所以直接将这个循环优化成了死循环。解决的办法是用 volatile 声明变量 i。
我手头的编译器这个例子不会出错,我也懒得找让这个例子成立的编译器了,大家自己动手试验一下看看吧。
如果大家静下心来读上一些汇编码就会发现,微软的编译器经常会有一些精彩的表现。
去微软面过,电话两轮,On site 6 轮,被拒了!