Appearance
基础、存储期与内存
局部变量、全局变量与静态局部变量
应从 作用域(名字可见范围)、存储期(对象生存期) 和 链接属性(跨编译单元可见性) 三个维度理解。
| 类型 | 典型声明位置 | 作用域 | 存储期 | 常见存放区域 |
|---|---|---|---|---|
| 自动局部变量 | 函数/代码块内 | 当前块 | 进入块时创建,离开时销毁 | 通常在栈帧,优化后可能在寄存器 |
| 全局变量 | 命名空间作用域 | 声明后的相应作用域 | 静态存储期,程序开始/结束附近构造析构 | 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 的意义随上下文改变:
- 局部变量:延长到静态存储期,同时保持块作用域;
- 命名空间作用域变量/函数:内部链接,只在当前翻译单元可见(现代 C++ 常优先用匿名命名空间);
- 类静态数据成员:属于类而非某个对象,所有实例共享;
- 类静态成员函数:没有
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)。
递归应有可达的终止条件;深度过大或无限递归会耗尽线程栈并导致栈溢出。编译器有时会内联函数,或对满足条件的尾递归进行尾调用优化,减少额外栈帧;但不能依赖所有编译器和所有构建配置都会进行此优化。