C++ / a working model

34 / 80   ·   C++14   ·   约 9 分钟

存储期 Storage duration 不等于内存分区

先记住这句话

C++ 规定自动、静态、线程和动态四种存储期,不要求固定的栈、堆或可执行文件分段。变量名的作用域、名字的链接属性、对象的生存期和操作系统映射分别回答不同问题,不能混成一张地址图。

本篇内容
  1. 语言层先问谁管理存储
  2. 作用域与生存期分开判断
  3. 操作系统分区只是实现视角
  4. 运行示例
  5. 动手练习

语言层先问谁管理存储

普通块内变量通常具有自动存储期,离开相应作用域时进行清理;静态存储期对象的存储贯穿程序执行;thread_local 对象拥有线程存储期,每个线程有自己的实例;动态存储期对象的管理则不受创建它的局部作用域直接约束。

这些类别描述语义而非地址高低。局部变量可能在寄存器中,也可能被优化掉;动态分配可能来自内存池。只有程序可观察行为符合标准即可。回答“局部变量一定在栈上”过度承诺了实现方式,尤其不适合推导对象的合法访问期限。

作用域与生存期分开判断

局部 static 的名字只在块内可见,但对象不会在每次函数返回时销毁。它首次经过声明时完成需要的动态初始化,成功后后续调用复用同一对象。C++11 保证这个初始化的并发安全,不保证之后对该对象的任意读写自动同步。

示例中局部 unique_ptr 自身具有自动存储期,它管理的整数具有动态存储期。移动指针把管理责任交给外层对象,内层变量离开作用域不会销毁该整数。这说明“指针放在哪里”和“它指向的对象活多久”是两个独立问题。

操作系统分区只是实现视角

常见程序映像会区分代码、只读数据、已初始化数据和零初始化区域,运行时还可见线程栈与动态映射。这些名称对调试、链接器和性能分析有用,但标准没有要求每种 C++ 声明必须落到某个固定段,也没有规定地址排列顺序。

const 主要限制语言层的修改操作,并非“放进只读段”的请求。一个 const 局部整数仍可具有自动存储期。排查问题时先用声明与所有权确定语义,再用平台文档解释实际映射;不要从某次打印的地址反推一条普遍规则。

容易答错的地方

  • 局部 static 的线程安全初始化不覆盖后续 ++counter;并发更新仍需要同步。
  • 作用域结束只销毁局部裸指针对象,不会替它自动 delete 所指的动态对象。

运行一个例子

最低标准 C++14 · 完整程序 · 下载 .cpp

#include <cassert>
#include <memory>
#include <utility>

int next_id() {
    static int value = 0;
    return ++value;
}

int main() {
    std::unique_ptr<int> owner;
    {
        auto local = std::make_unique<int>(42);
        owner = std::move(local);
        assert(!local);
    }
    assert(*owner == 42);
    assert(next_id() == 1);
    assert(next_id() == 2);
}

在本地编译

g++ -std=c++14 -Wall -Wextra -Wpedantic -pthread memory-storage.cpp -o example && ./example

预期结果

预期:正常退出、无输出;所有 assert 通过。

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

函数内 static std::vector<int> cache 的名字、vector 对象、元素存储分别具有什么特征?

查看参考答案

cache 的名字是块作用域;vector 对象具有静态存储期,其成功构造后的对象状态跨调用保留。元素使用 vector 管理的动态存储,可能随扩容更换,因此不能把元素指针的有效期等同于 cache 的存储期。清空会结束元素生存期,vector 对象本身仍然存在。

继续查证

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

回到目录