有一次项目上线后,服务器的内存占用一直在慢慢上涨。排查了三天,我终于在一个观察者模式的模块里找到了罪魁祸首:两个对象互相持有 std::shared_ptr,形成了循环引用。引用计数永远不会归零,内存根本释放不掉。
更让人无奈的是,这段代码是一个刚入职的同学写的。他的逻辑很简单:“用了 shared_ptr,就不会内存泄漏了。”这种想法很普遍,也很危险。智能指针确实比裸指针安全,但它绝对不是银弹。
shared_ptr 循环引用:用了也会内存泄漏

std::shared_ptr 的核心机制是引用计数。每多一个指针指向同一块资源,计数加一;每少一个,计数减一。计数归零时释放。这个逻辑本身没问题,问题出在“互相持有”的时候。
struct B;
struct A {
std::shared_ptr<B> bptr;
};
struct B {
std::shared_ptr<A> aptr;
};
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->bptr = b;
b->aptr = a; // 循环引用,计数永远不为零
上面这段代码结束后,a 和 b 的引用计数都是 2。局部变量销毁时计数变成 1,但永远不会变成 0。两个对象就这样在堆里“相互牵制”,谁也释放不掉。
解决办法是 std::weak_ptr。它不会增加引用计数,只是一个“旁观者”:
struct B {
std::weak_ptr<A> aptr; // 不增加计数,打破循环
};
这是面试里最爱追问的点之一。很多人知道 weak_ptr 能破循环,但说不清为什么——其实就是因为它不参与引用计数。

性能开销,严重被低估
shared_ptr 的引用计数是原子操作。每次拷贝、赋值、销毁,都需要线程安全地加减计数。在高并发场景下,这个开销不小。我们曾经在一个高频交易的模块里,把所有对象都包成 shared_ptr,结果 profiler 一打,20% 的 CPU 时间花在了引用计数的原子操作上。
更容易被忽略的是控制块开销。shared_ptr 底层不只管理一个指针,还有一个隐形的控制块(引用计数 + 弱引用计数 + 删除器指针)。这意味着每个 shared_ptr 都有额外的内存开销。
| 指针类型 |
引用计数开销 |
控制块开销 |
线程安全 |
std::unique_ptr |
无 |
无 |
不需要 |
std::shared_ptr |
原子加减 |
有 |
需要 |
如果你的资源根本不需要共享,却一股脑地用 shared_ptr,就是在为不必要的功能付出代价。资深程序员的原则是:能用 unique_ptr 绝不用 shared_ptr。
异常安全,构造函数抛异常时会泄漏

有人喜欢这么写:
auto p = std::shared_ptr<Foo>(new Foo());
看起来没问题,但如果 new Foo() 成功而 shared_ptr 构造函数抛出异常,那个已经 new 出来的对象就没人管了——内存泄漏。
正确的做法是用 std::make_shared 或 std::make_unique:
auto p = std::make_shared<Foo>(); // 原子性分配,异常安全
make_shared 把对象和控制块放在同一块内存里分配,不仅异常安全,还少了一次分配。

unique_ptr 也有坑
std::unique_ptr 是独占式指针,不能拷贝,只能移动。新手很容易写出这种代码:
std::unique_ptr<int> p1(new int(42));
std::unique_ptr<int> p2 = p1; // 编译失败!不能拷贝
如果需要转移所有权,应该用 std::move:
std::unique_ptr<int> p2 = std::move(p1); // p1 变空,p2 接管
另一个常见错误是用 unique_ptr<T> 管理数组:
std::unique_ptr<int> arr(new int[10]); // 错!会调用 delete 而非 delete[]
std::unique_ptr<int[]> arr(new int[10]); // 对,模板特化版本
这两个坑不会导致内存泄漏,但会让你花大把时间在编译提示或崩溃日志里排查。

面试官继续追问
如果你能把前面这几点说清楚,面试官通常还会追问两个问题:
第一,make_shared 和 new 构造的 shared_ptr 有什么区别?答案是 make_shared 只分配一次内存,而 new 方式可能分配两次(对象一次、控制块一次),而且异常安全性更差。
第二,weak_ptr 的 .lock() 返回的是什么?答案是一个 shared_ptr,如果对象已经被释放则返回空指针。这是 weak_ptr 不增加引用计数的关键设计。
一句话总结
智能指针帮我们减少了大量手动管理内存的麻烦,但它们本身也有边界。循环引用、性能开销、异常安全、拷贝限制——这些都是资深程序员用得更谨慎的原因。
简单记:能独占就用 unique_ptr,必须共享才用 shared_ptr,相互引用用 weak_ptr 打破。不要为了“省事”而用错工具。
更多 C++ 与内存管理讨论,欢迎来 云栈社区 交流。