static,register,volatile

来源:互联网 发布:c语言判断完全数 编辑:程序博客网 时间:2024/06/14 10:51

 static修饰符 

一,static全局变量

           我们知道,一个进程在内存中的布局如图1所示:

      其中.text段保存进程所执行的程序二进制文件,.data段保存进程所有的已初始化的全局变量,.bss段保存进程未初始化的全局变量(其他段中还有很多乱七八糟的段,暂且不表)。在进程的整个生命周期中,.data段和.bss段内的数据时跟整个进程同生共死的,也就是在进程结束之后这些数据才会寿终就寝。

     当一个进程的全局变量被声明为static之后,它的中文名叫静态全局变量。静态全局变量和其他的全局变量的存储地点并没有区别,都是在.data段(已初始化)或者.bss段(未初始化)内,但是它只在定义它的源文件内有效,其他源文件无法访问它。所以,普通全局变量穿上static外衣后,它就变成了新娘,已心有所属,只能被定义它的源文件(新郎)中的变量或函数访问。

以下是一些示例程序

file1.h如下:

[cpp] view plain copy
  1. #include <stdio.h>  
  2.   
  3. void printStr();  

我们在file1.c中定义一个静态全局变量hello, 供file1.c中的函数printStr访问.

[cpp] view plain copy
  1. #include "file1.h"  
  2.   
  3. static char* hello = "hello cobing!";  
  4.   
  5. void printStr()  
  6. {  
  7.     printf("%s\n", hello);  
  8. }  

file2.c是我们的主程序所在文件,file2.c中如果引用hello会编译出错

[cpp] view plain copy
  1. #include "file1.h"  
  2.   
  3. int main()  
  4. {  
  5.     printStr();  
  6.     printf("%s\n", hello);  
  7.     return 0;  
  8. }  

报错如下:

[liujx@server235 static]$ gcc -Wall file2.c file1.c -o file2
file2.c: In function ‘main’:
file2.c:6: 错误:‘hello’ 未声明 (在此函数内第一次使用)
file2.c:6: 错误:(即使在一个函数内多次出现,每个未声明的标识符在其
file2.c:6: 错误:所在的函数内只报告一次。)

如果我们将file2.c改为下面的形式:

[cpp] view plain copy
  1. #include "file1.h"  
  2.   
  3. int main()  
  4. {  
  5.     printStr();  
  6.     return 0;  
  7. }  

则会顺利编译连接。

运行程序后的结果如下:
[liujx@server235 static]$ gcc -Wall file2.c file1.c -o file2
[liujx@server235 static]$ ./file2
hello cobing!

上面的例子中,file1.c中的hello就是一个静态全局变量,它可以被同一文件中的printStr调用,但是不能被不同源文件中的file2.c调用。

 

      二,static局部变量

      普通的局部变量在栈空间上分配,这个局部变量所在的函数被多次调用时,每次调用这个局部变量在栈上的位置都不一定相同。局部变量也可以在堆上动态分配,但是记得使用完这个堆空间后要释放之。

       static局部变量中文名叫静态局部变量。它与普通的局部变量比起来有如下几个区别:

           1)位置:静态局部变量被编译器放在全局存储区.data(注意:不在.bss段内,原因见3)),所以它虽然是局部的,但是在程序的整个生命周期中存在。

           2)访问权限:静态局部变量只能被其作用域内的变量或函数访问。也就是说虽然它会在程序的整个生命周期中存在,由于它是static的,它不能被其他的函数和源文件访问。

           3)值:静态局部变量如果没有被用户初始化,则会被编译器自动赋值为0,以后每次调用静态局部变量的时候都用上次调用后的值。这个比较好理解,每次函数调用静态局部变量的时候都修改它然后离开,下次读的时候从全局存储区读出的静态局部变量就是上次修改后的值。
以下是一些示例程序:

     file1.h的内容和上例中的相同,file1.c的内容如下:

[cpp] view plain copy
  1. #include "file1.h"  
  2.   
  3. void printStr()  
  4. {  
  5.     int normal = 0;  
  6.     static int stat = 0;    //this is a static local var  
  7.     printf("normal = %d ---- stat = %d\n",normal, stat);  
  8.     normal++;  
  9.     stat++;  
  10. }  

为了便于比较,我定义了两个变量:普通局部变量normal和静态局部变量stat,它们都被赋予初值0;

file2.c中调用file1.h:

[cpp] view plain copy
  1. #include "file1.h"  
  2.   
  3. int main()  
  4. {  
  5.  printStr();  
  6.  printStr();  
  7.  printStr();  
  8.  printStr();  
  9.  printf("call stat in main: %d\n",stat);  
  10.  return 0;  
  11. }  

这个调用会报错,因为file2.c中引用了file1.c中的静态局部变量stat,如下:

[liujx@server235 static]$ gcc -Wall file2.c file1.c -o file2
file2.c: In function ‘main’:
file2.c:9: 错误:‘stat’ 未声明 (在此函数内第一次使用)
file2.c:9: 错误:(即使在一个函数内多次出现,每个未声明的标识符在其
file2.c:9: 错误:所在的函数内只报告一次。)

编译器说stat未声明,这是因为它看不到file1.c中的stat,下面注掉这一行:

[cpp] view plain copy
  1. #include "file1.h"  
  2.   
  3. int main()  
  4. {  
  5.     printStr();  
  6.     printStr();  
  7.     printStr();  
  8.     printStr();  
  9. //  printf("call stat in main: %d\n",stat);  
  10.     return 0;  
  11. }  

[liujx@server235 static]$ gcc -Wall file2.c file1.c -o file2
[liujx@server235 static]$ ./file2
normal = 0 ---- stat = 0
normal = 0 ---- stat = 1
normal = 0 ---- stat = 2
normal = 0 ---- stat = 3
运行如上所示。可以看出,函数每次被调用,普通局部变量都是重新分配,而静态局部变量保持上次调用的值不变。

需要注意的是由于static局部变量的这种特性,使得含静态局部变量的函数变得不可重入,即每次调用可能会产生不同的结果。这在多线程编程时可能会成为一种隐患。需要多加注意。


       三,static函数
              相信大家还记得C++面向对象编程中的private函数,私有函数只有该类的成员变量或成员函数可以访问。在C语言中,也有“private函数”,它就是接下来要说的static函数,完成面向对象编程中private函数的功能。

            当你的程序中有很多个源文件的时候,你肯定会让某个源文件只提供一些外界需要的接口,其他的函数可能是为了实现这些接口而编写,这些其他的函数你可能并不希望被外界(非本源文件)所看到,这时候就可以用static修饰这些“其他的函数”。

           所以static函数的作用域是本源文件,把它想象为面向对象中的private函数就可以了。

下面是一些示例:

file1.h如下:

[cpp] view plain copy
  1. #include <stdio.h>  
  2.   
  3. static int called();  
  4. void printStr();  

file1.c如下:

[cpp] view plain copy
  1. #include "file1.h"  
  2.   
  3. static int called()  
  4. {  
  5.     return 6;  
  6. }  
  7. void printStr()  
  8. {  
  9.     int returnVal;  
  10.     returnVal = called();  
  11.     printf("returnVal=%d\n",returnVal);  
  12. }  

file2.c中调用file1.h中声明的两个函数,此处我们故意调用called():

[cpp] view plain copy
  1. #include "file1.h"  
  2.   
  3. int main()  
  4. {  
  5.     int val;  
  6.     val = called();  
  7.     printStr();  
  8.     return 0;  
  9. }  

编译时会报错:

[liujx@server235 static]$ gcc -Wall file2.c file1.c -o file2
file1.h:3: 警告:‘called’ 使用过但从未定义
/tmp/ccyLuBZU.o: In function `main':
file2.c:(.text+0x12): undefined reference to `called'
collect2: ld 返回 1
因为引用了file1.h中的static函数,所以file2.c中提示找不到这个函数:undefined reference to 'called'

下面修改file2.c:

[cpp] view plain copy
  1. #include "file1.h"  
  2.   
  3. int main()  
  4. {  
  5.     printStr();  
  6.     return 0;  
  7. }  

编译运行:

[liujx@server235 static]$ gcc -Wall file2.c file1.c -o file2
[liujx@server235 static]$ ./file2
returnVal=6

       static函数可以很好地解决不同原文件中函数同名的问题,因为一个源文件对于其他源文件中的static函数是不可见的。




register修饰符

     register修饰符暗示编译程序相应的变量将被频繁地使用,如果可能的话,应将其保存在CPU的寄存器中,以加快其存储速度。

内存块拷贝代码:

  /* Procedure for the assignment of structures, */

  /* if the C compiler doesn't support this feature */

  #ifdef NOSTRUCTASSIGN

  memcpy (d, s, l)

  {register char *d;

  register char *s;

  register int i;

  while (i--)

  *d++ = *s++;

  }

  #endif

  使用限制:

  1.register变量必须是能被CPU所接受的类型。这通常意味着register变量必须是一个单个的值,并且长度应该小于或者等于整型的长度。不过,有些机器的寄存器也能存放浮点数。

  2.因为register变量可能不存放在内存中,所以不能用“&”来获取register变量的地址,由于寄存器的数量有限,而且某些寄存器只能接受特定类型的数据(如指针和浮点数),因此真正起作用的register修饰符的数目和类型都依赖于运行程序的机器,而任何多余的register修饰符都将被编译程序所忽略。

  3.在某些情况下,把变量保存在寄存器中反而会降低程序的运行速度。因为被占用的寄存器不能再用于其它目的;或者变量被使用的次数不够多,不足以装入和存储变量所带来的额外开销。

   4.当声明对象有自动生存周期时,可以使用register修饰符。因此,register也只能用在函数内的声明中。此关键字告诉编译器:此对象的存取应该尽量快,最好存储在CPU的寄存器中。然而,编译器不见得会这么做。另外要注意的是,当一个对象声明为register,就不可使用地址运算符&了,因为它有可能被放到寄存器中。

volatile关键字

volatile总是与优化有关,编译器有一种技术叫做数据流分析,分析程序中的变量在哪里赋值、在哪里使用、在哪里失效,分析结果可以用于常量合并,常量传播等优化,进一步可以死代码消除。但有时这些优化不是程序所需要的,这时可以用volatile关键字禁止做这些优化volatile的字面含义是易变的,它有下面的作用:

1.不会在两个操作之间把volatile变量缓存在寄存器中。在多任务、中断、甚至setjmp环境下,变量可能被其他的程序改变,编译器 自己无法知道,volatile就是告诉编译器这种情况。 
  2.不做常量合并、常量传播等优化,所以像下面的代码: 
  volatile int i = 1; 
  if (i > 0) ...  
  if的条件不会当作无条件真。 意思i可能被其它程序所改变
  3.volatile变量的读写不会被优化掉。如果你对一个变量赋值但后面没用到,编译器常常可以省略那个赋值操作,然而对Memory Mapped IO的处理是不能这样优化的。

4.volatile变量能防止优化,比如说你在某个地方可能连续调用了好几次这个函数,于是编译器优化后,可能就调用一次,其他几次就采用这一次调用的返回值,而volatile修饰后,要让每一次都进行函数调用, 而不采用暂存值。


将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化。从而可以提供对特殊地址的稳定访问

  1、中断服务程序中修改的供其它程序检测的变量需要加volatile  
  2、多任务环境下各任务间共享的标志应该加volatile; 
  3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义

例子:

int volatile nVint;

当要求使用volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。

例如:

volatile int i=10; 
int a = i; 
... 
//
其他代码,并未明确告诉编译器,对i进行过操作

int b = i;

volatile 指出 i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在b中。而优化做法是,由于编译器发现两次从i读数据的代码之间的代码没有对i进行过操作,它会自动把上次读的数据放在b中。而不是重新从i里面读。这样以来,如果i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile可以保证对特殊地址的稳定访问。

典型问题:

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子: 
1). 
并行设备的硬件寄存器(如:状态寄存器) 
2). 
一个中断服务子程序中会访问到的非自动变量(Non-automatic variables) 
3). 
多线程应用中被几个任务共享的变量 
volatile重要性: 
1). 
一个参数既可以是const还可以是volatile吗?解释为什么。 
2). 
一个指针可以是volatile 吗?解释为什么。 
3). 
下面的函数有什么错误: 
int square(volatile int *ptr) 

return *ptr * *ptr; 

答案: 
1). 
是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。 
2). 
是的。尽管这并不很常见。一个例子是当一个中服务子程序修改一个指向一个buffer的指针时。 
3). 
这段代码的有个恶作剧。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码: 
int square(volatile int *ptr) 

int a,b; 
a = *ptr; 
b = *ptr; 
return a * b; 

由于*ptr的值可能被意想不到地该变,因此ab可能是不同的。结果,这段代码可能返回不是你所期望的平方值!正确的代码如下: 
long square(volatile int *ptr) 

int a; 
a = *ptr; 
return a * a; 
}

补充:

volatile的本意是易变的” 由于访问寄存器的速度要快过RAM,所以编译器一般都会作减少存取外部RAM的优化。比如:

static int i=0; //应该为static volatile int i=0;

int main(void) 

... 
while (1) 

if (i) dosomething(); 

}

/* Interrupt service routine. */ 
void ISR_2(void) 

i=1; 
}

程序的本意是希望ISR_2中断产生时,在main当中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本,导致dosomething永远也不会被调用。 
如果将将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中i也应该如此说明。

volatile表示变量的内容可能在程序未知的情况下被改变 
比如,它对应的内存地址的内容被中断函数,或者其他的进程所改变 
这种类型的变量,程序执行的时候不会放到cache当中预取,而是每次用到的时候直接取得 
比如,你在c中间写这样的程序 
for (int i=0; i <100000;i++); 
空循环,什么也不做 
这个东西就会被优化调,如果在int前面加入这个标记则不会被优化的,因为i每次的变化不一定++也许在循环中间被别的程序所改变

Linuxsource codelinux/mm/memory.c)中有这样两句
volatile void do_exit(long code);

static inline volatile void oom(void) 

printk("out of memory\n\r"); 
do_exit(SIGSEGV);

 

 

volatile修饰的确实是一个变量,而且是一个容易变的变量。在每次取这个变量值的时候,要求不是取它上次在某个时候取的临时缓存变量(比如说暂存在某个寄存器中),而是直接到内存中取。 

volatile
变量能防止优化,比如说你在某个地方可能连续调用了好几次这个函数,于是编译器优化后,可能就调用一次,其他几次就采用这一次调用的返回值,而volatile修饰后,要让每一次都进行函数调用, 而不采用暂存值。 
 
volatile
修饰不返回函数,比如函数体里面有exit或者死循环之类的东西。这样该函数被调用的时候不用把返回地址压入堆栈,(当然还可能有其他),代码得到优化。这个是 GCC 的扩展
 
volatile
在嵌入式里使用较多。有时硬件动作会影响某存储空间的内容,对这些变量,必须加volatile修饰。 
volatie修饰的变量,每次操作时遵循下面动作: 
从内存取值 ---> 放入寄存器 ---> 操作 --->写回内存 
没有volatie修饰的变量,操作可能遵循(可能就是不是所有情况都如此): 
从内存取值 ---> 放入寄存器 ---> 第一次操作 ---> 第二次操作(此时仍操作寄存器中的值)…… --->N次操作 --->写回内存 
举个例子论述两者关系: 
    int volatie i; //
全局变量,在其它地方会被修改 
    while (i){ 
        do_somethings(); 
    } 
如果i没有被volatie修饰,当while循环执行时,另一段程序并发的执行了i=0,这个循环仍不会退出,因为每次循环都是检查寄存器中的值。 
如果有volatie修饰,那么循环结束,因为循环每次检查i的时候,会先从内存把i读入寄存器,这个时候i在其它地方被赋0,则循环结束。 
 
最容易理解的解析: 
如果编译器在代码中发现对同一地址的两次访问之间,没有对该地址进行写操作,那么编译器将优化为第一次寻址读该地址时取得的值作为第二次寻址的值,而并不是再做第二次物理上的 I/O 寻址操作。volatile 关键字指示编译器进行强制 I/O 寻址,因为编译器那样的优化,可能并不是我们真正期望的,譬如那个地址上连接着一个传感器上的寄存器,那么实际上,可能该寄存器的值是被传感器自身不断刷新的。因此,我们必要要求CPU每次都进行 I/O 操作。 
 volatile 
跟以前的 register 相反register 告诉编译器尽量将变量放到寄存器中使用volatile强制将更改后的值写回内存(无论是cache还是内存). 如果不写回内存对于一些全局共享的变量可能导致不一致问题.


const关键字

   关键字const用来定义常量,如果一个变量被const修饰,那么它的值就不能再被改变,我想一定有人有这样的疑问,C语言中不是有#define吗,干嘛还要用const呢,我想事物的存在一定有它自己的道理,所以说const的存在一定有它的合理性,与预编译指令相比。

const修饰符有以下的优点

1、预编译指令只是对值进行简单的替换,不能进行类型检查

2、可以保护被修饰的东西,防止意外修改,增强程序的健壮性

3、编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。

下面我们从几个方面来说一下const的用法:

一、修饰局部变量

const int n=5;

int const n=5;

这两种写法是一样的,都是表示变量n的值不能被改变了,需要注意的是,用const修饰变量时,一定要给变脸初始化,否则之后就不能再进行赋值了。

接下来看看const用于修饰常量静态字符串,例如:

const char* str="fdsafdsa";

如果没有const的修饰,我们可能会在后面有意无意的写str[4]='x'这样的语句,这样会导致对只读内存区域的赋值,然后程序会立刻异常终止。有了const,这个错误就能在程序被编译的时候就立即检查出来,这就是const的好处。让逻辑错误在编译期被发现。

二、常量指针与指针常量

常量指针是指针指向的内容是常量,可以有一下两种定义方式。

const int * n;

int const * n;

需要注意的是一下两点:

1、常量指针说的是不能通过这个指针改变变量的值,但是还是可以通过其他的引用来改变变量的值的。

int a=5;

const int* n=&a;

a=6;

2、常量指针指向的值不能改变,但是这并不是意味着指针本身不能改变,常量指针可以指向其他的地址。

int a=5;

int b=6;

const int* n=&a;

n=&b;

指针常量是指指针本身是个常量,不能在指向其他的地址,写法如下:

int *const n;

需要注意的是,指针常量指向的地址不能改变,但是地址中保存的数值是可以改变的,可以通过其他指向改地址的指针来修改。

int a=5;

int *p=&a;

int* const n=&a;

*p=8;

区分常量指针和指针常量的关键就在于星号的位置,我们以星号为分界线,如果const在星号的左边,则为常量指针,如果const在星号的右边则为指针常量。如果我们将星号读作‘指针',将const读作‘常量'的话,内容正好符合。int const * n;是常量指针,int *const n;是指针常量。

指向常量的常指针

是以上两种的结合,指针指向的位置不能改变并且也不能通过这个指针改变变量的值,但是依然可以通过其他的普通指针改变变量的值。

const int* const p;

三、修饰函数的参数

根据常量指针与指针常量,const修饰函数的参数也是分为三种情况

1、防止修改指针指向的内容

void StringCopy(char *strDestination, const char *strSource);

其中 strSource 是输入参数,strDestination 是输出参数。给 strSource 加上 const 修饰后,如果函数体内的语句试图改动 strSource 的内容,编译器将指出错误。

2、防止修改指针指向的地址

void swap ( int * const p1 , int * const p2 )

指针p1和指针p2指向的地址都不能修改。

3、以上两种的结合。

四、修饰函数的返回值

如果给以“指针传递”方式的函数返回值加 const 修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。 

例如函数

const char * GetString(void);

如下语句将出现编译错误:

char *str = GetString();

正确的用法是

const char *str = GetString();

五、修饰全局变量

全局变量的作用域是整个文件,我们应该尽量避免使用全局变量,以为一旦有一个函数改变了全局变量的值,它也会影响到其他引用这个变量的函数,导致除了bug后很难发现,如果一定要用全局变量,我们应该尽量的使用const修饰符进行修饰,这样方式不必要的以为修改,使用的方法与局部变量是相同的。


0 0