C++ / a working model

05 / 80   ·   C++11   ·   约 8 分钟

static:存储期、链接与类成员三种语境

先记住这句话

static 的意义取决于出现位置:局部变量获得静态存储期,命名空间实体可以获得内部链接,类的静态成员不属于某个实例。局部静态初始化的线程安全,不代表后续读写也自动线程安全。

本篇内容
  1. 局部名字,长期存储
  2. 命名空间中的内部链接
  3. 类共享成员与实例成员
  4. 运行示例
  5. 动手练习

局部名字,长期存储

函数里的 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;。两种共享写法都仍需另外解决并发访问同步。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录