CPP知识记录
2026-08-16
这是一份持续更新的 C++ 基础语义笔记。对象、存储和生命周期看起来接近,却分别描述“什么存在”“放在哪里”和“存在多久”。先把这些概念分清,许多悬空引用、资源泄漏和未定义行为问题都会更容易定位。
对象、变量、存储与生命周期
- 对象(object)
- 某种类型在一段存储区域中的实例。对象不仅包含一组原始字节,还受到类型、对齐、生命周期和访问规则的约束。
- 变量(variable)
- 由声明引入的对象或引用。变量通常有名字,但并非所有对象都是变量,例如表达式产生的临时对象。
- 存储(storage)
- 承载对象的原始空间。只有一块可用内存,并不代表其中已经存在一个可以按某种类型访问的对象。
- 生命周期(lifetime)
- 对象或引用从开始存在到结束存在的时间区间。对象生命周期结束后,即使原来的存储仍在,通常也不能再通过旧指针或旧引用访问该对象。
关键区别:存储期描述底层存储保持有效的时间;生命周期描述具体对象在该存储中存在的时间。两者相关,但不能画等号。
四种存储期
| 存储期 | 典型对象 | 开始与结束 | 重点注意 |
|---|---|---|---|
| 自动存储期 | 普通局部变量、函数参数 | 执行到声明后构造,离开所在作用域时销毁 | 最容易产生返回局部指针或引用的问题 |
| 动态存储期 | 通常由 new 创建的对象 |
由程序显式创建,并在所有者释放时结束 | 优先交给智能指针、容器或其他 RAII 类型管理 |
| 静态存储期 | 全局变量、命名空间变量、static 变量 |
存储贯穿程序运行期;局部静态对象通常在首次执行到声明时初始化 | 不同源文件之间的动态初始化顺序可能不明确 |
| 线程存储期 | thread_local 对象 |
每个线程拥有独立实例,通常在线程结束时销毁 | 线程池中的工作线程可能长期不退出 |
一个典型的悬空引用
const int& bad_reference() {
int value = 42;
return value; // value 离开作用域后被销毁
}
int main() {
const int& answer = bad_reference();
// 使用 answer:未定义行为
}
函数返回时,value 的生命周期已经结束。引用本身还能被复制和保存,但它不再指向一个仍然存在的 int 对象。
构造、析构与类不变量
构造函数的职责是让对象进入可用状态;析构函数的职责是结束对象持有资源的生命周期。资源不只包括堆内存,也可能是文件、锁、套接字、线程或数据库连接。
类不变量是对象在对外可观察的稳定状态中必须满足的条件。构造函数应建立不变量,公开成员函数退出前应恢复不变量,析构开始后则不应再把对象当作完整可用的实例。
构造函数抛出异常时
如果正在构造的对象抛出异常,它自身的析构函数不会执行,因为完整对象尚未构造成功。但是,已经构造完成的成员对象和基类子对象会按构造顺序的逆序自动析构。
- 先销毁已经构造完成的成员。
- 再销毁已经构造完成的基类子对象。
- 最外层对象的析构函数不会被调用。
因此,成员本身应采用 RAII 管理资源。不要等到最外层析构函数才尝试收拾构造过程中的半成品。
未定义行为(Undefined Behavior)
未定义行为表示 C++ 标准不再规定程序必须产生什么结果。程序可能崩溃、看似正常、输出随机结果,或者只在开启优化后出现完全不同的表现。
重点排错位置
- 越界访问:访问数组或容器有效范围之外的位置。
- 无效指针:解引用空指针、悬空指针或已经释放对象的地址。
- 未初始化值:读取不确定或错误的标量值;具体分类会随类型和标准版本变化,但都不应依赖其结果。
- 有符号整数溢出:计算结果超出有符号类型可表示范围。
- 数据竞争:多个线程没有同步地访问同一内存位置,并且至少一个操作是写入。
- 错误的类型访问:通过不具备类型可访问关系的左值、指针或引用读取对象存储。
错误判断:“Debug 模式能运行”不代表程序正确。优化器会假设未定义行为不会发生,并据此重排、合并甚至删除代码,所以问题经常只在 Release 模式暴露。
使用 Sanitizer 检查
| 工具 | 主要用途 | 常见目标 |
|---|---|---|
| AddressSanitizer(ASan) | 检查内存访问错误 | 堆、栈或全局缓冲区越界,use-after-free,double-free |
| UndefinedBehaviorSanitizer(UBSan) | 检查一部分可在运行时捕获的 UB | 有符号溢出、错误移位、空指针或未对齐访问等 |
| ThreadSanitizer(TSan) | 检查线程间的数据竞争 | 缺少互斥锁、原子操作或 happens-before 关系 |
Clang 编译示例
# 内存错误与部分未定义行为
clang++ -std=c++20 -O1 -g \
-fsanitize=address,undefined \
-fno-omit-frame-pointer main.cpp -o app-asan
# 数据竞争:单独构建一个 TSan 版本
clang++ -std=c++20 -O1 -g \
-fsanitize=thread main.cpp -o app-tsan
Sanitizer 是动态检查工具,只能检查实际执行到的路径;它不能替代测试用例、代码审查和静态分析。ASan 与 TSan 通常应分别构建,不要放进同一个测试二进制文件。
排错清单
- 先确认对象是否已经完成构造、是否仍在生命周期内。
- 再确认指针、引用和迭代器有没有因销毁、移动或容器扩容而失效。
- 检查索引、长度和整数运算是否可能越界或溢出。
- 多线程代码明确标出共享状态和同步关系。
- 分别运行 ASan + UBSan 和 TSan 构建,并保留可读的调试符号。