囫囵C语言(四):static 和 volatile

本文深入探讨C语言中static和volatile关键字的作用及用法。static关键字使局部变量具有全局特性,volatile则防止编译器优化特定代码。通过实例演示两者如何影响编译过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

囫囵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 轮,被拒了!
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值