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_ptrget() 拿到裸指针
  • 实在不行,用 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_sharedshared_ptr(new T) 好吗?」嗯,大部分情况下是的。但在性能敏感场景,make_shared 有一个坑:它把控制块和对象分配在同一块内存里。如果对象很大,而 weak_ptr 还活着,这块内存就不能释放。说白了,就是内存不能及时归还

所以,如果对象很大,且 weak_ptr 生命周期很长,我建议分开分配:

// 大对象 + 长生命周期 weak_ptr
std::shared_ptr<BigObject> sp(new BigObject);
// 控制块和对象分开,对象可以提前释放

三、代码审查清单

好,最后这部分,是我自己多年代码审查经验的总结。我把它做成一个清单,你审查代码时,一条一条过就行。

✅ 智能指针代码审查清单

  1. 所有权是否明确?——每个对象应该只有一个「主人」。如果到处都是 shared_ptr,说明设计有问题。
  2. 有没有循环引用?——检查 parent-child 关系。父持有子的 shared_ptr,子持有父的 weak_ptr
  3. 参数传递方式对吗?——只读观察用 const T&T*;需要延长生命周期用 shared_ptr 传值;唯一所有权用 unique_ptr 传右值。
  4. 有没有不必要的 shared_ptr 拷贝?——函数参数是不是传了值?循环里是不是反复拷贝了?
  5. 自定义删除器用对了吗?——文件句柄、socket、内存池资源,有没有忘记自定义删除器?
  6. make_shared 和 shared_ptr(new) 选对了吗?——大对象 + 长 weak_ptr 生命周期,避免用 make_shared。
  7. 有没有裸指针裸 delete?——新代码里不应该出现 delete 关键字,除非在自定义删除器里。
  8. 跨模块边界传递时,用的是 shared_ptr 吗?——unique_ptr 跨 DLL 传递会出问题,因为删除器在不同模块里。

💡 我的小技巧:审查代码时,我习惯先看头文件里的成员变量。如果看到 shared_ptr<T> 成员,我会问:「这个对象真的需要共享吗?」很多时候,改成 unique_ptr 就能让设计更清晰。

⚠️ 我曾经踩过的坑:有一次,我在一个网络库里用了 shared_ptr 管理连接对象。结果因为回调里捕获了 shared_ptr,导致连接断开后对象迟迟不释放。最后用 weak_ptr 配合 lock() 才解决。嗯,从那以后,我对「回调中捕获智能指针」这件事格外敏感。

四、知识体系总览

最后,我用一张图把整个章节的知识体系串起来。你保存下来,以后写代码时瞄一眼就行。

智能指针最佳实践知识体系 选型规则 • 唯一所有权 → unique_ptr • 共享所有权 → shared_ptr • 观察者 → weak_ptr • 跨模块 → shared_ptr 性能取舍 • 避免 shared_ptr 拷贝 • unique_ptr 零开销 • 内存池 + 自定义删除器 • 大对象慎用 make_shared 审查清单 • 所有权是否明确? • 有无循环引用? • 参数传递方式? • 有无裸 delete? 核心原则 明确所有权 → 最小开销原则 → 审查常态化 实践建议:从 unique_ptr 开始,按需升级为 shared_ptr —— 不要为不需要共享的东西买单

好了,到这里,整个《C++内存管理与智能指针深度解析》课程就结束了。说实话,写这 30 章的过程,我自己也重新梳理了一遍很多细节。希望这些内容能帮你在实际项目中少踩坑、多产出。嗯,代码路上,我们后会有期。


公众号:蓝海资料掘金营,微信deep3321