【C++ 面试真题】27. 聊聊 C++ 的内存泄漏与内存布局
【C 面试真题】聊聊 C 的内存泄漏与内存布局“线上进程内存一直涨怎么查”——这是工程里最真实的内存问题。查泄漏绕不开进程的内存布局数据在哪个区、泄漏发生在哪、工具看见的是什么。背得出new 了没 delete只是及格真考你的是一张完整的布局图、泄漏的判定标准、AddressSanitizer 和 valgrind 怎么用、free 之后内存去哪了。本文把泄漏排查和内存布局合起来讲透。一、开场先画进程的内存布局❓ 介绍一下 C 进程的内存布局✅ 从低地址到高地址一张图讲全高地址 ┌────────────────┐ │ 栈 stack │ ← 向下增长 │ ↓ │ │ 空洞 │ │ ↑ │ │ 堆 heap │ ← 向上增长 ├────────────────┤ │ 共享库/mmap 区 │ ├────────────────┤ │ BSS 段 │ 未初始化静态数据 ├────────────────┤ │ 数据段 data │ 已初始化静态数据 ├────────────────┤ │ 代码段 text │ 机器指令、字面量 └────────────────┘ 低地址回答思路按代码段 → 数据段 → BSS → 堆 → 栈从低到高报每段一句放什么最后补堆栈相向增长、中间是空洞。段存放谁管理text机器指令、字符串字面量只读系统data已初始化的全局/静态变量编译期BSS未初始化全局/静态清零加载时heapnew/malloc 的动态内存程序员stack局部变量、参数、返回地址编译器 经典追问点int* p new int;里p 在栈上它指向的 int 在堆上——变量在哪和资源在哪是两回事。泄漏就发生在堆这一区。二、堆与栈为什么泄漏只发生在堆❓ 为什么栈不会泄漏、堆会✅ 因为两者的回收机制完全不同维度栈堆回收作用域结束自动回收手动释放或智能指针顺序严格后进先出无碎片任意顺序有碎片分配移栈指针极快分配器找块慢大小有限Linux 默认 8MB大忘了释放不可能不存在这事泄漏栈的一切由编译器接管忘记回收在栈上根本不可能发生堆把释放责任交给了程序员——责任转移正是泄漏的温床。 new 的幕后三层new→operator new→malloc分配器分配器在用户态维护空闲块池池子不够才用brk/mmap向内核要内存。这层结构直接决定了后面free 之后内存去哪的答案。三、什么是内存泄漏怎么判定❓ 到底什么样的现象叫内存泄漏✅ 严格定义申请了堆内存、失去了所有访问它的途径、又没释放——这块内存在进程结束前永远无法回收。三个关键词对应三种形态// ① 丢了指针最直白的泄漏voidf(){int*pnewint[1000];if(err)return;// p 出作用域delete[]p;// 到不了}// ② 该释放没释放逻辑遗漏structC{C(){datanewchar[64];}~C(){/* 忘了 delete[] */}char*data;};// ③ 间接泄漏强引用清不掉// 循环引用的 shared_ptr上期详讲⚠️别把内存占用高直接等同泄漏分配器缓存空闲块RSS 不降是常态、真在用的缓存、内存池预分配——都不是泄漏。判定标准是**“不可达 未释放”**最终裁判是工具下一节不是眼瞪任务管理器。四、排查工具三层火力❓ 实际排查内存泄漏用什么工具✅ 由轻到重三层第一层编译期/静态——clang-tidy、编译器警告new后没有对应 delete、unique_ptr能替代的模式直接报出来。成本最低掐灭于摇篮。第二层运行期插桩——AddressSanitizer首选# 编译时加 sanitizers g -fsanitizeaddress -g main.cpp泄漏LeakSanitizer 默认附带在进程退出时报告每块泄漏内存的分配调用栈直接打出来精确到行。还有个隐藏好处ASan 顺带抓use-after-free、double free、越界——一次插桩全家桶。原理是把每块内存的可达性在退出时做图遍历从栈/全局出发标记可达堆块扫不着的分配即泄漏。第三层外挂式——valgrind memcheckvalgrind --leak-checkfull --show-leak-kindsall ./app不改编译、逐条指令翻译执行报告definitely lost / indirectly lost / still reachable分级。代价是程序慢 10~30 倍不适合压测场景好处是零侵入适合拿现成二进制查。实战路径CI 里挂 clang-tidy → 开发/测试构建开 ASan → 交付前或排查线上疑难点用 valgrind 复核。Windows 下 ASanMSVC 已支持之外还有 VLDVisual Leak Detector。五、泄漏的工程形态与预防❓ 真实项目里泄漏都藏在哪怎么防✅ 高发区四类 一条铁律异常路径正常分支 delete 了抛异常的分支漏了——try/catch 救不过来的复杂控制流最危险提前 return函数中段 return/continue 跳过清理接口契约不清谁分配谁释放没写明白双方都以为对方管C API 交接重灾区回调/异步注册了资源忘了注销异步任务持有缓冲任务丢了缓冲悬空或泄漏。铁律就是 RAII——把资源生命周期绑到对象// ❌ 四条路径都要记得 deletechar*bufnewchar[n];if(a)return;if(b)throw;delete[]buf;// ✅ 一条路径都不用管autobufmake_uniquechar[](n);if(a)return;// 自动释放if(b)throw;// 自动释放 预防优先级智能指针 容器 手写 RAII 包装 裸 new/delete。能不裸 new 就不裸 new——上一期讲的 malloc/new 底层机制是帮你理解原理不是鼓励使用。六、free 之后内存去哪了❓ delete/free 之后内存马上还给操作系统吗✅未必——分配器通常把块挂回空闲池等复用只有堆顶大块收缩brk或 mmap 的独立映射才真正还给内核。所以进程 RSS 不降不等于泄漏。void*pmalloc(120);// 1MBfree(p);// RSS 通常不变块进空闲池void*qmalloc(120);// 大概率复用// 同样的内存没有新向内核要 这个知识点连接三件事布局堆的边界由 brk/mmap 扩、判定工具看可达性不是 RSS、优化内存池本质就是把空闲池管理做成定制版。mallopt(M_TRIM_THRESHOLD) 这类参数还能调归还阈值面试当深水区加分项。七、面试高频追问❓ Q1栈溢出和内存泄漏是一回事吗✅ 不是。栈溢出是栈空间用完无界递归、超大局部数组表现是段错误——它是崩给你看泄漏是堆内存丢而未还进程继续跑但越吃越多——它是慢慢憋死。前者查递归和局部大对象改 vector 进堆后者查工具报告的分配栈。❓ Q2全局对象什么时候构造和泄漏有关吗✅ main 之前构造、main 返回后析构跨编译单元顺序不定。相关的是构造即分配、永不析构的单例泄漏new 出来故意不 delete 的 static 单例工具会报 still reachable——通常无害进程退出统一回收但报告里要有本事把它和真泄漏区分开。❓ Q3ASan 和 valgrind 怎么选✅ 开发自测/CI 用ASan——快约慢 2 倍、报告直接、顺带抓越界和 UAF拿不到源码、或不能改构建时用valgrind——零侵入但慢 10~30 倍。性能压测中两者都别开。❓ Q4valgrind 报告的 definitely lost 和 still reachable 什么区别✅ definitely lost没有任何指针指着的泄漏块——真泄漏必修still reachable退出时仍有指针常见 static 缓存、单例——进程结束由 OS 回收一般无害。间接丢失indirectly lost是只被丢失的块引用着的块。❓ Q5怎么写一个最简单的泄漏检测✅ 思路是重载全局 operator new/delete分配时记录指针 → 调用栈进注册表释放时移除atexit 时打印残留项——一个下午能写玩具版工业版就是 VLD/LeakSanitizer 的雏形。能讲出注册表 atexit 汇报结构这题就过了。❓ Q6内存碎片算泄漏吗✅ 不算。碎片是空闲但用不上小块散落凑不出大块内存还在分配器手里泄漏是根本回不来。碎片的表现是空闲很多却分配失败解法是内存池/slab 或换 jemalloc 这类分配器——和治理泄漏是两条路线。八、总结速查表考点一句话结论布局顺序text → data → BSS → 堆 → 栈泄漏定义不可达 未释放泄漏只发生在堆栈自动回收无忘放一说高发区异常路径、提前 return、契约不清ASan运行期插桩退出报告分配栈valgrind零侵入慢 10~30 倍报告分级definitely lost 才必修free 之后进空闲池RSS 不降 ≠ 泄漏预防铁律RAII / 智能指针碎片 ≠ 泄漏空闲但凑不出大块一句话回顾泄漏是**“堆上不可达又未释放”**布局里只有堆把回收交给了人排查三层火力——clang-tidy 静态 → ASan 插桩附分配栈→ valgrind 零侵入free 之后内存多半躺在分配器空闲池里RSS 不降不是泄漏而预防只有一条铁律——RAII能智能指针就别裸 new。如果您觉得本篇内容对你有帮助欢迎点赞 、收藏 ⭐、转发 。下期是内存管理篇收官——聊聊裸指针拯救散落的 free/delete 怎么改造、综合应用与真实问题排查怎么做敬请关注