# 【C++ 面试真题】28. 聊聊 C++ 的裸指针改造

【C++ 面试真题】聊聊 C++ 的裸指针改造

接手一个 C++ 老项目,几千行代码里散落着裸 new/delete、malloc/free:谁拥有谁说不清、哪条路径漏了释放没人知道------改一行崩三处。这是工程里最真实的内存战役,也是内存管理篇的收官题。背得出"换智能指针"只是及格,真考你的是"改造的先后路线、和 C API 怎么交接、纯观察的裸指针该不该动、真实问题怎么一步步排查"。本文按"改造 + 排查 + 综合应用"讲透。


一、开场:遗留代码的病灶

❓ 一个满地裸指针的项目,问题通常有哪几类?

✅ 先诊断、再动刀。四类典型病灶:

病灶 典型代码 危害
所有权不明 两个函数都 delete 同一指针 double free
路径遗漏 异常/提前 return 漏 delete 泄漏
配对混用 malloc 配 delete、new\[\] 配 delete UB(Undefined Behavior),崩溃诡异
接口契约靠猜 返回的指针"谁释放?"没人知道 泄漏或 UAF(Use-After-Free)

回答思路:先说"诊断重于动手"------搞清每块内存的 owner 是谁,再分类改造;上来无脑替换智能指针,多线程/共享场景反而会引入循环引用新坑。


二、改造路线:四步走,风险递增

❓ 改造顺序怎么定?

✅ 按风险从低到高四步走,每步可独立验证:

第一步:局部 new/delete → unique_ptr(最安全,立刻消灭泄漏):

cpp 复制代码
// 改造前
void f() {
    char* buf = new char[1024];
    if (!ok) { delete[] buf; return; }
    process(buf);
    delete[] buf;
}
// 改造后:三条路径的 delete 全消失
void f() {
    auto buf =
        make_unique<char[]>(1024);
    if (!ok) return;
    process(buf.get());
}

第二步:工厂/接口返回值 → 返回 unique_ptr(把所有权写进类型):

cpp 复制代码
// 旧:裸指针返回,调用方猜要不要删
Conn* createConn();
// 新:签名就说清"所有权交给你"
unique_ptr<Conn> createConn();

第三步:所有权确实共享的 → shared_ptr(慎用,先确认"真共享"):

cpp 复制代码
// 多个组件都要持有、生命周期不定
shared_ptr<Cache> cache_;
// 谨记:能 unique 不 shared,
// 互相引用处配 weak_ptr

第四步:C API 交接 → 智能指针 + 自定义删除器(下节专讲)。

🎯 顺序即策略:先 1 后 2 再 3------大部分代码走到第 1、2 步就治好了;第 3 步是"确认共享"而不是"图省事"。shared_ptr 滥用是改造中新生的最大坏味道。


三、和 C API 交接:删除器的妙用

❓ 和 C 接口(malloc 的、fopen 的、第三方库的)怎么交接?

用自定义删除器把"怎么释放"打包进智能指针------资源的"关闭姿势"一次性说清:

cpp 复制代码
// FILE* 的正确归宿:fclose
auto file = unique_ptr<FILE,
    decltype(&fclose)>(
        fopen("a.txt", "r"),
        fclose);
// 离开作用域自动 fclose

// malloc 的内存:free 来收
auto buf = unique_ptr<void,
    decltype(&free)>(
        malloc(4096), free);

// 第三方库:库自己的释放函数
auto h = unique_ptr<Handle,
    LibDeleter>(
        LibCreate(),
        LibDeleter{});

💡 这招的精髓:资源管理权和资源语义解耦------unique_ptr 负责"必然释放",删除器负责"按规矩释放"。无论多古董的 C 接口,都能纳入 RAII 体系。
⚠️ 两个细节:unique_ptr<void, D> 合法(void 也行,只要删除器懂它);fclose 指针在 MSVC 上因 [[nodiscard]] 标注可能需要包一层 lambda------动手时留意编译器提示。


四、不该动的:观察指针与 C 兼容层

❓ 所有裸指针都要消灭吗?

✅ 不。两种裸指针是合法且更好的存在

① 纯观察指针------只是"看看",不参与生命周期:

cpp 复制代码
// 参数只是借用:传裸指针/引用
void print(const string* s);
// 不需要 shared_ptr 接收------
// 所有权语义和开销都是误导

② 与 C 交互的边界 ------get() 把管辖区内的资源临时递给老接口:

cpp 复制代码
auto buf = make_unique<char[]>(n);
legacy_fill(buf.get(), n);
// C 函数填完就完,指针还在
// unique_ptr 手里,安全

🎯 判断标准一句话:"谁负责释放"如果是这个指针,必须智能指针化;如果只是"指过去看看",裸指针/引用就是正确工具。消灭的是"所有权裸指针",不是裸指针本身。


五、综合应用:一次完整的问题排查

❓ 线上服务内存持续上涨,怎么一步步排查?

✅ 按流程走一遍(面试讲出这个流程就是高级感):

① 先定性:泄漏还是正常波动?

text 复制代码
监控:RSS 曲线只涨不跌?
  压测后回落 → 缓存/空闲池,非泄漏
  单调上涨   → 疑似泄漏,进入 ②

② 复现与插桩 :测试环境同负载跑,开 -fsanitize=address------退出时 LeakSanitizer 直接给出每块泄漏的分配调用栈

③ 读栈定位病灶

text 复制代码
Direct leak of 1024 byte(s)
  #1 Conn::Conn() conn.cpp:42
  #2 Pool::create() pool.cpp:77

顺着栈看代码:pool.cpp:77 里 new 出的 Conn 注册进了列表,析构却没人清------病灶是"注册了没注销",不是简单的"忘了 delete"。

④ 按改造路线修 :注册列表本来就用裸指针存(这是合法观察),但创建处 该返回 unique_ptr,进列表时 shared_ptr/weak_ptr 按真实所有权定。

⑤ 回归验证:ASan 跑干净 + 监控曲线平稳,结案。

💡 注意 ③ 的教训:工具报告的是"分配点",病灶常在"所有权设计"------修一行 delete 是止痛,理顺所有权才是治病。


六、改造纪律:别引入新问题

❓ 改造最容易引入哪些新坑?

✅ 四个高频新坑,动手前心里有数:

  • shared_ptr 循环引用------图省事全换 shared,父子互指成环,泄漏换了马甲回来。互指处必须 weak_ptr;
  • 所有权双轨 ------同一资源一会儿裸指针一会儿智能指针,边界处 double free。入口唯一:资源诞生地立即进智能指针,此后只传引用/get();
  • 生命周期延长过度------本来作用域即释放的资源被 shared_ptr 拖到全局,内存峰值上升;
  • 线程边界的悬空------回调里持有的裸指针,对象早被析构。异步持有一律 shared_ptr/weak_ptr + lock。

🎯 一条总纪律:每块内存自诞生起,owner 唯一且写在类型上。改造完成的标志不是"没有裸指针",而是"每行代码都能回答:这块内存现在归谁"。


七、面试高频追问

❓ Q1:接手百万行裸指针代码,从哪里开始?

✅ 不是大面积替换,三步:先挂 clang-tidy 拿全量报告做"泄漏地图";再挑热点路径 (崩溃/泄漏集中的模块)按四步路线改造、ASan 验证;同时立规矩------新代码必须 RAII,老代码"touch 到就改"。技术债靠增量消化,一次性重写是最大风险。

❓ Q2:unique_ptr.get() 交出去的指针,对方存起来用了怎么办?

✅ 这就是引入 UAF 的典型操作。纪律是:get() 只用于同步调用、当场用完 的场景;对方若需要"持有",要么传 shared_ptr(共享),要么传 weak_ptr(观测 + lock),不能存裸指针过夜。

❓ Q3:资源不是内存(文件/socket/锁)也能这么拯救吗?

✅ 能,这正是 RAII 的本意------unique_ptr<FILE, decltype(&fclose)> 管 FILE,lock_guard 管锁,自定义删除器管任意"打开/关闭"型资源。RAII 拯救的不只是内存,是一切成对出现的资源操作

❓ Q4:为什么说"修一行 delete 是止痛"?

✅ 工具给的分配栈只是症状发生地,病因是所有权设计(谁注册、谁注销、谁共享)。头痛医头的补丁会漏掉同类路径------同构的 bug 在下一个函数里等着。改造必须落到"owner 唯一"的结构上。

❓ Q5:改造后如何证明没变慢?

✅ 智能指针的额外开销可量化:unique_ptr 零开销;shared_ptr 每次 copy/reset 是原子计数(纳秒级)。基准测试对比改造前后热点路径;确认无循环引用、无过度生命周期延长,性能差距通常在噪声内------结构正确的代码才有资格谈性能

❓ Q6:C++ 项目里混着 malloc 的部分(第三方库)怎么管?

✅ 边界收口:库的 API 用删除器包装 (第三节的 unique_ptr + free/closer),包装层以内是 RAII 世界、以外是库的地盘------门面上永远只出现智能指针和引用。malloc 家族本身不消灭,只是被圈养在边界上。


八、总结速查表

考点 一句话结论
第一步 局部 new/delete → unique_ptr
第二步 工厂返回 unique_ptr,所有权入签名
第三步 真共享才 shared,互指配 weak
C API 交接 自定义删除器打包释放语义
合法裸指针 纯观察、get() 当场用
排查流程 定性 → ASan → 读栈 → 修所有权 → 回归
新坑 循环引用、双轨所有权、悬空回调
总纪律 每块内存 owner 唯一且写在类型上

一句话回顾

拯救散落的 free/delete:先诊断所有权、按"unique → 接口 → shared → C 交接"四步走 ;C API 用删除器圈进 RAII 边界;纯观察的裸指针不动;排查靠"定性 → ASan → 读栈 → 修所有权",而完成的标志是------每块内存都能回答"现在归谁"。内存管理篇到此收官,下期进入多线程篇,敬请关注 👋

相关推荐
小小龙学IT2 小时前
FlatBuffers 深度解析:Google 开源零拷贝序列化库完全指南
c++·开源
xier_ran2 小时前
【infra之路】GPU 存储层次总结:L1 / L2 / HBM
java·网络·数据库
wuminyu3 小时前
JDK21中虚拟线程和FFM协同实现高并发源码剖析
java·linux·c语言·jvm·c++
SWAGGY..3 小时前
【C++进阶】:(1)继承机制详解
java·jvm·c++
官乐3 小时前
AI面试指南(多agent开发流程)
人工智能·面试·职场和发展
听取WA声一片(无恶意)3 小时前
CSP-J/CSP-S 深度优先搜索(DFS)完全讲义
c++·算法·深度优先
码匠许师傅3 小时前
【C++ 面试真题】27. 聊聊 C++ 的内存泄漏与内存布局
java·c++·面试
跟我一起学测试呀4 小时前
软件测试面试总结,备战金九银十
面试·职场和发展
PPPPickup4 小时前
金证秋招笔试
java·开发语言
不正经学生4 小时前
C语言结构体:自定义数据类型,让变量打包出行
java·c语言·开发语言·数据结构·算法