作用域
作用域(Scope)是一个很关键的概念。为了演示,我们先从复合语句谈起:
上面这段代码声明并定义了两个变量 a
和 b
,然后进行赋值。但是出现了问题:第 7 行可以给 a
成功赋值,但是第 8 行却无法给 b
赋值,编译会报错。这是为什么呢?
原因就是 a
和 b
声明语句的位置不同。对于不同位置的声明引入的名字,这些名字可以使用的范围是不同的。一个名字可以被使用的范围称为这个名字的作用域。
对于变量来说,变量名的作用域从声明语句中名字被写下的那一位置开始(这个位置被称为声明点)。如果它的声明语句位于复合语句内,则这个名字的作用域结束于复合语句的结尾 }
。因此刚才的名字 b
的作用域从第 3 行开始(因为在这一行被声明),到第 6 行结束(因为这一行外面的复合语句到达了结尾)。
因此,第 8 行已经超出了名字 b
的作用范围,便不能再使用了,编译出现错误。而名字 a
的作用域仍然持续,所以名字 a
还可以使用。
显然,在某个名字的声明点之前,这个名字是没法用的。这在某些教材中被称为“先声明、后使用”。
除此之外,如果名字声明在 for 语句和 while 语句内,则这个名字的作用域结束于循环语句的结尾。比如:
这里名字 i
声明于 for
语句的初始语句中,从此开始了 i
的作用域。它结束于 for 语句的结尾,也就是第三行的大括号 }
。因此你可以在 for 语句内部使用名字 i
,但不能在除此之外的地方(比如第 4 行)使用。
对于分支语句也有这样的特点,这里不再重复。
将临时的变量放在复合语句中,使得它的作用域缩小是一个好习惯。因为这样可以有效避免命名冲突——你可能在一大段代码的前面命名一个变量为 temp
,然后忘记了在后面又命名了一个变量也叫 temp
,这个时候电脑就搞不清楚你的 temp
到底指的是哪个 temp
。因此 C++ 不允许在同一层复合语句中出现相同名字的重复定义。如果把这两个声明用不同的复合语句括起,那么它们只在各自的作用域内起作用,就不会出现问题了。
对于变量来说,C++ 不允许重复定义,但允许重复声明;这些声明的名字都代表同一个被定义的变量——尽管我们并不知道如何只声明而不定义一个变量。
名字的隐藏
最后一点是有关名字的隐藏。对于一个位于复合语句中的声明,如果它已经位于某个在外层声明的相同名字的作用域内,则从此刻开始,那个外层的名字会被隐藏起来。听上去非常绕,来看一个实际的例子就好理解了:
int a{42}; // 姑且称这个变量叫“a1”吧
{
a = 42; // 这里是对变量“a1”进行赋值
int a{56}; // 管这个变量叫“a2”:
// 由于外层已经出现了一个同名变量 a ,因此变量“a1”被隐藏
// 被隐藏的变量“a1”的名字没法再用了。接下来提到的名字 a
// 都指代刚刚声明的变量“a2”
a = 63; // 因此这里赋值的是变量“a2”
}
a = 63; // 这里出了“a2”的作用域,因此这个是“a1”
也就是说,尽管 C++ 不允许在同一层复合语句中出现相同名字的定义,但是如果是这种“嵌套”的定义,则在这两个声明的名字“重叠”的地方,会选择里层的那个名字来使用。刚刚的例子中,外层和里层都声明了名字 a
,但是使用的时候外层名字 a
被隐藏,只能使用里层的名字 a
。
名字的隐藏规则会一定程度上造成麻烦。有的时候我们想操作外层的名字,但由于被隐藏了而误操作了里层的名字,这个时候尽管不会报编译错误,但是程序的运行结果可能会和预期不符。因此我在这里建议:在嵌套的复合语句中,尽量避免声明同名的名字,尽管这是合法的。
作用域不是生存期。作用域是名字的属性,指明这个名字在代码的何处可以使用;生存期是变量(对象)的属性,指明这个变量何时分配内存、释放内存。