4.7 作用域
作用域又称作用范围。在程序中出现的各种标识符,它们的作用域是不同的。不同标识符的作用域是怎样规定的,这就是下边要讨论的内容。
4.7.1 标识符的作用域规则
标识符的作用域规则如下:标识符只能在声明或定义它的范围内是可见的,而在该范围之外是不可见的。
现将其规则说明如下:
- 对于大多数标识符,声明和定义是一回事。只有少数标识符例外,声明和定义是两回事,例如外部变量、函数和类等。
- 标识符包含常量名、变量名、函数名、类名、对象名、语句标号等。凡是使用标识符规则定义的各种单词都属于标识符。
- 作用域有大有小,最大的是整个程序,最小的是块,中间的有文件、类和函数。标识符是在一定范围内定义的。
- 可见指的是可以进行存取或访问操作,不可见指的是不可进行存取或访问操作。任何标识符的作用域都遵从上述规则。
4.7.2 作用域的种类
不同标识符定义在不同的范围内,有着不同的作用域。按作用域的大小可分为如下几类:
- 程序级
- 文件级
- 函数级
- 块级
其中,程序级的作用域最大,它包含着组成该程序的所有文件。属于程序级作用域的有外部函数和外部变量。这类标识符是在某个文件中定义的,在该程序的其他文件中都是可见的,一般在访问之前需要加以声明。
属于文件级作用域的有内部函数和外部静态变量。这种作用域仅在定义它的文件内。对外部静态变量来讲,作用域是从定义时起到该文件结束为止。另外,用宏定义所定义的符号常量一般是属于文件级的。它也是从定义时起,在没有使用#undef
命令取消宏定义时,作用域到该文件结束时为止。
属于函数级作用域的有函数的形参和在函数内定义的自动类变量、寄存器类变量和内部静态类变量以及语句标号。这些标识符的作用域都是在它所定义的函数体内,即在定义它的函数体内从定义时开始,到该函数体结束为止。需要指出的是:不包括在该函数体内的分程序或者if
语句、switch
语句以及循环语句中所定义的变量。
属于块级作用域的有定义在分程序、if
语句、switch
语句以及循环语句中的自动类变量、寄存器类变量和内部静态类变量。它们也是从定义时开始在其相应的范围内是可见的。
4.7.3 关于重新定义标识符的作用域规定
在存在层次结构的作用域中,变量不能重复定义。这是指在相同的作用域内,不可有同名变量。但是,在不同的作用域内允许对某个变量进行重新定义。例如,在一个函数体内定义了分程序变量a,不能同时又定义一个同名变量a,但是可以在该函数体内的某个分程序中对变量a进行重新定义。下面的程序段是合法的:
void fun() {
int a;
{
float a;
}
}
这里有两个不同的变量名字都是a。按作用域的规则规定,int
型变量a在整个函数fun
内都是有效的、可见的。而float
型变量a仅在定义它的分程序内是可见的,在该分程序前后的函数体内是不可见的。那么,int
型变量a在分程序内(已定义了float
型变量a)是否可见呢?这将由重新定义标识符的作用域规定来回答。
重新定义标识符的作用域规定如下:在某个作用范围内定义的标识符在该范围内的子范围内中可以重新定义该标识符。这时原定义的标识符在子范围内是不可见的,但是它还是存在的,只是在子范围内由于出现了同名的标识符而被暂时隐藏起来。过了子范围后,它又是可见的。
在上述例子中,int
型变量a在分程序中是不可见的,它被隐藏起来,但它仍然存在。当过了分程序后,它又是可见的。在分程序中,float
型变量a是可见的,出了分程序float
型变量a是不可见的。
例4.19 分析下列程序的输出结果:
#include <iostream.h>
void main() {
int a = 5, b = 7, c = 10;
cout << a << "," << b << "," << c << endl;
{
int b = 8;
float c = 8.8;
cout << a << "," << b << "," << c << endl;
a = b;
{
int c;
c = b;
cout << a << "," << b << "," << c << endl;
}
cout << a << "," << b << "," << c << endl;
}
cout << a << "," << b << "," << c << endl;
}
执行该程序后,输出结果如下:
5,7,10
5,8,8.8
8,8,8
8,8,8.8
8,7,10
说明:在该程序中,先在函数体内定义了三个int
型变量a
、b
、c
;又在外层分程序中重新定义了b
和c
。因此在外层分程序中,a
仍然可见,但原来定义的int
型的b
和c
被隐藏起来,是不可见的。而重新定义的int
型变量b
和float
型变量c
是可见的。在内层分程序中,又重新定义了int
型变量c
,它在内层分程序中可见,而外层分程序中的float
型变量c
被隐藏起来。这里,由于变量a
没有被重新定义过,因此它在函数体内的各个分程序内都是可见的。变量a
在外层分程序中被改变了值,并保持改变后的值到内层分程序以及后面各个作用域中。
该程序是训练对重新定义标识符作用域规定的理解的很好的例子。请读者通过分析该程序掌握对重新定义标识符作用域的规定,并能在以后的程序设计中熟练运用。
例4.20 分析下列程序的输出结果:
#include <iostream.h>
void main() {
int x = 3;
for (; x > 0; x--) {
int x = 5;
cout << x << "\t";
}
cout << endl << x << endl;
}
执行该程序后,输出结果如下:
5 5 5
0
说明:在该程序中,先在函数体内定义了一个自动变量x
,并初始化为3。又在for
循环语句的循环体内重新定义了一个int
型变量x
,并初始化为5。这时,在循环体内,新定义的x
是可见的,而原来定义的x
被隐藏了起来。只有退出循环后,原来的x
才重新可见。因此,该程序的输出结果是for
循环中的x
值为5,循环结束后的x
值为0。
4.7.4 局部变量和全局变量
在C++程序中,所定义的变量可分为局部变量和全局变量两大类。
1. 局部变量
局部变量是指作用域在函数级和块级的变量,包括自动类变量、寄存器类变量和内部静态类变量以及函数参数。自动类变量是在函数体或分程序内定义的变量,它们的作用域分别在所定义的函数体或分程序内。
自动类变量是定义在函数体或分程序内的一种变量,定义时可加auto
说明符,也可以省略。与自动类变量作用域相同的另一种变量是寄存器类变量,这种变量也是定义在函数体内或分程序内,定义时前面加说明符register
。寄存器类变量有可能被存放到CPU的通用寄存器中。如果被存放到通用寄存器中便可提高存取速度,如果没被存放到通用寄存器中便按自动类变量处理。能否存放到通用寄存器中取决于当时通用寄存器是否空闲。因此,在定义寄存器类变量时,应注意如下几点:
- 该变量的数据长度与通用寄存器的长度相当。一般是
char
型和int
型变量。 - 寄存器类变量不宜定义过多,因为通用寄存器个数有限。
- 要选择一些使用频率高的变量优先定义为寄存器类变量。例如,多重循环的内重循环变量等。
内部静态类变量是定义在函数体内或分程序内,并且用说明符static
说明的一种变量。它的作用域与自动变量相同,但它的生存期较长。这是一种可见性与存在性不一致的变量,后面还将详述。
2. 全局变量
全局变量是指作用域在程序级和文件级的变量,包括外部变量和外部静态变量。外部变量的作用域是程序级的,即在一个文件中定义的外部变量,在该程序的其他文件中都可使用。外部变量是一种定义在函数体外,定义时不加任何存储类说明的变量。外部变量在引用之前需要声明,声明外部变量时应在前面加说明符extern
表示该变量是外部变量。在一个文件中,先引用后定义的外部变量引用前必须声明,这称为外部变量提前声明。在一个文件中定义的外部变量在另外一个文件要引用,则在引用前必须声明。由此可见,外部变量的定义和声明是两回事。在一个程序中,一个外部变量只能定义一次,但是可以声明多次。
外部静态变量被定义在函数体外,定义时前面加说明符static
表示为静态变量。外部静态变量的作用域是从定义该变量时起直到该文件结束,因此,称外部静态变量的作用域为文件级的。外部静态变量和外部变量的寿命都是长的。可见,外部静态变量的可见性与存在性是不一致的,因为它的作用域不是整个程序,但它的寿命存在于整个程序。
外部静态变量定义时有默认值。int
型为0,浮点型为0.0,char
型为空。
例4.21 分析下列程序的输出结果:
#include <iostream.h>
void other();
void main() {
int a = 3;
register int b = 5;
static int c;
cout << "a=" << a << ", b=" << b << ", c=" << c << endl;
other();
other();
}
void other() {
int a = 5;
static int b = 12;
a += 10;
b += 20;
cout << "a=" << a << ", b=" << b << endl;
}
执行该程序,输出结果如下:
a=3, b=5, c=0
a=15, b=32
a=15, b=52
说明:该程序中出现了三种不同存储类的局部变量:自动类、寄存器类和内部静态类。这里,值得注意的在如下两点:
- 内部静态类的变量定义时有默认值。
int
型为0,浮点型的为0.0,char
型的为空。外部变量和外部静态变量也是如此。而自动类和寄存器类的变量定义后在赋初值或赋值前没有默认值,其值是无意义的(通称垃圾值)。 - 内部静态变量定义在函数体内,它的作用域定义在它的函数体内或分程序内,然而,在定义它的作用域外,它虽然不可见,但是它仍然存在;它没有被释放掉,一旦回到作用域后,仍然保留其原来的值。这便是内部静态类变量的特点,也是它与自动类变量的区别。理解了这一点,就会分析和选用内部静态类变量。
例4.22 分析下列程序的输出结果:
该程序由三个文件组成,编译时要生成一个项目文件,三个文件的内容如下:
文件1(main.cpp)内容如下:
#include <iostream.h>
void fun1(), fun2(), fun3();
int i = 5;
void main() {
i = 20;
fun1();
cout << "main(): i=" << i << endl;
fun2();
cout << "main(): i=" << i << endl;
fun3();
cout << "main(): i=" << i << endl;
}
文件file1.cpp内容如下:
#include <iostream.h>
static int i;
void fun1() {
i = 50;
cout << "fun1(): i (static) = " << i << endl;
}
文件file2.cpp内容如下:
#include <iostream.h>
void fun2() {
int i = 15;
cout << "fun2(): i (auto) = " << i << endl;
if (i) {
extern int i;
cout << "fun2(): i (extern) = " << i << endl;
}
}
extern int i;
void fun3() {
i = 30;
cout << "fun3(): i (extern) = " << i << endl;
{
int i = 10;
cout << "fun3(): i (auto) = " << i << endl;
}
}
执行该程序输出结果如下:
fun1(): i (static) = 50
main(): i = 20
fun2(): i (auto) = 15
fun2(): i (extern) = 20
main(): i = 20
fun3(): i (extern) = 30
fun3(): i (auto) = 10
main(): i = 30
说明:该例的main.cpp
文件中首先定义了一个外部变量i
,并初始化为5。在main()
函数中,将i
的值更新为20。在file1.cpp
文件中,定义了一个外部静态变量i
,在fun1()
函数中,将它的值设为50。在file2.cpp
文件中,fun2()
函数中定义了一个自动变量i
,并初始化为15,在if
语句中又引用了外部变量i
。在fun3()
函数中,引用了外部变量i
,并将其值设为30,然后在块作用域内重新定义了一个自动变量i
,并初始化为10。
不同存储类的变量在不同作用域内的行为是十分重要的。第一个输出结果是调用fun1()
函数中的外部静态变量i
的值50。第二个输出结果是main()
中的外部变量i
的值20。第三个输出结果是fun2()
函数中的自动变量i
的值15。第四个输出结果是fun2()
函数中的外部变量i
的值20。第五个输出结果是main()
中的外部变量i
的值20。第六个输出结果是调用fun3()
函数中的外部变量i
的值30。第七个输出结果是fun3()
函数中的自动变量i
的值10。最后一个输出结果是main()
中的外部变量i
的值30。
通过分析不同位置的变量的存储类,可以理解其值的变化。
4.7.5 内部函数和外部函数
根据存储类,函数可分为两类:一类是内部函数,另一类是外部函数。
1. 内部函数
内部函数只在定义它的文件中可以被调用,而在同一程序的其他文件中不可以被调用。定义内部函数的格式如下:
static (类型说明) (函数名) (参数表) {
(函数体)
}
其中,static
是关键字,用它说明的函数是静态函数。
例4.23 内部函数的定义和调用。分析下列程序的输出结果:
#include <iostream.h>
int i = 10;
static int reset(), next(int), last(int), other(int);
void main() {
int i = reset();
for (int j = 1; j <= 3; j++) {
cout << i << "," << j << ",";
cout << next(i) << ",";
cout << last(i) << ",";
cout << other(i + j) << endl;
}
}
static int reset() {
return i;
}
static int next(int j) {
j = i++;
return j;
}
static int last(int j) {
static int i = 20;
j = i--;
return j;
}
static int other(int i) {
int j = 15;
return i + j;
}
该程序执行后的输出结果请读者自己分析。在分析该程序中,请注意:
- 该程序中定义了4个静态(即内部)函数,注意定义和说明的方法。
- 注意程序中变量
i
和j
在各函数中的存储类,其描述如表4-1所示。
变量 | 函数 | 存储类 |
---|---|---|
i | main() | 自动类 |
i | reset() | 外部类 |
i | next() | 外部类 |
i | last() | 内部静态类 |
i | other() | 局部变量 |
j | main() | 自动类 |
j | next() | 局部变量 |
j | last() | 局部变量 |
j | other() | 局部变量 |
2. 外部函数
外部函数是一种作用域在整个程序中的函数,包含组成该程序的所有文件。外部函数的定义格式如下:
[extern] (类型说明) (函数名) (参数表) {
(函数体)
}
其中,extern
是关键字,它是外部类函数的说明符。一般情况下,在定义函数时可以省略。
例4.24 外部函数的定义、声明和调用。
该程序由三个文件组成,即f1.cpp
、f2.cpp
和f3.cpp
。
文件f1.cpp
内容如下:
#include <iostream.h>
int i = 1;
extern int reset(), next(), last(), other(int);
void main() {
int i = reset();
for (int j = 1; j <= 3; j++) {
cout << i << "," << j << ",";
cout << next() << ",";
cout << last() << ",";
cout << other(i + j) << endl;
}
}
文件f2.cpp
内容如下:
#include <iostream.h>
static int i = 10;
extern int next() {
return i += 1;
}
extern int last() {
return i -= 1;
}
extern int other(int i) {
static int j = 5;
return i + j;
}
文件f3.cpp
内容如下:
extern int i;
extern int reset() {
return i;
}
该程序由三个文件和五个函数组成。除main()
函数外,其余四个函数被定义成外部函数。
该程序与例4.23有相似之处,但是仔细分析却不一样。分析该程序的输出结果时,仍然要搞清楚变量i
和j
在不同的函数中的存储类,否则无法确定它的值。该程序的输出结果,请读者自己分析。