05 / 80 · C++11 · 约 8 分钟
static:存储期、链接与类成员三种语境
static 的意义取决于出现位置:局部变量获得静态存储期,命名空间实体可以获得内部链接,类的静态成员不属于某个实例。局部静态初始化的线程安全,不代表后续读写也自动线程安全。
局部名字,长期存储
函数里的 static 局部变量仍然只能在其作用域内直接按名字访问,但存储具有静态存储期,不会在函数返回时自动消失。对于需要动态初始化的局部静态变量,第一次执行到声明时初始化;后续调用复用同一个对象。
从 C++11 起,多个线程同时首次到达该声明时,初始化受到语言保证的同步保护。如果初始化抛出异常,之后进入还会重试;若初始化过程中递归再次进入同一声明,则是未定义行为。不要把初始化保证扩展为对对象全部操作的自动加锁。
命名空间中的内部链接
命名空间作用域的普通非线程局部变量本来就有静态存储期,此处 static 的重要额外作用是使名字具有内部链接。同名定义可以在不同翻译单元分别存在,彼此不是同一个实体;函数也可用 static 限制到当前翻译单元。
匿名命名空间是组织一组文件内部实体的常用方式。若把 static 可变变量定义在头文件,通常每个包含它的翻译单元都会拥有独立状态。这与全程序共享一个计数器完全不同,不能仅凭同一变量名推断同一地址。
类共享成员与实例成员
静态数据成员不嵌在每个对象里,而是由该类的所有实例共享;静态成员函数没有 this,不能直接访问某个未指定对象的普通成员。可以用 Type::member 明确表达访问的并不是某个实例私有状态。
传统非 inline 静态数据成员通常需要类外定义;C++17 的 inline static 数据成员能在类内完成定义。示例只演示局部静态状态跨调用保留,并按顺序存下两次结果,避免用输出表达式里的求值顺序混淆核心概念。
容易答错的地方
- 局部 static 计数器的 ++ 在并发调用中仍可能发生数据竞争;初始化同步不保护后续递增。
- 不同翻译单元的动态初始化依赖容易形成顺序问题,static 关键字本身不会解决全局初始化次序。
运行一个例子
最低标准 C++11 · 完整程序 · 下载 .cpp
#include <cassert>
#include <iostream>
int next_id() {
static int id = 0;
return ++id;
}
int main() {
const int first = next_id();
const int second = next_id();
assert(first == 1);
assert(second == 2);
std::cout << first << ' ' << second << '\n';
}
在本地编译
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread basics-static.cpp -o example && ./example预期结果
1 2
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
头文件里放 static int count = 0;,a.cpp 与 b.cpp 都包含它。a.cpp 递增后,b.cpp 读到的 count 一定增加吗?如何表达共享状态?
查看参考答案
不会,两份定义具有内部链接,分别属于各自翻译单元。可在头文件写 extern int count;,并仅在一个源文件写 int count = 0;。C++17 也可以在头文件使用 inline int count = 0;。两种共享写法都仍需另外解决并发访问同步。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。