Skip to content

基础、存储期与内存

局部变量、全局变量与静态局部变量

应从 作用域(名字可见范围)存储期(对象生存期)链接属性(跨编译单元可见性) 三个维度理解。

类型典型声明位置作用域存储期常见存放区域
自动局部变量函数/代码块内当前块进入块时创建,离开时销毁通常在栈帧,优化后可能在寄存器
全局变量命名空间作用域声明后的相应作用域静态存储期,程序开始/结束附近构造析构data/BSS 等静态区域
静态局部变量函数/代码块内加 static当前块静态存储期,只初始化一次静态区域
cpp
int global_count = 1;       // 全局对象:静态存储期

int next_id() {
    static int id = 0;      // 仅该函数可见,但整个程序期间存在
    return ++id;
}
  • 未显式初始化的静态存储期对象会先零初始化;自动局部基础类型若未初始化,其值通常是不确定的。
  • C++11 起,函数内静态变量的初始化由语言保证线程安全;但之后对该变量的并发读写仍需自行同步。
  • 非局部静态对象跨编译单元的初始化顺序可能有问题(static initialization order fiasco);按需构造的函数内静态对象常用于规避。

static 的常见含义

static 的意义随上下文改变:

  1. 局部变量:延长到静态存储期,同时保持块作用域;
  2. 命名空间作用域变量/函数:内部链接,只在当前翻译单元可见(现代 C++ 常优先用匿名命名空间);
  3. 类静态数据成员:属于类而非某个对象,所有实例共享;
  4. 类静态成员函数:没有 this 指针,只能直接访问静态成员,可通过 Class::func() 调用。

const 的作用

const 表示通过该名字/访问路径不可修改:

  • const int value = 42;:对象不可经 value 修改;
  • void print(const Widget& w);:承诺不经该引用修改对象;
  • int size() const;:成员函数不能修改对象的非 mutable 成员;
  • 配合指针限定“指向的数据”和“指针自身”是否可变,详见下一篇。

const 不等于绝对线程安全:不可变对象通常更易安全共享,但若对象包含 mutable 状态、指向可变外部数据或发生未同步发布,仍可能有并发问题。

常见程序内存区域

教材常将进程映像概括为代码段、只读数据段、已初始化数据段、BSS、堆和栈。它有助于理解,但标准 C++ 不规定具体地址布局;实际布局会受操作系统、ABI、ASLR、优化和分配器影响。

.bss 为什么能节省可执行文件体积

零初始化的全局/静态对象通常位于 .bss,例如:

cpp
int huge_buffer[10'000'000]; // 通常位于 .bss

链接器不必把一千万个 0 真正写入可执行文件,只需记录 .bss 所需的大小;程序加载时,操作系统再为它提供零初始化的内存页。因此 .bss 可以让运行时占用很大内存的零初始化数据不显著增大可执行文件。相对地,含有非零初始值的数据通常需要把初值写入文件,常位于 .data

核心原则是对象的存储期和所有权:自动对象由作用域销毁,动态对象由 delete 或 RAII 所有者销毁,静态对象由运行时在程序结束附近清理。

函数调用栈与递归

每次函数调用通常会创建一个栈帧(stack frame),保存该调用恢复执行所需的信息,例如返回地址、参数、自动局部变量以及部分被保存的寄存器状态。具体布局由 CPU 架构、ABI、编译器和优化选项决定,并非 C++ 标准固定规定。

以阶乘递归为例:

cpp
int factorial(int n) {
    if (n == 1) return 1;
    int sub = factorial(n - 1);
    return n * sub;
}

调用 factorial(3) 时,调用栈的变化可简化为:

text
压栈:main → factorial(3) → factorial(2) → factorial(1)
出栈:factorial(1) 返回 1
      factorial(2) 恢复,计算 2 * 1,返回 2
      factorial(3) 恢复,计算 3 * 2,返回 6
      main 获得结果

每一层递归都必须保留自己的 n 与“递归调用返回后还要继续执行什么”。例如 factorial(3) 必须等待 factorial(2) 的结果,才能完成 3 * sub;因此递归额外空间通常与最大递归深度成正比,即 O(depth)。

递归应有可达的终止条件;深度过大或无限递归会耗尽线程栈并导致栈溢出。编译器有时会内联函数,或对满足条件的尾递归进行尾调用优化,减少额外栈帧;但不能依赖所有编译器和所有构建配置都会进行此优化。

使用 Markdown 与 VitePress 构建