C语言之变量作用域和存储类型

一、作用域和生存期

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*/
int add();
main()
{ 
    int result; 
    result=add();
    printf("%d" ,result);
    result=add();
    printf("%d" ,result);
    result=add();
    printf("%d",result);
}
int add()
{
    int num=50;
    num++;
    return num;
}


/*2.C*/
int add();
main()
{
    int result;
    result=add();
    printf("%d" ,result);
    result=add();
    printf("%d" ,result);
    result=add();
    printf("%d",result);
}
int add()
{
    static int num=50;
    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);
}

使用寄存器存储类型的目的是让程序员指定某个局部变量存放在计算机的某个硬件寄存器里而不是内存中,以提高程序的运行速度。不过,这只是反映了程序员的主观意愿,编译器可以忽略寄存器存储类型修饰符。
寄存器变量的地址是无法取得的,因为绝大多数计算机的硬件寄存器都不占用内存地址。而且,即使编译器忽略寄存器类型修饰符把变量放在可设定地址的内存中,我们也无法取地址的限制仍然存在。
要想有效的利用寄存器存储类型,必须象汇编语言程序员那样了解处理器的内部构造,知道可用于存放变量的寄存器的数量和种类,以及他们是如何工作的。但是,不同计算机在这些细节上未必是一样的,因此对于一个可移植的程序来说,寄存器存储类型的作用不大。特别是现在很多编译器都能提供很好的优化效果,远比程序员来选择有效的多。不过,寄存器存储类型还是可以为优化器提供重要的参考。

添加新评论