首页 > C语言入门 > 函数 阅读:881

变量的作用域和存储方式,C语言变量作用域和存储方式完全攻略

pk10开户推出辅导班啦,包括「C语言辅导班、C++辅导班、算法/数据结构辅导班」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践 + 永久学习。QQ在线,随时响应!

变量按作用域可分为“局部变量”和“全局变量”。按存储方式又可分为“自动变量(auto)”、“静态变量(static)”、“寄存器变量(register)”和“外部变量(extern)”。注意,这里的“自动变量”不是指的“动态变量”。

什么叫“寄存器”?我们知道,内存条是用来存储数据的,硬盘也是存储数据的,而在 CPU 内部也有一些用来存储数据的区域,即寄存器。寄存器是 CPU 的组成部分,是 CPU 内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。它同内存一样,只不过它能存储的数据要少得多。

局部变量

局部变量是定义在函数内部的变量,全局变量是定义在函数外部的变量。

局部变量只在本函数内有效,即只有在本函数内才能使用,在本函数外不能使用。如果局部变量定义在子函数中,那么只能在该子函数中使用。该子函数调用完后,系统为该子函数中的局部变量分配的内存空间就会被释放掉。

如果局部变量定义在主函数 main 中,那么只能在 main 函数中使用,main 函数执行结束后,系统为其中的局部变量分配的内存空间就会被释放掉。主函数也不能使用其他函数中定义的变量。所以不同函数中可以定义同名的变量,但它们所表示的是不同的对象,互不干扰。

在一个函数内部,可以在复合语句中定义变量,这些变量只在本复合语句中有效,离开本复合语句就无效,且内存单元随即被释放。所谓复合语句就是用大括号“{}”括起来的多个语句。下面给大家举一个例子:
# include <stdio.h>
int main(void)
{
    int a = 1, b = 2;
    {
        int c;
        c = a + b;
    }
    printf("c = %dn", c);
    return 0;
}
编译结果:
error C2065: 'c' : undeclared identifier

这个程序有一个错误,编译结果显示:“变量 c 身份不明”,也就是说系统不认识 c 是什么。变量 a 和 b 在整个 main 函数中都有效,但变量 c 是在复合语句中定义的,所以只有在复合语句中才能使用。printf 在复合语句外,所以不能使用在复合语句中定义的变量 c。如果把 printf 放到复合语句中程序就编译成功了。
# include <stdio.h>
int main(void)
{
    int a = 1, b = 2;
    {
        int c;
        c = a + b;
        printf("c = %dn", c);
    }
    return 0;
}
输出结果:
c = 3

我们在编程的时候,if 语句、for 循环while 循环下面都有大括号。这就意味着如果局部变量是定义在这些大括号中的,那么就只能在这些大括号中使用。当这些大括号执行完之后,在其中定义的局部变量随即就会被释放。函数的形参也是局部变量,调用完该函数后,形参也会随之被释放。

所以局部变量的作用范围准确地说不是以函数来限定的,而是以大括号“{}”来限定的。在一个大括号中定义的局部变量就只能在这个大括号中使用,但因为每个函数体都是用大括号括起来的,而且我们一般也不会在函数体中的其他大括号中定义变量,所以习惯上就说“局部变量是定义在‘函数’内部的变量,只在本‘函数’内有效”。

全局变量

定义在函数内部的变量叫作局部变量,定义在函数外部的变量叫作全局变量。局部变量只能在该函数内才能使用;而全局变量可以被整个 C 程序中所有的函数所共用。它的作用范围是从定义的位置开始一直到整个 C 程序结束。所以根据定义位置的不同,全局变量的作用范围不同。

在一个函数中既可以使用本函数中的局部变量,也可以使用有效的全局变量。但需要注意的是,全局变量和局部变量命名决不能冲突。

前面说过,不同函数中可以定义同名的变量,但它们代表的是不同的对象,所以互不干扰,原因是局部变量的作用范围不同。但是全局变量和局部变量的作用范围可能有重叠,这时局部变量和全局变量的名字就不能定义成相同的了,否则就会互相干扰。如果在同一个作用范围内局部变量和全局变量重名,则局部变量起作用,全局变量不起作用。这样局部变量就会屏蔽全局变量。下面写一个程序验证一下:
# include <stdio.h>
int a = 10;  //定义一个全局变量
int main(void)
{   
    int a = 5;  //定义一个同名的局部变量
    printf("a = %dn", a);
    return 0;
}
输出结果是:
a = 5

所以定义变量的时候局部变量和全局变量最好不要重名。此外,如果局部变量未初始化,那么系统会自动将“–858993460”放进去(VC++6如此)。但如果全局变量未初始化,那么系统会自动将其初始化为 0。它们的这个区别主要源自于它们存储空间的不同。局部变量是在栈中分配的,而全局变量是在静态存储区中分配的。只要是在静态存储区中分配的,如果未初始化则系统都会自动将其初始化为 0。下面来写一个程序验证一下:
# include <stdio.h>
int a;  //定义一个全局变量
int main(void)
{   
    printf("a = %dn", a);
    return 0;
}
输出结果是:
a = 0

设置全局变量的作用是增加函数间数据联系的渠道。因为全局变量可以被多个函数所共用,这样就可以不通过实参和形参向被调函数传递数据。所以利用全局变量可以减少函数中实参和形参的个数,从而减少内存空间传递数据时的时间消耗。但是除非是迫不得已,否则不建议使用全局变量。

为什么不建议使用全局变量

1) 全局变量在程序的整个执行过程中都占用存储单元,而局部变量仅在需要时才开辟存储单元。

2) 全局变量降低了函数的通用性,因为函数在执行时要依赖那些全局变量。我们将一个功能编写成函数,往往希望下次遇到同样功能的时候直接复制过去就能使用。但是如果使用全局变量的话,这个函数和全局变量有联系,就不好直接复制过去了,还要考虑这个全局变量的作用。即使把全局变量也复制过去,但是如果该全局变量和其他文件中的变量重名的话,程序就会出问题。而且如果该全局变量和其他函数也有关联的话,程序也会出问题。所以全局变量降低了程序的可靠性和通用性。

在程序设计中,划分模块时要求模块的“内聚性”强、与其他模块的“关联性”弱。即模块的功能要单一,不要把许多互不相干的功能放到一个模块中,与其他模块间的相互影响要尽量小。而使用全局变量是不符合这个原则的。一般要求把C程序中的函数做成一个封闭体,除了可以通过“实参–形参”的渠道与外界发生联系外,没有其他渠道。这样的程序可移植性好,可读性强,而使用全局变量会使程序各函数之间的关系变得极其复杂。

3) 过多的全局变量会降低程序的清晰性。人们往往难以清楚地判断出每个时刻各个全局变量的值,原因是每个函数在执行的时候都有可能改变全局变量的值。这样程序就很容易出错,而且逻辑上很乱。因此要限制全局变量的使用,不到万不得已,不要使用全局变量。

4) 如果在同一个程序中,全局变量与局部变量同名,则在局部变量的作用范围内,全局变量就会被“屏蔽”,即它不起作用。

自动变量(auto)

前面定义的局部变量其实都是 auto 型,只不过 auto 可以省略,因为省略就默认是 auto 型。

静态变量(static)

static 还是比较重要的,static 可以用于修饰局部变量,也可以用于修饰全局变量。下面分别介绍一下。

用 static 修饰过的局部变量称为静态局部变量。局部变量如果不用 static 进行修饰,那么默认都是 auto 型的,即存储在栈区。而定义成 static 之后就存储在静态存储区了。

前面说过,存储在静态存储区中的变量如果未初始化,系统会自动将其初始化为 0。用 static 修饰过的局部变量也不例外,下面写一个程序验证一下:
# include <stdio.h>
int main(void)
{   
    static int a;
    printf("a = %dn", a);
    return 0;
}
输出结果是:
a = 0

静态存储区主要用于存放静态数据和全局数据。存储在静态存储区中的数据存在于程序运行的整个过程中。所以静态局部变量不同于普通局部变量,静态局部变量是存在于程序运行的整个过程中的。下面来写一个程序看一下:
# include <stdio.h>
void Increment(void);  //函数声明
int main(void)
{
    Increment();
    Increment();
    Increment();
    return 0;
}
void Increment(void)
{
    static int x = 0;
    int y = 0;
    ++x;
    ++y;
    printf("x = %d, y = %d", x, y);
    printf("n");
    return;
}
输出结果是:
x = 1, y = 1
x = 2, y = 1
x = 3, y = 1

我们看到,变量 x 是静态局部变量,而变量 y 是普通的局部变量。从输出结果可以看出,y 的值一直都是 1,而 x 的值是变化的。这是因为变量 y 是普通的局部变量,每次函数调用结束后就会被释放;而变量 x 是用 static 修饰的,所以它是静态局部变量,即使函数调用结束它也不会被释放,而是一直存在于程序运行的整个过程中,直到整个程序运行结束后才会被释放。这就相当于全局变量,但它不是全局变量,静态局部变量仍然是局部变量,仍然不能在它的作用范围之外使用。

有人可能会问,如果静态局部变量在函数调用结束后也不释放,那函数每调用一次变量 x 不就重新定义一次,并重新赋值一次了吗?

静态局部变量仅在第一次函数调用时定义并初始化,以后再次调用时不再重新定义和初始化,而是保留上一次函数调用结束后的值。也就是说“static int x=0;”这条语句只会执行一次。

下面再来看全局变量。全局变量默认都是静态的,都是存放在静态存储区中的,所以它们的生存周期固定,都是存在于程序运行的整个过程中。所以一个变量生命周期的长短本质上是看它存储在什么地方。存储在栈区中的变量,在函数调用结束后内存空间就会被释放;而存储在静态存储区中的变量会一直存在于程序的整个运行过程中。这就是局部变量和全局变量生命周期不同的原因。

虽然全局变量本身就是存储在静态存储区的,但它仍然可以用 static 进行修饰。而且修饰和不修饰是有区别的。用 static 修饰全局变量时,会限定全局变量的作用范围,使它的作用域仅限于本文件中。这个是使用 static 修饰全局变量的主要目的。

那么,不用 static 修饰,全局变量不也是只能在本文件中使用吗?这么说不完全对,因为虽然全局变量的作用范围不会自己主动作用到其他文件中,但不代表其他文件不会使用它。如果不用 static 进行修饰,那么其他文件只需要用 extern 对该全局变量进行一下声明,就可以将该全局变量的作用范围扩展到该文件中。但是当该全局变量在定义时用static进行修饰后,其他文件不论通过什么方式都不能访问该全局变量。

而且如果一个项目的多个 .c 文件中存在同名的全局变量,那么在编译的时候就会报错,报错的内容是“同一个变量被多次定义”。但是如果在这些全局变量前面都加上 static,那么编译的时候就不会报错。因为用 static 修饰后,这些全局变量就只属于各自的 .c 文件了,它们是相互独立的,所以编译的时候就不会发生冲突而产生“多次定义”的错误。所以使用 static 定义全局变量是非常有用的,因为当一个项目中有很多文件的时候,重名不可避免。这时候只要在所有的全局变量前面都加上 static 就能很好地解决这个问题。定义全局变量时用 static 进行修饰可以大大提高代码的质量。

总结:
1) 用 static 修饰后的局部变量称为静态局部变量。因为局部变量在函数调用结束后就会被释放,所以如果不希望它被释放的话,就在定义时用 static 对它进行修饰。如:
static int a;
2) 定义成 static 型的变量是存放在静态存储区中的,在程序的整个运行过程中都不会被释放。这跟全局变量很像,但它不是全局变量。静态局部变量仍然是局部变量,只不过它的生命周期改变了,但作用范围并没有改变,所以其他函数仍然不能使用它。它之所以跟全局变量很像是因为它们都是存放在静态存储区中,所以它们都存在于程序运行的整个过程中。

3) 如果把局部变量定义成 static 型,那么在程序的整个运行过程中,它只在编译时赋初值一次,以后每次调用函数时不会再重新给它赋值,而是保留上一次函数调用结束时的值。如果定义静态局部变量时没有初始化的话,那么在编译时系统会自动给它赋初值 0(对数值型变量)或空字符 '0'(对字符变量)。

4) 但是静态局部变量跟全局变量一样,长期占用内存不放,而且降低了程序的可读性,当调用次数过多时往往弄不清楚其当前值是什么。所以若非必要,不要过多使用静态局部变量。

5) 对全局变量使用 static 进行修饰是 static 非常重要的用法。如果不用 static 对全局变量进行修饰,那么其他文件只需要使用 extern 对该全局变量进行声明就可以直接使用该全局变量。但是当用 static 进行修饰后,即使其他文件用 extern 对它进行声明也不能使用它。这种用法可以有效防止并解决不同文件中全局变量重名的问题。

寄存器变量(register)

无论是存储在静态存储区中还是存储在栈区中,变量都是存储在内存中的。当程序用到哪个变量的时候,CPU 的控制器就会发出指令将该变量的值从内存读到 CPU 里面。然后 CPU 再对它进行处理,处理完了再把结果写回内存。

但是除了内存可以存储数据之外,CPU 内部也有一些用来存储数据的区域,这就是寄存器。寄存器是 CPU 的组成部分,是 CPU 内部用来存放数据的小型存储区域,用来暂时存放参与运算的数据和运算结果。与内存相比,寄存器所能存储的数据要小得多,但是它的存取速度要比内存快很多。

那为什么寄存器的存取速度比内存快呢?最主要的原因是因为它们的硬件设计不同。计算机中硬件运行速度由快到慢的顺序是:

寄存器>缓存>内存>固态硬盘>机械硬盘

为了提高代码执行的效率,可以考虑将经常使用的变量存储到寄存器中。比如循环变量和循环体内每次循环都要使用的局部变量。这种变量叫作寄存器变量,用关键字 register 声明。如
register  int  a;
但是需要注意的是,register 关键字只是请求编译器尽可能地将变量存储在 CPU 内部的寄存器中,但并不一定。因为我们说过,寄存器所能存储的数据是很少的,所以如果寄存器已经满了,那么即使你用 register 进行声明,数据仍然会被存储到内存中。而且需要注意的是,并不是所有的变量都能定义成寄存器变量,只有局部变量才可以。或者说寄存器变量也是一个局部变量,在函数调用结束后就会被释放。

register 看起来很有用,但是要跟大家说的是,这个修饰符现在已经不用了。现在的编译器都比较智能化,它会自动分析,即使变量定义的是 auto 型(默认的),但如果它发现这个变量经常使用,那么它也会把这个变量放到寄存器中。同样,即使定义成 register 型,但如果寄存器中没地方存放,那么它也会把这个变量放到内存中。

这时有些人就提出一个疑问:“既然寄存器速度那么快,那么为什么不把计算机的内存和硬盘都改成寄存器?我们前面说过,寄存器之所以比内存速度快,最主要的原因是因为它们的硬件设计不同。从硬件设计的角度来看,寄存器的制作成本要比内存高很多!而且寄存器数量的增加对 CPU 的性能也提出了极高的要求,而这往往是很难实现的。

外部变量(extern)

extern 变量是针对全局变量而言的。通过前面了解到,全局变量都是存放在静态存储区中的,所以它们的生存期是固定的,即存在于程序的整个运行过程中。但是对于全局变量来说,还有一个问题尚待解决,就是它的作用域究竟从什么位置起,到什么位置结束。作用域是包括整个文件范围,还是只包括文件中的一部分?是在一个文件中有效,还是在程序的所有文件中都有效?

一般来说,外部变量是在函数的外部定义的全局变量,它的作用域是从变量的定义处开始,到本程序文件的末尾结束。在此作用域内,全局变量可以被程序中各个函数所引用。但是有时程序设计人员希望能扩展全局变量的作用域,如以下几种情况:

1) 在一个文件内扩展全局变量的作用域

如果全局变量不在文件的开头定义,其有效的作用范围只限于定义处到文件结束。在定义点之前的函数不能引用该全局变量。如果出于某种考虑,在定义点之前的函数需要引用该全局变量,那么就在引用之前用关键字extern对该变量作“外部变量声明”,表示将该全局变量的作用域扩展到此位置,比如:
extern  int  a;
有了此声明就可以从“声明”处起,合法地使用该全局变量了。

2) 将外部变量的作用域扩展到其他文件

一个 C 程序可以由一个或多个 .c 文件组成。如果程序只由一个 .c 文件组成,那么使用全局变量的方法前面已经介绍了。如果程序由多个 .c 文件组成,那么如何在一个文件中引用另一个文件中定义的全局变量呢?

假如在 1.c 中定义了全局变量“int a=10;”,如果 2.c 和 3.c 也想使用这个变量 a,而我们不能在 2.c 和 3.c 中重新定义这个 a,否则在编译链接时就会出现“重复定义”的错误。正确的做法是在 2.c 和 3.c 中分别用 extern 对 a 作“外部变量声明”,即在 2.c 和 3.c 中使用 a 之前作如下声明:

这样在编译和链接时,当编译器在 2.c 中遇到变量 a 的声明时就会知道它有“外部链接”,就会从其他文件中寻找a的定义,并将在另一文件中定义的全局变量a的作用范围扩展到本文件中。这样在本文件中也可以合法地使用全局变量 a 了。但是现在问大家一个问题:“以 2.c 为例,如果在 2.c 中对 a 进行多次声明,即写多个“externinta;”,那么程序会不会有错?”答案是不会,C 语言中是允许多次声明的,但有效的只有一个。

与全局变量一样,同一个项目的不同 .c 文件中也不能定义重名的函数。如果 2.c 和 3.c 想要使用 1.c 中定义的函数,那么也只需要在 2.c 和 3.c 中分别对该函数进行声明就可以了。即直接把 1.c 中对函数的声明拷贝过来就行了。

与全局变量不同的是,它的前面不用加 extern。因为对于函数而言,默认就是 extern,所以声明时前面的 extern 可以省略。此外在实际编程中,我们一般不会直接把 1.c 中某个函数的声明拷贝到 2.c 和 3.c 中,而是把该声明写在一个 .h 头文件中,然后分别在 2.c 和 3.c 中用 #include 包含该头文件即可。如果不用头文件当然也可以,但此时 2.c 和 3.c 中都要写该函数的声明,这样如果要修改的话就都要修改。而如果写在头文件中,要修改的话就只需要改头文件中的内容就可以了。

关于头文件后面还会专门介绍。此外,同一个 .c 文件中对同一个函数进行多次声明也是允许的,但它们起作用的只有一个。