2-5 局部作用域介绍

局部变量

在函数体内定义的变量称为局部变量local variables(与全局变量相对,全局变量将在后续章节中讨论):

int add(int x, int y)
{
    int z{ x + y }; // z is a local variable

    return z;
}

函数形参通常也被视为局部变量,我们将它们作为局部变量来处理:

int add(int x, int y) // function parameters x and y are local variables
{
    int z{ x + y };

    return z;
}

在本节课中,我们将更详细地探讨局部变量的一些特性。


局部变量的生命周期

第1.3课——对象与变量介绍中,我们讨论了诸如 int x; 这样的变量定义如何在执行该语句时实例化变量。函数形参在进入函数时创建并初始化,而函数体内的变量则在定义处创建并初始化。

例如:

int add(int x, int y) // x and y created and initialized here
{
    int z{ x + y };   // z created and initialized here

    return z;
}

自然而然地,人们会接着问:“那么实例化变量何时会被销毁?”局部变量会在其定义所在的大括号结束时按与创建相反的顺序被销毁(若为函数形参,则在函数结束时销毁)。

int add(int x, int y)
{
    int z{ x + y };

    return z;
} // z, y, and x destroyed here

正如人的生命周期被定义为出生至死亡之间的时间,对象的生命周期也被定义为其创建至销毁之间的时间。请注意,变量的创建与销毁发生在程序运行时(称为运行时),而非编译时。因此,生命周期属于运行时属性。

对于高级读者

上述关于对象创建、初始化和销毁的规则具有强制性。这意味着对象必须在定义点之前完成创建和初始化,且销毁时间不得早于其所在花括号块的结束位置(若为函数形参,则在函数结束时销毁)。

实际上,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 函数执行期间所花费的时间。


当对象被销毁时会发生什么?

在大多数情况下,什么也不会发生。被销毁的对象只是变得无效了。

对于高级读者

若对象为类类型对象,在销毁前会调用名为析构函数的特殊函数。多数情况下析构函数不执行任何操作,此时不会产生额外开销。我们将在第15.4节——析构函数介绍中详细介绍析构函数。

在对象被销毁后对其进行任何操作都将导致行为未定义。

销毁之后某个时刻,该对象占用的内存将被释放deallocated(腾出空间供重新使用)。


局部作用域(块作用域)

标识符的作用域scope决定了它在源代码中可被访问和使用的范围。当标识符可被访问和使用时,我们称其处于作用域内in scope;当标识符不可见时,我们无法使用它,此时称其作用域外out of scope。作用域是编译时属性,尝试使用不在作用域内的标识符将导致编译错误。

局部变量的标识符具有局部作用域。具有局部作用域local scope(技术上称为块作用域block scope)的标识符,可在定义点至包含该标识符的最内层花括号对结束处(若为函数形参,则在函数结束处)使用。这确保局部变量无法在定义点之前(即使编译器选择提前创建)或销毁之后被使用。在某个函数中定义的局部变量,在被调用的其他函数中同样不在作用域内。

下面的程序演示了名为 x 的变量的范围:

#include <iostream>

// x is not in scope anywhere in this function
void doSomething()
{
    std::cout << "Hello!\n";
}

int main()
{
    // x can not be used here because it's not in scope yet

    int x{ 0 }; // x enters scope here and can now be used within this function

    doSomething();

    return 0;
} // x goes out of scope here and can no longer be used

在上面的程序中,变量 x 在定义处进入作用域。x 在包含该标识符的最内层大括号对结束时(即函数 main() 的闭合大括号)退出作用域。请注意,变量 x 在函数 doSomething 内部任何位置都不在作用域内。函数 main 调用函数 doSomething 的事实在此上下文中无关紧要。


“Out of scope” vs “going out of scope”

对于新手程序员而言,“作用域外Out of scope”和“即将超出作用域going out of scope”这两个术语可能令人困惑。

当标识符在代码中无法被访问时,即处于作用域外。在上例中,标识符 x 从其定义点到 main 函数结尾都处于作用域内。在该代码区域之外,标识符 x 则处于作用域外。

“即将超出作用域”一词通常用于对象而非标识符。当对象在实例化所在的作用域(即大括号结束处)结束时,我们说该对象即将超出作用域。在上例中,名为x的对象在main函数结束时即将超出作用域。

局部变量的生命周期在作用域结束时终止,因此局部变量此时会被销毁。

需注意并非所有类型的变量在作用域结束时都会被销毁,后续课程将展示相关示例。


另一个示例

这里是一个稍复杂些的示例。请记住,生命周期是运行时属性,作用域则是编译时属性,因此尽管我们在同一个程序中讨论二者,但它们在不同的阶段被强制执行。

#include <iostream>

int add(int x, int y) // x and y are created and enter scope here
{
    // x and y are usable only within add()
    return x + y;
} // y and x go out of scope and are destroyed here

int main()
{
    int a{ 5 }; // a is created, initialized, and enters scope here
    int b{ 6 }; // b is created, initialized, and enters scope here

    // a and b are usable only within main()

    std::cout << add(a, b) << '\n'; // calls add(5, 6), where x=5 and y=6

    return 0;
} // b and a go out of scope and are destroyed here

形参 x 和 y 在调用 add 函数时创建,仅能在 add 函数内部被访问/使用,并在 add 函数结束时销毁。变量 a 和 b 在 main 函数内部创建,仅能在 main 函数内部被访问/使用,并在 main 函数结束时销毁。
为帮助您理解这些元素如何协同工作,让我们更详细地追踪程序执行过程。按顺序发生以下事件:

  • 执行从 main 函数顶部开始。
  • main 变量 a 被创建并赋值 5。
  • main 变量 b 被创建并赋值 6。
  • 调用 add 函数,传入实参值 5 和 6。
  • add 形参 x 和 y 被创建,分别初始化为 5 和 6。
  • 表达式 x + y 被求值,产生值 11。
  • add 将值 11 复制回调用方 main。
  • add函数的形参y和x被销毁。
  • main函数向控制台输出11。
  • main函数向操作系统返回0。
  • main函数的变量b和a被销毁。

至此完成。

需注意:若add函数被调用两次,参数x和y将分别在每次调用中创建并销毁。在包含大量函数及函数调用的程序中,变量的创建与销毁操作将频繁发生。


函数的分离

在上例中,很容易看出变量 a 和 b 与 x 和 y 是不同的变量。

现在考虑以下类似的程序:

#include <iostream>

int add(int x, int y) // add's x and y are created and enter scope here
{
    // add's x and y are visible/usable within this function only
    return x + y;
} // add's y and x go out of scope and are destroyed here

int main()
{
    int x{ 5 }; // main's x is created, initialized, and enters scope here
    int y{ 6 }; // main's y is created, initialized, and enters scope here

    // main's x and y are usable within this function only
    std::cout << add(x, y) << '\n'; // calls function add() with x=5 and y=6

    return 0;
} // main's y and x go out of scope and are destroyed here

在这个示例中,我们所做的只是将主函数内部的变量 a 和 b 的名称改为 x 和 y。尽管主函数和加法函数都包含名为 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。

关键要点:

函数体内声明的函数形参或变量名称仅在其声明的函数内部可见。这意味着函数内的局部变量命名时无需考虑其他函数中变量的名称,从而有助于保持函数的独立性。

关于局部作用域及其他作用域类型,我们将在后续章节中深入探讨。


局部变量应在何处定义

在现代C++中,最佳实践是函数体内的局部变量应尽可能靠近其首次使用的位置进行定义:

#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语言标准中解除。


何时使用函数形参与局部变量

由于函数形参和局部变量均可在函数体内使用,新手程序员常难以理解二者的适用场景。当调用方需传递初始化值作为形参时,应使用函数形参;否则应使用局部变量。

若在应使用局部变量时使用函数形参,代码将呈现如下形态:

#include <iostream>

int getValueFromUser(int val) // val is a function parameter
{
    std::cout << "Enter a value: ";
    std::cin >> val;
    return val;
}

int main()
{
    int x {};
    int num { getValueFromUser(x) }; // main must pass x as an argument

    std::cout << "You entered " << num << '\n';

    return 0;
}

在上例中,getValueFromUser() 将 val 定义为函数形参。因此 main() 必须定义 x 以便传递实参。然而 x 的实际值从未被使用,val 初始化的值也从未被使用。要求调用方定义并传递一个从未使用的变量,增加了不必要的复杂性。

正确的写法应如下所示:

#include <iostream>

int getValueFromUser()
{
    int val {}; // val is a local variable
    std::cout << "Enter a value: ";
    std::cin >> val;
    return val;
}

int main()
{
    int num { getValueFromUser() }; // main does not need to pass anything

    std::cout << "You entered " << num << '\n';

    return 0;
}

在此示例中,val 现已成为局部变量。main() 函数变得更简洁,因为它无需定义或传递变量即可调用 getValueFromUser()。

最佳实践
当函数内部需要变量时:

  • 若调用方会将变量的初始化值作为实参传递,则使用函数形参。
  • 否则使用局部变量。

临时对象介绍

临时对象temporary object(有时也称为匿名对象anonymous object)是一种无名的对象,用于存储仅需短暂使用的值。临时对象由编译器在需要时生成。

创建临时值的方法多种多样,以下是一种常见方式:

#include <iostream>

int getValueFromUser()
{
 	std::cout << "Enter an integer: ";
	int input{};
	std::cin >> input;

	return input; // return the value of input back to the caller
}

int main()
{
	std::cout << getValueFromUser() << '\n'; // where does the returned value get stored?

	return 0;
}

在上面的程序中,函数 getValueFromUser() 将存储在局部变量 input 中的值返回给调用方。由于 input 将在函数结束时被销毁,调用方接收的是该值的副本,因此即使 input 被销毁后,调用方仍可使用该值。

那么被复制回调用方的值存储在哪里?main()中并未定义任何变量。答案是:返回值存储在临时对象中,该对象随后被传递给std::cout进行输出。

关键要点
值返回机制将临时对象(其内存中包含返回值的副本)传递给调用方。

临时对象完全没有作用域(这很合理,因为作用域是标识符的属性,而临时对象没有标识符)。

临时对象在其创建的完整表达式结束时被销毁。这意味着临时对象总是在下一条语句执行之前被销毁。

在上例中,用于存储 getValueFromUser() 返回值的临时对象将在 std::cout << getValueFromUser() << ‘\n’ 执行后被销毁。

若临时对象用于初始化变量,则初始化操作将在临时对象销毁前完成。

在现代C++(特别是C++17之后)中,编译器会运用多种技巧避免生成原本必需的临时对象。例如,当使用返回值初始化变量时,通常会先创建临时对象存储返回值,再用该临时对象初始化变量。但在现代C++中,编译器通常会跳过临时对象的创建,直接用返回值初始化变量。

同理,在上例中由于getValueFromUser()的返回值被立即输出,编译器可跳过main()中临时对象的创建与销毁,直接用getValueFromUser()的返回值初始化operator<<的形参。


测验时间

问题 #1

下列程序会输出什么?

#include <iostream>

void doIt(int x)
{
    int y{ 4 };
    std::cout << "doIt: x = " << x << " y = " << y << '\n';

    x = 3;
    std::cout << "doIt: x = " << x << " y = " << y << '\n';
}

int main()
{
    int x{ 1 };
    int y{ 2 };

    std::cout << "main: x = " << x << " y = " << y << '\n';

    doIt(x);

    std::cout << "main: x = " << x << " y = " << y << '\n';

    return 0;
}

显示答案

image

该程序的执行流程如下:

  • 执行从 main 函数顶部开始
  • main 函数的变量 x 被创建并初始化为 1
  • main 函数的变量 y 被创建并初始化为 2
  • std::cout 输出 main: x = 1 y = 2
  • 调用 doIt 函数并传入实参 1
  • 创建 doIt 的形参 x 并初始化为 1
  • 创建 doIt 的变量 y 并初始化为 4
  • doIt 输出:x = 1 y = 4
  • 将新值 3 赋给 doIt 的变量 x
  • std::cout 输出 doIt: x = 3 y = 4
  • doIt的y和x被销毁
  • std::cout打印主函数:x = 1 y = 2
  • 主函数向操作系统返回0
  • 主函数的y和x被销毁

注意:尽管doIt的变量x和y的值被初始化或赋予了与主函数不同的值,但主函数的x和y并未受影响,因为它们是不同的变量。

posted @ 2026-02-09 11:31  游翔  阅读(0)  评论(0)    收藏  举报