量作用域、存储类别、内存分配
变量类别
|
子类别
|
作用域
|
生存域
|
内存分配Section
|
Section属性
|
局部变量
|
不赋初值的静态变量
|
函数
|
整个程序
|
DATA
|
R/W
|
赋初值的静态变量
|
函数
|
整个程序
|
INIT
DCONST(初始值)
|
R/W
R
| |
自动变量
|
函数
|
函数
|
STACK
|
R/W
| |
寄存器变量
|
函数
|
函数
|
Register/ STACK
|
R/W
| |
全局变量
|
不赋初值的静态变量
|
文件
|
整个程序
|
DATA
|
R/W
|
赋初值的静态变量
|
文件
|
从变量的作用域角度(空间)来分,可以分为全局变量和局部变量。
从变量的存在时间角度(生存期)来分,可以分为静态存储方式和动态存储方式。
内存中供用户使用的存储空间分为三部分:程序区、静态存储区、动态存储区。
数据分别存放在静态存储区和动态存储区中。
全局变量全部存放在静态存储区中,在程序开始执行时给全局变量分配存储区,程序执行完毕就释放。程序执行过程中它们占据固定的存储单元,而不是动态分配和释放。
而在动态存储区中存放以下数据:1.函数形式参数;2.自动变量;3.函数调用时的现场保存和返回地址。
数据在内存中的存储方法两类:静态存储类和动态存储类。具体包含四种:自动的(auto),静态的(static),寄存器的(register),外部的(extern)。
函数中的局部变量称为自动变量。
静态变量属于静态存储类别,在静态存储区内分配存储单元。在程序运行期间不释放。静态局部变量是在编译时赋初值的,即只赋初值一次,在程序运行时它已有初值。若定义时不赋初值,编译时自动赋初值;而对自动变量,如果不赋初值则它的值是不确定的。
虽然静态局部变量在函数调用后依然存在,但其它函数是不能引用它的。
以下情况适合用静态局部变量:
1.需要保留函数上一次调用结束时的值。
2.如果初始化后,变量只被引用而不改变其值,则这时用静态局部变量比较方便,以免每次调用时重新赋值。
寄存器变量是为了提高效率。因为寄存器的存取速度远高于内存的存取速度。
注意:1.只有局部自动变量和形式参数可以作为寄存器变量。2.不能定义任意多个寄存器变量,因为计算机系统中寄存器数目有限。3.局部静态变量不能定义为寄存器变量,一个变量只能声明为一个存储类别。
外部变量(即全局变量)是在函数的外部定义的,它的作用域为从变量的定义处开始,到本程序的末尾。编译时将外部变量分配在静态存储区。
如果想在定义点之前引用外部变量,可以声明为extern以扩展作用域。用extern声明外部变量时,类型名可以省略。
在一个文件file1中定义了外部变量,而想在另一个文件file2中引用它,可以在另一个文件file2中对此外部变量进行extern声明。这样两个文件都可以引用这个外部变量。这是用extern将外部变量作用域扩展到其它文件中。
如果想使一个外部变量只在一个文件中使用,而不能被其它文件引用,可以在定义外部变量时加一个static声明。这种只能用于本文件的外部变量(全局变量)成为静态外部变量。
声明部分出现的变量有两种情况:
一是需要建立存储空间的,称为定义性声明(如int a;);二是不需要建立存储空间的,称为引用性声明(如extern a;)。
外部变量只能由一次定义,但可以多次声明。
用static声明一个变量的作用有二:1.对局部变量用static声明,则为该变量分配的空间在整个程序执行期间始终存在;2.全局变量用static声明,则该变量的作用域只限于本文件。
内部函数:如果一个函数只能被本文件中其它函数所调用,它称为内部函数。形如:static 类型标识符 函数名(形参表)
外部函数:如果此函数可以被其它文件调用,称为外部函数,形如:extern 类型标识符 函数名(形参表)。在需要调用此函数的文件中,用extern声明所调函数是外部函数,形如:extern 函数名(实参表)。
一、作用域和生存期
C程序的标识符作用域有三种:局部、全局、文件。标识符的作用域决定了程序中的哪些语句可以使用它,换句话说,就是标识符在程序其他部分的可见性。通常,标识符的作用域都是通过它在程序中的位置隐式说明的。
1.局部作用域
前面各个例子中的变量都是局部作用域,他们都是声明在函数内部,无法被其他函数的代码所访问。函数的形式参数的作用域也是局部的,它们的作用范围仅限于函数内部所用的语句块。
void add(int);
main()
{
int num=5;
add(num);
printf("%d/n",num); /*输出5*/
}
void add(int num)
{
num++;
printf("%d/n",num); /*输出6*/
}
上面例子里的两个num变量都是局部变量,只在本身函数里可见。前面我们说了,在两个函数出现同名的变量不会互相干扰,就是这个道理。所以上面的两个输出,在主函数里仍然是5,在add()函数里输出是6。
2.全局作用域
对于具有全局作用域的变量,我们可以在程序的任何位置访问它们。当一个变量是在所有函数的外部声明,也就是在程序的开头声明,那么这个变量就是全局变量。
void add(int);
int num;
main()
{
int n=5;
add(n);
printf("%d/n",num); /*输出6*/
}
void add(num) /*形式参数没有指定类型*/
{
num++;
printf("%d/n",num); /*输出6*/
}
上面的main()和add()里面,并没有声明num,但是在最后输出的时候却要求输出num,这是由于在程序的开始声明了num是全局变量,也就是在所有函数里都可以使用这个变量。这时候一个函数里改变了变量的值,其他函数里的值也会出现影响。上面的例子输出都是6,因为在add()函数里改变了num的值,由于num是全局变量,就好象它们两个函数共用一个变量,所以在main()函数里的num也随之改变了。
3.文件作用域
在很多C语言书上,都没有说明文件作用域,或者只是略微的提到,其实文件作用域在较大程序中很有作用(在多文件系统中)。文件作用域是指外部标识符仅在声明它的同一个转换单元内的函数汇总可见。所谓转换单元是指定义这些变量和函数的源代码文件(包括任何通过#include指令包含的源代码文件)。static存储类型修饰符指定了变量具有文件作用域。
static int num;
static void add(int);
main()
{
scanf("%d",&num);
add(num)
printf("%d/n",num);
}
void add(num)
{
num++;
}
上面的程序中变量num和函数add()在声明是采用了static存储类型修饰符,这使得它们具有文件作用域,仅爱定义它们的文件内可见。
由于我们提到的大多数程序都只有一个编译文件组成,所以这种写法没有实际意义。但是实际工程上的文件有很多,它们不是由一个人写成的,由很多人共同完成,这些文件都是各自编译的,这难免使得某些人使用了一样的全局变量名,那么为了以后程序中各自的变量和函数不互相干扰,就可以使用static修饰符,这样在连接到同一个程序的其他代码文件而言就是不可见的。
二、变量存储类型
前面我们说了,声明变量时用如下类似的形式:
int num;
float total;
它们都没有存储类型修饰符,我们在声明时也可以通过存储类型修饰符来告诉编译器将要处理什么类型的变量。存储类型有以下四种:自动(auto)、静态(static)、外部(extern)、寄存器(regiser)。
1.自动存储类型
自动存储类型修饰符指定了一个局部变量为自动的,这意味着,每次执行到定义该变量的语句块时,都将会为该变量在内存中产生一个新的拷贝,并对其进行初始化。实际上,如果不特别指明,局部变量的存储类型就默认为自动的,因此,加不加auto都可以。
main()
{
auto int num=5;
printf("%d/n",num);
}
在这个例子中,不论变量num的声明是否包含关键字auto,代码的执行效果都是一样的。函数的形式参数存储类型默认也是自动的。
2.静态存储变量
前面已经使用了static关键字,但是对于局部变量,静态存储类型的意义是不一样的,这时,它是和自动存储类型相对而言的。静态局部变量的作用域仍然近局限于声明它的语句块中,但是在语句块执行期间,变量将始终保持它的值。而且,初始化值只在语句块第一次执行是起作用。在随后的运行过程中,变量将保持语句块上一次执行时的值。看下面两个对应的程序:
/*1.C*/ /*2.C*/
int add(); int add();
main() main()
{ {
int result; int result;
result=add() result=add();
printf("%d ",result); printf("%d ",result);
result=add(); result=add();
printf("%d ",result); printf("%d ",result);
result=add(); result=add();
printf("%d",result); printf("%d",result);
} }
int add() int add()
{ {
int num=50; static int num=50;
num++; num++;
return num; return num;
} }
上面两个源文件,只有函数add()里的变量声明有所不同,一个是自动存储类型,一个是静态存储类型。
对于1.C文件,输出结果为51 51 51;这很好理解,每次初始值都是50,然后加1上来。
对于2.C文件,输出结果为51 52 53;这是由于变量是静态的,只在第一次初始化了50,以后都是使用上次的结果值。当第一次调用add()时,初始化为50,然后加1,输出为51;当第二次调用时,就不初始化了,这时num的值为上次的51,然后加1,输出52;当第三次调用时,num为52,加1就是53了。
比较就会发现它们的不同之处了。静态变量在下一节要说的递归函数中经常使用到。
当第一次不指明静态变量的初始值时,默认为0。
下面举一个例子,把我们说到的静态变量理解一下。
求1+2+……+100的值
void add();
int result;
main()
{
int i;
result=0;
for(i=0;i<100;i++) add();
printf("%d/n",result);
}
void add()
{
static int num=0;
num++;
result+=num;
}
add()函数被调用了100次,num的值从1一直变到100,这样就可以求出它们的和了。如果写成int num=0;那就是求1+1+……+1这100个1的值了。
实际上类似的这类问题我们可以通过递归函数来解决,什么是递归,我们下一节介绍。
3.外部存储类型
外部存储类型声明了程序将要用到的、但尚未定义的外部变量。通常,外部存储类型都是用于声明在另一个转换单元中定义的变量。下面举一个例子,这个例子包括两个文件。
/*1.C*/
void a();
main()
{
extern int num;
a();
printf("%d/n",num);
}
/*2.C*/
int num;
void a()
{
num=5;
}
这两个程序是分别编译的,然后连接成一个执行文件。具体如何操作,可以查看一些手册,这儿我简单说了一下。把上面两个文件都编译好后,再制作一个.prj文件,里面的内容是:
1.c
2.c
只有这两行,这可在编辑状态下写成,存盘,取名为1.prj。
然后选择project选项,选择project name,填入1.prj文件名,按F9后,即可生成1.exe文件。
main()函数中变量num是在另一个文件中定义的。因此,当编译器编译1.c时,无法确定该变量的地址。这时,外部存储类型声明告诉编译器,把所有对num的引用当作暂且无法确定的引用,等到所有便宜好的目标代码连接成一个可执行程序模块时,再来处理对变量num的引用。
外部变量的声明既可以在引用它的函数的内部,也可以在外部。如果变量声明在函数外部,那么同一转换单元内的所有函数都可以使用这个外部变量。反之,如果在函数内部,那么只有这一个函数可以使用该变量。
前面说了文件作用域的问题,如果在声明全局变量时,加上static修饰符,那么该变量只在当前文件内可见,而extern又可以引用其它文件里的变量。所以在一个大型程序中,每个程序员只是完成其中的一小块,为了让自己的变量不让其他程序员使用,保持一定的独立性,经常在全局变量前加static。我们可以这样来说明一下:
还是上面的两个文件,现在再增加一个文件3.c,内容为:
static int num;
void a()
{
num=6;
}
把1.prj文件后面加上3.c 这样,我们生成的1.exe文件,执行时输出是5,而不是6。因为3.c文件的num变量增加了文件作用域,在其他文件中是无法使用它的。
4.寄存器存储类型
被声明为寄存器存储类型的变量,除了程序无法得到其地址外,其余都和自动变量一样。至于什么是变量地址,以后说指针时会详细介绍。
main()
{
register int num;
num=100;
printf("%d",num);
}
使用寄存器存储类型的目的是让程序员指定某个局部变量存放在计算机的某个硬件寄存器里而不是内存中,以提高程序的运行速度。不过,这只是反映了程序员的主观意愿,编译器可以忽略寄存器存储类型修饰符。
寄存器变量的地址是无法取得的,因为绝大多数计算机的硬件寄存器都不占用内存地址。而且,即使编译器忽略寄存器类型修饰符把变量放在可设定地址的内存中,我们也无法取地址的限制仍然存在。
要想有效的利用寄存器存储类型,必须象汇编语言程序员那样了解处理器的内部构造,知道可用于存放变量的寄存器的数量和种类,以及他们是如何工作的。但是,不同计算机在这些细节上未必是一样的,因此对于一个可移植的程序来说,寄存器存储类型的作用不大。特别是现在很多编译器都能提供很好的优化效果,远比程序员来选择有效的多。不过,寄存器存储类型还是可以为优化器提供重要的参考。