作用域

作用域(Scope)是一个很关键的概念。为了演示,我们先从复合语句谈起:

int a{42};
{
    int b{42};
    a = 56;
    b = 56;
}
a = 56;
b = 56; // 编译报错!

上面这段代码声明并定义了两个变量 ab,然后进行赋值。但是出现了问题:第 7 行可以给 a 成功赋值,但是第 8 行却无法给 b 赋值,编译会报错。这是为什么呢?

原因就是 ab 声明语句的位置不同。对于不同位置的声明引入的名字,这些名字可以使用的范围是不同的。一个名字可以被使用的范围称为这个名字的作用域

对于变量来说,变量名的作用域从声明语句中名字被写下的那一位置开始(这个位置被称为声明点)。如果它的声明语句位于复合语句内,则这个名字的作用域结束于复合语句的结尾 }。因此刚才的名字 b 的作用域从第 3 行开始(因为在这一行被声明),到第 6 行结束(因为这一行外面的复合语句到达了结尾)。

因此,第 8 行已经超出了名字 b 的作用范围,便不能再使用了,编译出现错误。而名字 a 的作用域仍然持续,所以名字 a 还可以使用。

显然,在某个名字的声明点之前,这个名字是没法用的。这在某些教材中被称为“先声明、后使用”。

除此之外,如果名字声明在 for 语句和 while 语句内,则这个名字的作用域结束于循环语句的结尾。比如:

for (int i{0}; i < 10; i++) { // i 作用域开始
    cout << i << endl;
}      // i 作用域结束
i = 0; // 错误

这里名字 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

名字的隐藏规则会一定程度上造成麻烦。有的时候我们想操作外层的名字,但由于被隐藏了而误操作了里层的名字,这个时候尽管不会报编译错误,但是程序的运行结果可能会和预期不符。因此我在这里建议:在嵌套的复合语句中,尽量避免声明同名的名字,尽管这是合法的。

作用域不是生存期。作用域是名字的属性,指明这个名字在代码的何处可以使用;生存期是变量(对象)的属性,指明这个变量何时分配内存、释放内存。

最近更新:
代码未运行