static and volatile

来源:互联网 发布:淘宝开店失败经验 编辑:程序博客网 时间:2024/06/03 16:26

   一:static

在C语言中,static的字面意思很容易把我们导入歧途,其实它的作用有三条。
(1)先来介绍它的第一条也是最重要的一条:隐藏。
当我们同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。为理解这句话,我举例来说明。我们要同时编译两个源文件,一个是a.c,另一个是main.c。
下面是a.c的内容
char a = 'A'; // global variable
void msg()
{
printf("Hello/n");
}
下面是main.c的内容
int main(void)
{
extern char a; // extern variable must be declared before use
printf("%c ", a);
(void)msg();
return 0;
}
程序的运行结果是:
A Hello
你可能会问:为什么在a.c中定义的全局变量a和函数msg能在main.c中使用?前面说过,所有未加static前缀的全局变量和函数都具有全局可见性,其它的源文件也能访问。此例中,a是全局变量,msg是函数,并且都没有加static前缀,因此对于另外的源文件main.c是可见的。
如果加了static,就会对其它源文件隐藏。例如在a和msg的定义前加上static,main.c就看不到它们了。利用这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲突。Static可以用作函数和变量的前缀,对于函数来讲,static的作用仅限于隐藏,而对于变量,static还有下面两个作用。
(2)static的第二个作用是保持变量内容的持久。存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。虽然这种用法不常见,但我还是举一个例子。
#i nclude <stdio.h>
int fun(void){
static int count = 10; //事实上此赋值语句从来没有执行过
return count--;
}
int count = 1;
int main(void)
{
printf("global/t/tlocal static/n");
for(; count <= 10; ++count)
printf("%d/t/t%d/n", count, fun());
return 0;
}
程序的运行结果是:
global local static
1 10
2 9
3 8
4 7
5 6
6 5
7 4
8 3
9 2
10 1
(3)static的第三个作用是默认初始化为0。其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。比如初始化一个稀疏矩阵,我们可以一个一个地把所有元素都置0,然后把不是0的几个元素赋值。如果定义成静态的,就省去了一开始置0的操作。再比如要把一个字符数组当字符串来用,但又觉得每次在字符数组末尾加&rsquo;/0&rsquo;太麻烦。如果把字符串定义成静态的,就省去了这个麻烦,因为那里本来就是&rsquo;/0&rsquo;。不妨做个小实验验证一下。
#i nclude <stdio.h>
int a;
int main(void)
{
int i;
static char str[10];
printf("integer: %d; string: (begin)%s(end)", a, str);
return 0;
}
程序的运行结果如下
integer: 0; string: (begin)(end)
最后对static的三条作用做一句话总结。首先static的最主要功能是隐藏,其次因为static变量存放在静态存储区,所以它具备持久性和默认值0。
   
 
  
  ////////////////////////////////////////////////////////////////
  
  /*
   * 使用 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,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。
  
  我手头的编译器这个例子不会出错,我也懒得找让这个例子成立的编译器了,大家自己动手试验一下看看吧。
  
  如果大家静下心来读上一些汇编码就会发现,微软的编译器经常会有一些精彩的表现。哎,拍微软的马屁也没用,给微软发过简历,连面试的机会都没给,就被拒了,太郁闷了!