7-11 静态局部变量
static 是 C++ 语言中最令人困惑的术语之一,很大程度上是因为 static 在不同上下文中具有不同含义。
在之前的课程中,我们提到全局变量具有 static 生命周期,这意味着它们在程序启动时创建,在程序结束时销毁。
我们还讨论过static关键字如何使全局标识符获得内部作用域,这意味着该标识符仅能在定义它的文件中使用。
本节课我们将探讨static关键字应用于局部变量时的用法。
静态局部变量
在第2.5课——局部作用域介绍中,你了解到局部变量默认具有自动存储期,这意味着它们在定义处创建,并在离开代码块时销毁。
在局部变量前添加static关键字,会将其从自动存储期转换为静态存储期。这意味着该变量将在程序开始时创建,并在程序结束时销毁(如同全局变量)。因此,静态变量即使超出作用域范围后仍会保留其值!
通过实例展示自动存储期与静态存储期局部变量的差异最为直观:
自动存储期(默认):
#include <iostream>
void incrementAndPrint()
{
int value{ 1 }; // automatic duration by default
++value;
std::cout << value << '\n';
} // value is destroyed here
int main()
{
incrementAndPrint();
incrementAndPrint();
incrementAndPrint();
return 0;
}
每次调用 incrementAndPrint() 时,都会创建一个名为 value 的变量并赋值为 1。incrementAndPrint() 将 value 递增为 2,然后输出 2 的值。当 incrementAndPrint() 执行完毕后,该变量超出作用域并被销毁。因此,该程序输出:

现在考虑这个程序的一个版本,它使用静态局部变量。此版本与上述程序的唯一区别在于,我们通过使用static关键字将局部变量的作用域从自动存储期改为静态存储期。
静态存储期(使用static关键字):
#include <iostream>
void incrementAndPrint()
{
static int s_value{ 1 }; // static duration via static keyword. This initializer is only executed once.
++s_value;
std::cout << s_value << '\n';
} // s_value is not destroyed here, but becomes inaccessible because it goes out of scope
int main()
{
incrementAndPrint();
incrementAndPrint();
incrementAndPrint();
return 0;
}
在此程序中,由于 s_value 被声明为 static,它将在程序开始时创建。
静态局部变量若被初始化为零或具有 constexpr 初始化器,则可在程序开始时初始化。
未初始化或具有非 constexpr 初始化的静态局部变量将在程序启动时初始化为零。具有非 constexpr 初始化的静态局部变量将在首次遇到变量定义时重新初始化。后续调用将跳过该定义,因此不会再次重新初始化。由于具有静态作用域,未显式初始化的静态局部变量默认将初始化为零。
由于 s_value 具有常量初始化器 1,s_value 将在程序启动时初始化。
当 s_value 在函数结束时超出作用域时,它不会被销毁。每次调用 incrementAndPrint() 函数时,s_value 的值都保持在我们上次留下的值。因此,该程序输出:

ID生成
静态作用域局部变量最常见的用途之一是生成唯一ID。设想一个包含大量相似对象的程序(例如被众多僵尸围攻的游戏场景,或需显示大量三角形的模拟程序)。若发现缺陷,几乎无法区分哪个对象存在问题。但若在创建时为每个对象分配唯一标识符,便能更轻松地识别对象以便后续调试。
使用静态作用域局部变量生成唯一ID号非常简单:
int generateID()
{
static int s_itemID{ 0 };
return s_itemID++; // makes copy of s_itemID, increments the real s_itemID, then returns the value in the copy
}
首次调用此函数时,它返回0。第二次调用时,它返回1。每次调用时,它返回的数值都比前一次调用时高1。您可以将这些数值作为对象的唯一标识符。由于s_itemID是局部变量,其他函数无法对其进行“篡改”。
静态变量兼具全局变量的部分优势(程序结束前不会被销毁),同时将其可见性限制在代码块范围内。这使得静态变量更易于理解且更安全。
关键要点
静态局部变量与局部变量具有相同的块作用域,但其生命周期如同全局变量般持续至程序结束。
静态局部常量
静态局部变量可以声明为 const(或 constexpr)。const 静态局部变量的一个典型应用场景是:当函数需要使用常量值,但创建或初始化对象的成本较高时(例如需要从数据库读取值)。若使用普通局部变量,每次函数执行时都会创建并初始化该变量。而采用 const/constexpr 静态局部变量时,只需创建并初始化高成本对象一次,后续每次调用函数时即可复用该对象。
核心要点
静态局部变量最适用于避免函数每次调用时都进行高成本的局部对象初始化。
不要使用静态局部变量来改变流程
请考虑以下代码:
#include <iostream>
int getInteger()
{
static bool s_isFirstCall{ true };
if (s_isFirstCall)
{
std::cout << "Enter an integer: ";
s_isFirstCall = false;
}
else
{
std::cout << "Enter another integer: ";
}
int i{};
std::cin >> i;
return i;
}
int main()
{
int a{ getInteger() };
int b{ getInteger() };
std::cout << a << " + " << b << " = " << (a + b) << '\n';
return 0;
}
示例输出

这段代码实现了预期功能,但由于使用了静态局部变量,导致代码可读性下降。若有人仅阅读main()中的代码而未查看getInteger()的实现,便无法推断两次调用getInteger()的操作存在差异。然而这两次调用确实不同——若差异不仅限于提示语变更,则极易造成混淆。
假设你按下微波炉的+1按钮,它将剩余时间延长1分钟。食物变热了,你很开心。但在取出食物前,你瞥见窗外有只猫,便驻足观望片刻——毕竟猫咪很酷。这片刻比预期更长,当你咬下第一口食物时,它已重新变冷。没关系,只需重新放入微波炉按+1再加热一分钟。但这次微波炉只增加了1秒而非1分钟。这时你会惊呼“我什么都没改它却坏了”或“上次明明正常”。若重复相同操作,你自然期待获得与上次相同的反馈。函数的运作原理亦是如此。
假设我们想在计算器中添加减法功能,使其输出效果如下:
Addition
Enter an integer: 5
Enter another integer: 9
5 + 9 = 14
Subtraction
Enter an integer: 12
Enter another integer: 3
12 - 3 = 9
我们可以尝试使用 getInteger() 函数读取接下来的两个整数,就像我们在求和时所做的那样。
int main()
{
std::cout << "Addition\n";
int a{ getInteger() };
int b{ getInteger() };
std::cout << a << " + " << b << " = " << (a + b) << '\n';
std::cout << "Subtraction\n";
int c{ getInteger() };
int d{ getInteger() };
std::cout << c << " - " << d << " = " << (c - d) << '\n';
return 0;
}
但这不符合我们的要求,因为输出结果是:

(倒数第三行应为“输入另一个整数”而非“输入一个整数”)
getInteger() 函数不可复用,因为它包含无法从外部重置的内部状态(静态局部变量 s_isFirstCall)。s_isFirstCall 不应是整个程序中唯一的变量。尽管初始编写时程序运行良好,但静态局部变量阻碍了后续函数复用。
改进 getInteger 实现的更佳方案是将 s_isFirstCall 作为参数传递,这允许调用方选择打印何种提示:
#include <iostream>
// We'll define a symbolic constant with a nice name
constexpr bool g_firstCall { true };
int getInteger(bool bFirstCall)
{
if (bFirstCall)
{
std::cout << "Enter an integer: ";
}
else
{
std::cout << "Enter another integer: ";
}
int i{};
std::cin >> i;
return i;
}
int main()
{
int a{ getInteger(g_firstCall) }; // so that it's clearer what the argument represents here
int b{ getInteger(!g_firstCall) };
std::cout << a << " + " << b << " = " << (a + b) << '\n';
return 0;
}

非 const 静态局部变量仅应在满足以下条件时使用:在整个程序及可预见的未来中,该变量具有唯一性,且重置该变量毫无意义。
最佳实践
const 静态局部变量通常可安全使用。非 const 静态局部变量通常应避免使用。若必须使用,请确保该变量永不需重置,且不用于改变程序流程。
技巧
更具复用性的解决方案是将bool参数改为std::string_view,让调用方传入实际使用的文本提示!
对于进阶读者
当需要多个能记住值的非const变量实例(例如实现多个ID生成器)时,函数对象是理想方案(参见第21.10课——重载括号运算符)。
测验时间
问题 #1
使用关键字 static 对全局变量有何影响?对局部变量有何影响?
显示解答
当应用于全局变量时,static 关键字将该全局变量定义为具有内部链接,这意味着该变量无法导出到其他文件。
当应用于局部变量时,static 关键字将该局部变量定义为具有静态存储期,这意味着该变量仅会被创建一次,且在程序结束前不会被销毁。

浙公网安备 33010602011771号