30. 总结与最佳实践:何时用哪种智能指针、性能敏感场景的取舍、代码审查清单
好,终于到了最后一章。说实话,写到这里我反而有点不舍。不过该总结的还是要总结,该落地的还是要落地。这一章我们不聊新概念,就聊三件事:什么时候该用哪种智能指针、性能敏感场景下怎么取舍、以及代码审查时该盯哪些点。
嗯,说白了,就是把你前面学的东西,变成你手头项目里能用的规则。
一、智能指针选型:一张表说清楚
我个人习惯,在项目里定一个简单的选型规则。你想想看,如果团队里每个人都在纠结「这里该用 shared_ptr 还是 unique_ptr」,那代码审查就没法过了。所以,我一般用下面这张表来统一口径:
| 场景 | 推荐指针 | 理由 |
|---|---|---|
| 对象有唯一所有者,生命周期明确 | std::unique_ptr |
零开销抽象,所有权清晰 |
| 对象需要被多个所有者共享 | std::shared_ptr |
引用计数管理生命周期 |
| 只观察对象,不参与生命周期管理 | std::weak_ptr |
避免循环引用,安全访问 |
| 性能敏感、高频分配/释放 | std::unique_ptr 或裸指针 |
shared_ptr 的引用计数有原子操作开销 |
| 需要自定义删除器(如文件句柄、socket) | std::unique_ptr |
删除器是类型的一部分,零额外开销 |
| 跨 DLL/模块边界传递对象 | std::shared_ptr |
引用计数在堆上,跨模块安全 |
我在项目中遇到过好几次,有人为了「省事」把所有地方都写成 shared_ptr。结果呢?性能分析一跑,发现引用计数的原子操作占了 5% 的 CPU 时间。说白了,不要为不需要共享的东西买单。
二、性能敏感场景的取舍
性能敏感场景,说白了就是那些每秒调用几万次、几十万次的热路径。在这种地方,智能指针的选择直接决定了你的延迟和吞吐量。
2.1 避免 shared_ptr 的拷贝
shared_ptr 的拷贝不是简单的指针赋值,它背后有原子递增操作。在高并发场景下,这个操作会引发缓存行竞争。我建议:
- 函数参数传
const shared_ptr<T>&,不要传值 - 如果只是临时用一下,考虑用
shared_ptr的get()拿到裸指针 - 实在不行,用
weak_ptr::lock()临时提升,用完即弃
2.2 unique_ptr 是性能首选
你想想看,unique_ptr 在默认删除器下,和裸指针的大小一样,操作也一样。它唯一的额外开销就是析构时调用 delete——而这个开销你用手动管理也逃不掉。
所以,我的经验是:能用 unique_ptr 的地方,绝不用 shared_ptr。这不是教条,这是性能。
2.3 自定义内存池 + unique_ptr
我曾经在一个实时音频处理项目里,需要频繁创建和销毁小对象。直接用 unique_ptr 配合 new/delete,性能还是不够。后来我写了一个固定大小的内存池,然后让 unique_ptr 使用自定义删除器,把内存归还到池子里。嗯,效果立竿见影,延迟降了 40%。
// 自定义删除器示例
struct PoolDeleter {
void operator()(MyObject* ptr) {
// 归还到内存池,而不是 delete
MemoryPool::instance().deallocate(ptr);
}
};
using PoolPtr = std::unique_ptr<MyObject, PoolDeleter>;
2.4 慎用 make_shared
等等,你可能要问:「不是都说 make_shared 比 shared_ptr(new T) 好吗?」嗯,大部分情况下是的。但在性能敏感场景,make_shared 有一个坑:它把控制块和对象分配在同一块内存里。如果对象很大,而 weak_ptr 还活着,这块内存就不能释放。说白了,就是内存不能及时归还。
所以,如果对象很大,且 weak_ptr 生命周期很长,我建议分开分配:
// 大对象 + 长生命周期 weak_ptr
std::shared_ptr<BigObject> sp(new BigObject);
// 控制块和对象分开,对象可以提前释放
三、代码审查清单
好,最后这部分,是我自己多年代码审查经验的总结。我把它做成一个清单,你审查代码时,一条一条过就行。
✅ 智能指针代码审查清单
- 所有权是否明确?——每个对象应该只有一个「主人」。如果到处都是
shared_ptr,说明设计有问题。 - 有没有循环引用?——检查 parent-child 关系。父持有子的
shared_ptr,子持有父的weak_ptr。 - 参数传递方式对吗?——只读观察用
const T&或T*;需要延长生命周期用shared_ptr传值;唯一所有权用unique_ptr传右值。 - 有没有不必要的 shared_ptr 拷贝?——函数参数是不是传了值?循环里是不是反复拷贝了?
- 自定义删除器用对了吗?——文件句柄、socket、内存池资源,有没有忘记自定义删除器?
- make_shared 和 shared_ptr(new) 选对了吗?——大对象 + 长 weak_ptr 生命周期,避免用 make_shared。
- 有没有裸指针裸 delete?——新代码里不应该出现
delete关键字,除非在自定义删除器里。 - 跨模块边界传递时,用的是 shared_ptr 吗?——unique_ptr 跨 DLL 传递会出问题,因为删除器在不同模块里。
💡 我的小技巧:审查代码时,我习惯先看头文件里的成员变量。如果看到 shared_ptr<T> 成员,我会问:「这个对象真的需要共享吗?」很多时候,改成 unique_ptr 就能让设计更清晰。
⚠️ 我曾经踩过的坑:有一次,我在一个网络库里用了 shared_ptr 管理连接对象。结果因为回调里捕获了 shared_ptr,导致连接断开后对象迟迟不释放。最后用 weak_ptr 配合 lock() 才解决。嗯,从那以后,我对「回调中捕获智能指针」这件事格外敏感。
四、知识体系总览
最后,我用一张图把整个章节的知识体系串起来。你保存下来,以后写代码时瞄一眼就行。
好了,到这里,整个《C++内存管理与智能指针深度解析》课程就结束了。说实话,写这 30 章的过程,我自己也重新梳理了一遍很多细节。希望这些内容能帮你在实际项目中少踩坑、多产出。嗯,代码路上,我们后会有期。
公众号:蓝海资料掘金营,微信deep3321