43 / 80 · C++14 · 约 10 分钟
内存泄漏 Memory leaks:从责任到证据
泄漏首先是资源没有在约定时间被正确回收,不是简单观察进程内存有没有下降。用 RAII 覆盖异常路径,再结合 LeakSanitizer 的分配栈与重复工作负载区分失联分配、引用环、无界缓存以及分配器保留。
先定位生命周期契约
典型泄漏是动态分配后丢失唯一的释放路径,例如裸指针被覆盖或异常越过手工 delete。还有一种业务泄漏:对象仍可从全局容器访问,但缓存无限增长、过期任务永不移除。检测器对可达对象的判断不等于业务已经接受其永久存活。
shared_ptr 也可能通过环形成泄漏,因此不能只搜索裸 new。审查时沿拥有关系寻找谁应释放谁,检查回调捕获、订阅注册和全局缓存。真正的修复是明确责任与退出条件,而不是在程序结束前遍历所有地址盲目 delete。
用动态工具得到分配证据
在支持的平台上,可用 clang++ -std=c++14 -g -O1 -fsanitize=address -fno-omit-frame-pointer example.cpp -o example 编译链接,再运行可重复工作负载。ASan 在部分平台集成 LSan,必要时通过 ASAN_OPTIONS=detect_leaks=1 开启;也可单独使用 -fsanitize=leak。
阅读报告时保留首次分配调用栈,缩小到创建、转移和退出路径。动态检测只覆盖实际运行到的路径,无报告不是无泄漏证明。异常、取消和早退往往比成功路径更容易暴露缺失清理,必须纳入复现,而不是仅运行一次正常结束。
用可观察状态确认回收
示例在失败路径与成功路径分别创建 Worker,靠 RAII 销毁,再断言存活计数回到零。它不故意泄漏,不把错误运行当教学前提。计数能说明这种对象的构造析构平衡,但不是全进程泄漏检测,仍应与工具报告结合。
内存监控还受分配器缓存、容量保留和碎片影响。vector 清空后 size 归零但 capacity 可以保留;操作系统看到的驻留量也可能滞后。先证明对象是否仍被拥有,再分析底层存储为什么保留,避免为“降低曲线”引入反复分配和更差性能。
容易答错的地方
- 用进程退出后操作系统回收地址空间作为长期服务的清理策略,会掩盖运行期间无界增长。
- 忽略或屏蔽所有检测报告不能证明修复;应保存复现输入,并对同一路径确认报告消失。
运行一个例子
最低标准 C++14 · 完整程序 · 下载 .cpp
#include <cassert>
#include <memory>
#include <stdexcept>
struct Worker {
static int alive;
Worker() { ++alive; }
~Worker() { --alive; }
};
int Worker::alive = 0;
void run(bool fail) {
auto worker = std::make_unique<Worker>();
if (fail) throw std::runtime_error("cancelled");
}
int main() {
bool caught = false;
try { run(true); }
catch (const std::runtime_error&) { caught = true; }
assert(caught && Worker::alive == 0);
run(false);
assert(Worker::alive == 0);
}
在本地编译
g++ -std=c++14 -Wall -Wextra -Wpedantic -pthread memory-leaks.cpp -o example && ./example预期结果
预期:正常退出、无输出;所有 assert 通过。
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
服务每处理一批任务后 RSS 上升,但所有 Worker 都已析构。能直接判定没有泄漏吗?
查看参考答案
不能。Worker 计数只覆盖一种对象,其他分配、缓存或引用环仍可能增长。固定工作负载重复运行,结合 LSan 或堆分析的分配栈,检查全局容器尺寸和保留容量。若业务拥有量稳定而 RSS 在预热后稳定,才有证据进一步调查分配器缓存;不能仅凭析构计数或一次内存下降下结论。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。