在函数体内定义的变量称为局部变量(与全局变量相对,我们将在以后的章节中讨论):
int add(int x, int y)
{
int z{ x + y }; // z 是局部变量
return z;
}
函数参数通常也被认为是局部变量:
int add(int x, int y) // 函数参数 x 和 y 是局部变量
{
int z{ x + y };
return z;
}
在本课中,我们将更详细地了解局部变量的一些属性。
局部变量的生存期
在对象和变量简介中,我们讨论了如何定义变量,例如 int x; 执行此语句时会导致变量被实例化(创建)。 函数参数在进入函数时创建并初始化,函数体内的变量在定义时创建并初始化。
例如:
int add(int x, int y) // x 和 y 在这里创建并初始化
{
int z{ x + y }; // z 在这里创建并初始化
return z;
}
局部变量在定义它的花括号组末尾(或者对于函数参数,在函数末尾)以与创建相反的顺序销毁。
int add(int x, int y)
{
int z{ x + y };
return z;
} // z、y 和 x 变量被销毁
就像一个人的生命周期被定义为出生和死亡之间的时间一样,一个对象的生命周期被定义为它的创建和销毁之间的时间。请注意,变量创建和销毁发生在程序运行时(称为运行时),而不是在编译时。因此,生命周期是一个运行时属性。
实际上,C++ 规范为编译器提供了很大的灵活性来确定何时创建和销毁局部变量。出于优化目的,可以提前创建对象,也可以稍后销毁对象。大多数情况下,局部变量在进入函数时创建,并在函数退出时以相反的创建顺序销毁。我们将在以后的课程中讨论调用堆栈时更详细地讨论这一点。
这是一个稍微复杂的程序,演示了名为 x 的变量的生命周期:
#include <iostream>
void doSomething()
{
std::cout << "Hello!\n";
}
int main()
{
int x{ 0 }; // x's lifetime begins here
doSomething(); // x is still alive during this function call
return 0;
} // x's lifetime ends here
在上面的程序中,x 的生命周期是从定义点到 main 函数结束。 这包括执行函数 doSomething 期间所花费的时间。
局部作用域
标识符的范围决定了在源代码中可以看到和使用标识符的位置。当一个标识符可以被看到和使用时,我们说它在范围内。当一个标识符不可见时,我们就不能使用它,我们就说它超出了范围。范围是一个编译时属性,尝试使用不在范围内的标识符将导致编译错误。
局部变量的作用域从变量定义点开始,到定义该变量的花括号组末尾为止(或者对于函数参数,在函数末尾结束)。这确保了变量在定义点之前不能使用(即使编译器选择在此之前创建它们)。在一个函数中定义的局部变量也不在被调用的其他函数的范围内。
#include <iostream>
// x is not in scope anywhere in this function
void doSomething()
{
std::cout << "Hello!\n";
}
int main()
{
// x 不能在这里使用,因为它不在范围内
int x{ 0 }; // x 在局部作用域内,现在可以在此函数中使用
doSomething();
return 0;
} // x 超出了作用域,无法再使用
超出作用域 vs 即将超出作用域
术语“out of scope”(超出作用域)和“going out of scope”(即将超出作用域)对于新程序员来说可能会令人困惑。以下是对这两个术语的澄清:
- Out of Scope(超出作用域): 当一个标识符在代码中无法被访问时,它被称为“out of scope”。例如,如果在函数内部声明一个变量
x
,那么x
的作用域从它在该函数内定义的地方开始,一直延续到该函数的结束。一旦超出该函数的代码区域,x
被视为超出范围,无法访问。 - Going Out of Scope(即将超出作用域): “going out of scope”(即将超出作用域)通常用于对象,特别是在描述它们的生命周期时。一个对象被认为“going out of scope”是指在创建或实例化它的块的末尾(通常由闭合花括号
{}
标志)时。在上面的示例中,如果在main
函数内创建一个名为x
的对象,那么当main
函数结束时,x
就会超出范围,并被销毁。
另一个例子
这是一个稍微复杂的示例。请记住,生命周期是一个运行时属性,而作用域是一个编译时属性,因此尽管我们在同一程序中讨论两者,但它们是在不同的点强制执行的。
#include <iostream>
int add(int x, int y) // x 和 y 在这里被创建并进入作用域
{
// x 和 y 只在这个函数内可见和可用
return x + y;
} // y 和 x 在这里超出范围并被销毁
int main()
{
int a{ 5 }; // a 在这里被创建、初始化,并进入作用域
int b{ 6 }; // b 在这里被创建、初始化,并进入作用域
// a 和 b 只在这个函数内可见和可用
std::cout << add(a, b) << '\n'; // 调用函数 add() 传递 x=5 和 y=6
return 0;
} // b 和 a 在这里超出范围并被销毁
参数 x 和 y 在调用 add 函数时创建,只能在函数 add 中看到/使用,并在 add 结束时销毁。 变量 a 和 b 在 main 函数中创建,只能在 main 函数中查看/使用,并在 main 结束时销毁。
为了加深您对所有这些如何组合在一起的理解,让我们更详细地回顾一下这个程序。 以下情况按顺序发生:
- 执行从 main 的顶部开始。
- 创建变量 a 并赋予值 5。
- 创建变量 b 并赋予值 6。
- 使用参数值 5 和 6 调用函数 add。
- add 参数 x 和 y 分别创建并初始化为值 5 和 6。
- 表达式 x + y 的计算结果为 11。
- add 将值 11 复制回调用者 main。
- 添加参数 y 和 x 被破坏。
- main函数中将 11 打印到控制台。
- main函数中将 0 返回给操作系统。
- 变量 b 和 a 被销毁。
请注意,如果函数 add 被调用两次,参数 x 和 y 将被创建和销毁两次——每次调用一次。 在具有大量函数和函数调用的程序中,变量经常被创建和销毁。
下面这段代码和解释展示了函数内的变量具有各自的作用域和命名空间,即使它们的名称在不同的函数中重复使用也没有问题。
#include <iostream>
int add(int x, int y) // add 函数的 x 和 y 在此处创建并进入作用域
{
// add 函数的 x 和 y 只在该函数内可见和可用
return x + y;
} // add 函数的 y 和 x 在此处超出作用域并被销毁
int main()
{
int x{ 5 }; // main 函数的 x 在此处创建、初始化,并进入作用域
int y{ 6 }; // main 函数的 y 在此处创建、初始化,并进入作用域
// main 函数的 x 和 y 只在该函数内可见和可用
std::cout << add(x, y) << '\n'; // 调用函数 add() 传递 x=5 和 y=6
return 0;
} // main 函数的 y 和 x 在此处超出作用域并被销毁
在这个示例中,我们只是将函数 main
内的变量 a
和 b
的名称更改为 x
和 y
。尽管函数 main
和 add
都有名为 x
和 y
的变量,但这个程序编译和运行完全相同。为什么会这样呢?
首先,我们需要认识到,尽管函数 main
和 add
都有名为 x
和 y
的变量,但这些变量是独立的。函数 main
中的 x
和 y
与函数 add
中的 x
和 y
没有任何关联,它们只是恰好使用了相同的名称。
其次,在函数 main
内部,名称 x
和 y
引用的是 main
局部作用域内的变量 x
和 y
。这些变量只能在 main
内部被看到(和使用)。同样,当在函数 add
内部时,名称 x
和 y
引用的是函数参数 x
和 y
,它们只能在 add
内部被看到(和使用)。
简而言之,函数 add
和 main
都不知道另一个函数具有相同名称的变量。因为作用域没有重叠,编译器总是清楚地知道在任何时候引用的是哪个 x
和 y
。
用于函数体中声明的函数参数或变量的名称仅在声明它们的函数中可见。 这意味着可以命名函数内的局部变量,而无需考虑其他函数中的变量名称。 这有助于保持功能独立。
在哪里定义局部变量
#include <iostream>
int main()
{
std::cout << "Enter an integer: ";
int x{}; // x defined here
std::cin >> x; // and used here
std::cout << "Enter another integer: ";
int y{}; // y defined here
std::cin >> y; // and used here
int sum{ x + y }; // sum can be initialized with intended value
std::cout << "The sum is: " << sum << '\n';
return 0;
}
在上面的示例中,每个变量都是在首次使用之前定义的。 没有必要对此严格要求——如果您喜欢交换第 5 行和第 6 行,也可以。
由于旧的、更原始的编译器的限制,C 语言过去要求所有局部变量都在函数的顶部定义。使用该风格的等效 C++ 程序如下所示:
#include <iostream>
int main()
{
int x{}, y{}, sum{}; // how are these used?
std::cout << "Enter an integer: ";
std::cin >> x;
std::cout << "Enter another integer: ";
std::cin >> y;
sum = x + y;
std::cout << "The sum is: " << sum << '\n';
return 0;
}
由于以下几个原因,这种样式并不是最理想的:
- 这些变量的预期用途在定义时并不明显。 您必须浏览整个函数才能确定每个函数的使用位置和方式。
- 预期的初始化值可能在函数顶部不可用(例如,我们无法将 sum 初始化为其预期值,因为我们还不知道 x 和 y 的值)。
- 变量的初始值设定项和首次使用之间可能有很多行。 如果我们不记得它初始化的值,我们将不得不滚动回函数的顶部,这会分散我们的注意力。
C99 语言标准中取消了此限制。
原创文章,作者:jkhxw,如若转载,请注明出处:https://www.jkhxw.com/cpp-local-scope/