前面的运算符那篇文章提到过相关内容,C提供了不同的存储类别在内存中存储数据,C把存储值的数据存储区叫做对象,不同于面向对象的编程语言,如JAVA中的类,其定义包括数据和允许对数据进行的操作,而C中的对象你可以从硬件方面理解成存储C数据的一块内存。C对象可能并未存储实际的值,但是它在存储适当的值时一定具有相对应的大小。
从软件方面(即你编写的C程序)来看,程序需要一种方法访问对象,如通过声明变量来完成:int entity = 1;
,声明了一个名称为entity的标识符,标识符指定的对象内容是1;也可以通过int *pt = &entity;
,pt是一个标识符,它指定了一个存储地址的对象,但是表达式*pt不是一个标识符,标识符的命名规则中也不能出现*,然而,它确实指定了一个对象,且与entity指定的对象相同。标识符entity是一个左值,表达式*pt也是一个左值,综上,左值是用于标识或定位存储位置的标签,左值能够指定特定内存位置的值,而可修改的左值则是能够改变对象中的值,复习一下前面说的内容,赋值运算符的左侧只能够是可修改的左值,右值即表达式的值。
可以用存储期描述对象,所谓存储期是指对象在内存中保留了多长时间。标识符用于访问对象,可以用作用域和链接描述标识符,标识符的作用域和链接表明了程序中的哪些部分可以使用它。而不同的存储类别具有不同的存储期、作用域和链接。
1.作用域
作用域描述程序中可访问标识符的区域。一个C标识符的作用域可以是块作用域、函数作用域、函数原型作用域或文件作用域。块是指用一对花括号括起来的代码区域,定义在块中的变量具有块作用域,也就是只能在该块中从定义处到块结束处使用;函数作用域与块作用域类似,函数作用域内声明的变量,不能被外部函数使用;函数原型作用域是从形参到原型声明结束,所以函数原型中如果有形参标识符,也不必与函数定义中的形参标识符相同;文件作用域的变量声明在函数的外部,作用单位是在它定义处到文件末尾。
2.链接
C标识符有3中链接属性:外部链接、内部链接或无链接。具有块作用域、函数作用域或函数原型作用域的都是无链接变量,这意味着这些变量属于定义它们的块、函数或原型所有。具有文件作用域的变量可以是外部链接或内部链接,外部链接变量可以在多个文件程序中使用,内部链接变量只能在一个翻译单元(即一个源代码文件和它所包含的头文件)中使用。一般”内部链接的文件作用域“被简称为”文件作用域“,”外部链接的文件作用域“被简称为”全局作用域“或”程序作用域“。区分文件作用域是内部链接还是外部链接,看外部定义中是否使用了static存储类别说明符,使用static修饰则是内部链接文件作用域,使用extern存储类别说明符或者不使用,则是外部链接的文件作用域,这种变量也叫做全局变量。
3.存储期
作用域和链接描述了标识符的可见性。存储期描述了通过这些标识符访问的对象的生存期。C对象有四种存储期:静态存储期、线程存储期、自动存储期、动态分配存储期。
如果对象具有静态存储期,那么它在程序的执行期间一直存在。文件作用域变量以及static存储类别说明符修饰的变量都具有静态存储期,如果static修饰的是文件作用域变量那么只表明了其链接属性,而非存储期。
线程存储期用于并发程序设计,程序可被分为多个线程。具有线程存储期的对象,从被声明时到线程结束一直存在。以关键字_Thread_local声明一个对象时,每个线程都获得该变量的私有备份。
块作用域的变量通常都具有自动存储期。当程序进入定义这些变量的块时,为这些变量分配内存,当退出这个块时,释放刚才为变量分配的内存。但变长数组稍有不同,它们的存储期从声明处到块的末尾,而不是从块的开始到块的末尾。
4.存储类别
C中有四种存储类别:自动、寄存器、静态、外部;有五个关键字作为存储类别说明符:auto,register,static,extern,typedef。
存储类别 | 存储期 | 作用域 | 链接 | 声明方式 |
自动 | 自动 | 块 | 无 | 块内 |
寄存器 | 自动 | 块 | 无 | 块内,使用关键字register |
静态 | 静态 | 文件/块 | 内部/无 | 函数外/块内,使用关键字static |
外部 | 静态 | 文件 | 外部 | 函数外,使用关键字static |
4.1自动变量
默认情况下,声明在块内或函数头中的任何变量都属于自动存储类别。为了更清楚的表达你的意图,例如,为了表明有意覆盖一个外部变量的定义,或者强调不要把该变量变为其他存储类别,可以显示使用关键字auto。注意,auto关键字在C++中的用法完全不同,如果编写C/C++兼容的程序,最好不要使用auto作为存储类别说明符。
块作用域和无链接意味着只有在变量定义所在的块中才能通过变量名访问该变量,或者通过间接的方式,将其地址作为参数传递;故另一个函数可以使用同名变量,因为它们存储在内存的不同位置。自动存储期意味着,程序进入变量所在块时变量存在,当退出该块时变量消失。原来自动变量占用的内存可作他用。
#include <stdio.h>
int main(void){
int n = 4;
printf("n:%d\n", n);
for(int n = 1; n <= 3; n++){ //循环体可看作for循环快的子块
printf("for(n):%d\n", n);
int n = 8;
printf("for{n}:%d\n", n);
n++;
printf("for{n}:%d\n", n);
}
printf("n:%d", n);
return 0;
}
通过printf里的提示可以看出自动变量是如何隐藏同名变量,并在块结束后变量何时消亡的。有些编译器不支持上面的作用域规则,需要用选项激活程序中使用的特性。
4.2寄存器变量
使用存储类别说明符register就可声明寄存器变量,绝大多数方面,寄存器变量和自动变量都一样,它们都是块作用域、无链接和自动存储期。如果幸运的话,寄存器变量存储在CPU的寄存器(机组的知识点)中,并且由于变量在寄存器中,所以无法获取寄存器变量的地址,但是声明寄存器变量与直接命令相比更像是请求,CPU有时可能并不会分配寄存器,在这种情况下,寄存器变量就变成普通的自动变量,即便这样,仍然不能对寄存器变量使用地址运算符。通常使用最经常使用的变量作为寄存器变量,使用寄存器变量是想改善运行速度。
4.3静态变量
静态的意思指的是内存地址不变,const修饰的只读变量和#define声明的常量指的是值在程序运行过程中不可改变。静态变量包括有块作用域的静态变量,内部链接的静态变量以及外部链接的静态变量。具有文件作用域的变量自然具有静态存储期,以及用static修饰符修饰具有块作用域的局部变量。
块作用域的静态变量和自动变量一样,具有块作用域和无链接,但是具有静态存储期,即程序离开它所在的块之后,这些变量仍然存在,计算机在多次函数调用之间会记录它们的值,也叫做局部静态变量。
内部链接的静态变量具有静态存储期、文件作用域和内部链接,在函数外部用static修饰的变量都属于这种存储类别。
外部链接的静态变量部链接的静态变量具有静态存储期、文件作用域和外部链接,属于该类别的变量称为外部变量。
4.3.1初始化外部变量
自动变量不会初始化,除非显示初始化它,外部变量也可以显示初始化,但是与自动变量不同的是,如果未初始化外部变量,它们会被自动初始化为0,这一原则也适用于外部定义的数组元素,所以有时数组过大(程序的静态存储区远大于栈),需要定义在函数外部时,可以不必显示初始化,数组元素也会被初始化为0;但是与自动变量的显示初始化不同的是,只能使用常量表达式初始化文件作用域变量,而对于自动变量可以使用之前声明并赋值过的变量进行初始化int a = 2 * num;
。
4.3.2外部名称
C99和C11标准要求编译器识别局部标识符的前63个字符和外部标识符的前31个字符。外部变量名比局部变量名的规则严格,因为外部变量名还要遵循局部环境规则,所受的限制更多。
4.3.3定义和声明
int tern = 1; //定义式声明
int main(void){
extern int tern; //引用式声明
//extern tern;相当于上一句
这里tern被声明了两次,第一次声明为变量预留了存储空间,该声明构成了变量的定义,第二次声明只是为了告诉编译器使用之前已创建的tern变量,所以这不是定义。关键字extern表明该声明不是定义,因为它指示编译器区别处查询其定义,extern是为了告诉编译器在本文件或者其它文件中查找变量。并且使用extern声明并不会引起分配内存空间。因此不要用关键字extern创建外部定义,只用它来引用现有的外部定义。
如果在main内部是定义式声明int tern;
,则tern被默认为自动变量,属于main()私有,并且外部变量tern对main()不可见,因为块作用域的变量将覆盖文件作用域中的同名变量。