static,inline,volatile

来源:互联网 发布:中信淘宝卡积分规则 编辑:程序博客网 时间:2024/04/30 12:50

1、static

(1)先来介绍它的第一条也是最重要的一条:隐藏。

当我们同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。为理解这句话,我举例来说明。我们要同时编译两个源文件,一个是a.c,另一个是main.c。

下面是a.c的内容

[cpp] view plaincopyprint?
  1. char a = 'A';// global variable 
  2. void msg()  
  3.     printf("Hello/n");  
下面是main.c的内容
[cpp] view plaincopyprint?
  1. int main(void
  2. {     
  3.     extern char a;   // extern variable must be declared before use 
  4.     printf("%c ", a); 
  5.     (void)msg(); 
  6.     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还是用来隐藏的。虽然这种用法不常见,但我还是举一个例子。
[cpp] view plaincopyprint?
  1. #include <stdio.h> 
  2. int fun(void){ 
  3.     static int count = 10;   // 事实上此赋值语句从来没有执行过 
  4.     return count--; 
  5. int count = 1; 
  6. int main(void
  7. {     
  8.     printf("global/t/tlocal static/n"); 
  9.     for(; count <= 10; ++count) 
  10.         printf("%d/t/t%d/n", count, fun());     
  11.      
  12.     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

3static的第三个作用是默认初始化为0。其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。比如初始化一个稀疏矩阵,我们可以一个一个地把所有元素都置0,然后把不是0的几个元素赋值。如果定义成静态的,就省去了一开始置0的操作。再比如要把一个字符数组当字符串来用,但又觉得每次在字符数组末尾加’/0’太麻烦。如果把字符串定义成静态的,就省去了这个麻烦,因为那里本来就是’/0’
2 inline
我们看下面的函数,函数体中只有一行语句:
    double Average(double total, int number){
      return total/number;
      }
定义这么简单的函数有必要吗?实际上,它还是有一些优点的:第一,它使程序更可读;第二,它使这段代码可以重复使用。但是,它也有缺点:当它被频繁地调用的时候,由于调用函数的开销,会对应用程序的性能(时间+空间效率,这儿特指时间)有损失。例如,Average在一个循环语句中重复调用几千次,会降低程序的执行效率。


    那么,有办法避免函数调用的开销吗?对于上面的函数,我么可以把它定义为内联函数的形式:
    inline double Average(double total, int number){
      return total/number;
    }
函数的引入可以减少程序的目标代码,实现程序代码的共享。
  函数调用需要时间和空间开销,调用函数实际上将程序执行流程转移到被调函数中,被调函数的代码执行完后,再返回到调用的地方。这种调用操作要求调用前保护好现场并记忆执行的地址,返回后恢复现场,并按原来保存的地址继续执行。对于较长的函数这种开销可以忽略不计,但对于一些函数体代码很短,又被频繁调用的函数,就不能忽视这种开销。引入内联函数正是为了解决这个问题,提高程序的运行效率。
    在程序编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来进行替换。由于在编译时将内联函数体中的代码替代到程序中,因此会增加目标程序代码量,进而增加空间开销,而在时间开销上不象函数调用时那么大,可见它是以目标代码的增加为代价来换取时间的节省。

◆总结:inline函数是提高运行时间效率,但却增加了空间开销。
       即inline函数目的是:为了提高函数的执行效率(速度)。
非内联函数调用有栈内存创建和释放的开销
在C中可以用宏代码提高执行效率,宏代码不是函数但使用起来像函数,编译器用复制宏代码的方式取代函数调用,省去了参数压栈、生成汇编语言的CALL调用、返回参数、执行return等过程,从而提高速度。

◆使用宏的缺点:(1)容易出错(预处理器在复制宏代码时常常产生意想不到的边际效应)
                  例如:#define MAX(a,b)    (a) > (b) ? (a) : (b)
                        语句result = MAX(i,j) + 2 却被扩展为result = (i)>(j)?(i):(j)+2;
                        但意却为result = ((i)>(j)?(i):(j)) + 2;
              (2)不可调试
              (3)无法操作类的私有数据成员

        C++函数内联机制既具备宏代码的效率,又增加了安全性,且可自由操作类的数据成员。
        关键字inline必须与函数定义体放在一起才能使函数真正内联,仅把inline放在函数声明的前面不起任何作用。因为inlin是一种用于实现的关键字,不是一种用于声明的关键字。
        许多书籍把内联函数的声明、定义体前都加了inline关键字,但声明前不应该加(加不加不会影响函数功能),因为声明与定义不可混为一谈。

★声明、定义和语句
声明:就是在向系统介绍名字(一个名字是一块内存块的别名),只是告诉编译器这个名字值的类型及宣告该名字的存在性,仅此而已。
定义:则是分配存储空间,即具有了存储类型。
语句:程序的基本组成部分,分可执行语句(定义是)和不可执行语句(声明是)。
在正式编写程序语句前定义的一些全局变量或局部变量,在C中为声明,C++中为定义。
例如:int  a;//在标C中为声明,是不可执行语句;在C++中为定义,是可执行语句
      extern int a;//为声明,是不可执行语句   CWinApp  curApp;//对象定义是可执行语句

◆使用内联函数时应注意以下几个问题:
(1) 在一个文件中定义的内联函数不能在另一个文件中使用。它们通常放在头文件中共享。
(2) 内联函数应该简洁,只有几个语句,如果语句较多,不适合于定义为内联函数。
(3) 内联函数体中,不能有循环语句、if语句或switch语句,否则,函数定义时即使有inline关键字,编译器也会把该函数作为非内联函数处理。
(4) 内联函数要在函数被调用之前声明。

例如:
#include <iostream.h>
int increment(int i);
inline int increment(int i){
  i++; return i;
}
void main(void){  ……
}
如果我们修改一下程序,将内联函数的定义移到main()之后:
    #include <iostream.h>
int increment(int i);
void main(void){  ……
}
//内联函数定义放在main()函数之后
inline int increment(int i){
  i++; return i;
}
内联函数在调用之后才定义,这段程序在编译的时候编译器不会直接把它替换到main中。也就是说实际上"increment(int i)"只是作为一个普通函数被调用,并不具有内联函数的性质,无法提高运行效率。

3 volatile

volatile 提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有 volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。下面举例说明。在DSP开发中,经常需要等待某个事件的触发,所以经常会写出这样的程序:
short flag;
void test()
{
do1();
while(flag==0);
do2();
} 这段程序等待内存变量flag的值变为1(怀疑此处是0,有点疑问,)之后才运行do2()。变量flag的值由别的程序更改,这个程序可能是某个硬件中断服务程序。例如:如果某个按钮按下的话,就会对DSP产生中断,在按键中断程序中修改flag为1,这样上面的程序就能够得以继续运行。但是,编译器并不知道flag的值会被别的程序修改,因此在它进行优化的时候,可能会把flag的值先读入某个寄存器,然后等待那个寄存器变为1。如果不幸进行了这样的优化,那么while循环就变成了死循环,因为寄存器的内容不可能被中断服务程序修改。为了让程序每次都读取真正flag变量的值,就需要定义为如下形式:
volatile short flag;
需要注意的是,没有volatile也可能能正常运行,但是可能修改了编译器的优化级别之后就又不能正常运行了。因此经常会出现debug版本正常,但是release版本却不能正常的问题。所以为了安全起见,只要是等待别的程序修改某个变量的话,就加上volatile关键字。


  volatile的本意是“易变的”
  由于访问寄存器的速度要快过RAM,所以编译器一般都会作减少存取外部RAM的优化。比如:
static int i=0;
int main(void)
{
   ...
   while (1)
   {
      if (i) do_something();
   }
}
/* Interrupt service routine. */
void ISR_2(void)
{
   i=1;
}
   程序的本意是希望ISR_2中断产生时,在main当中调用do_something函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致do_something永远也不会被调用。如果将将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中i也应该如此说明。
  一般说来,volatile用在如下的几个地方:
  1、中断服务程序中修改的供其它程序检测的变量需要加volatile;
  2、多任务环境下各任务间共享的标志应该加volatile;
  3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;
  另外,以上这几种情况经常还要同时考虑数据的完整性(相互关联的几个标志读了一半被打断了重写),在1中可以通过关中断来实现,2中可以禁止任务调度,3中则只能依靠硬件的良好设计了。
  volatile 的含义
  volatile总是与优化有关,编译器有一种技术叫做数据流分析,分析程序中的变量在哪里赋值、在哪里使用、在哪里失效,分析结果可以用于常量合并,常量传播等优化,进一步可以死代码消除。但有时这些优化不是程序所需要的,这时可以用volatile关键字禁止做这些优化,volatile的字面含义是易变的,它有下面的作用:
1 不会在两个操作之间把volatile变量缓存在寄存器中。在多任务、中断、甚至setjmp环境下,变量可能被其他的程序改变,编译器自己无法知道,volatile就是告诉编译器这种情况。 
2 不做常量合并、常量传播等优化,所以像下面的代码: 
volatile int i = 1; 
if (i > 0) ... 
if的条件不会当作无条件真。 
  3 对volatile变量的读写不会被优化掉。如果你对一个变量赋值但后面没用到,编译器常常可以省略那个赋值操作,然而对Memory Mapped IO的处理是不能这样优化的。
  前面有人说volatile可以保证对内存操作的原子性,这种说法不大准确,其一,x86需要LOCK前缀才能在SMP下保证原子性,其二,RISC根本不能对内存直接运算,要保证原子性得用别的方法,如atomic_inc。
  对于jiffies,它已经声明为volatile变量,我认为直接用jiffies++就可以了,没必要用那种复杂的形式,因为那样也不能保证原子性。
  你可能不知道在Pentium及后续CPU中,下面两组指令
inc jiffies 
;; 
mov jiffies, %eax 
inc %eax 
mov %eax, jiffies
作用相同,但一条指令反而不如三条指令快。

描述:每个变量和他的名字一样很善变,有时候它善变是发自内心的,有时是外部因素决定的,只有volatile变量才会表里如一,因此获得了专一王子的美誉。

作用:volatile字面意思是易挥发,易变化的意思,它修辞的变量表示该变量的值很容易由于外部因素发生改变,强烈请求编译器要老老实实的在每次对变量进行访问时去内存里读取。可能上面说的还不是很清楚,我们换个例子来说,你明天一个朋友过生日,今天把要送的礼物打包好了,一般情况下,我们明天起来不再需要再打开验证一下里面礼物是否存在,因为我们知道,只要礼物的外包装没有动过,里面东西应该不会被动。其实编译器和人一样聪明,为了提高效率也会玩省事,如下面的例子:

1  int a = 10;

2  int b = a;

3  int c = a;

编译器扫描了代码发现上面,第一行代码在将10赋给了整形变量a,之后a变量的值没有再发生改变。在后面第二行中,将a变量里的值取出来赋给b变量。在第三行代码里将a变量的值赋给c的时候,因为CPU访问内存速度较慢(看register关键字介绍),编译器为了提高效率,玩了“省事”,直接将10赋给了c。

单从上述代码我们来看是没有问题的,就如同从外包装看生日礼物完好一样。但是,上述代码如果运行在多线程中,在一个线程上下文中没有改变它的值,但是我们不能保证变量的值没有被其它线程改变。就好比是,生日礼物放到其它人那里保存,我们不敢100%保证它里面的东西还完好。当然这种数据不一致的机制不仅仅出现在多线程中,同样在设备的状态寄存器里也会存在。比如:网卡里的某状态寄存器里的值是否为1表示是否有网络数据到达,在当前时刻其值为1,不能代表着下一时刻其值还为1,它的值是由外界条件决定的,编译器肯定不能在这种情况下玩“省事”

为了防止在类似的情况下,编译器玩省事,可以将这些变量声明为volatile,这样,不管它的值有没有变化,每次对其值进行访问时,都会从内存里,寄存器里读取,从而保证数据的一致,做到表里如一。

 

原创粉丝点击