找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

5936

积分

0

好友

752

主题
发表于 前天 19:55 | 查看: 1| 回复: 0

有一次项目上线后,服务器的内存占用一直在慢慢上涨。排查了三天,我终于在一个观察者模式的模块里找到了罪魁祸首:两个对象互相持有 std::shared_ptr,形成了循环引用。引用计数永远不会归零,内存根本释放不掉。

更让人无奈的是,这段代码是一个刚入职的同学写的。他的逻辑很简单:“用了 shared_ptr,就不会内存泄漏了。”这种想法很普遍,也很危险。智能指针确实比裸指针安全,但它绝对不是银弹。

shared_ptr 循环引用:用了也会内存泄漏

shared_ptr循环引用导致内存泄漏与weak_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; // 循环引用,计数永远不为零

上面这段代码结束后,ab 的引用计数都是 2。局部变量销毁时计数变成 1,但永远不会变成 0。两个对象就这样在堆里“相互牵制”,谁也释放不掉。

解决办法是 std::weak_ptr。它不会增加引用计数,只是一个“旁观者”:

struct B {
std::weak_ptr<A> aptr; // 不增加计数,打破循环
};

这是面试里最爱追问的点之一。很多人知道 weak_ptr 能破循环,但说不清为什么——其实就是因为它不参与引用计数。

unique_ptr零开销与shared_ptr原子计数开销对比图

性能开销,严重被低估

shared_ptr 的引用计数是原子操作。每次拷贝、赋值、销毁,都需要线程安全地加减计数。在高并发场景下,这个开销不小。我们曾经在一个高频交易的模块里,把所有对象都包成 shared_ptr,结果 profiler 一打,20% 的 CPU 时间花在了引用计数的原子操作上。

更容易被忽略的是控制块开销。shared_ptr 底层不只管理一个指针,还有一个隐形的控制块(引用计数 + 弱引用计数 + 删除器指针)。这意味着每个 shared_ptr 都有额外的内存开销。

指针类型 引用计数开销 控制块开销 线程安全
std::unique_ptr 不需要
std::shared_ptr 原子加减 需要

如果你的资源根本不需要共享,却一股脑地用 shared_ptr,就是在为不必要的功能付出代价。资深程序员的原则是:能用 unique_ptr 绝不用 shared_ptr

异常安全,构造函数抛异常时会泄漏

make_shared与new构造shared_ptr的异常安全对比示意图

有人喜欢这么写:

auto p = std::shared_ptr<Foo>(new Foo());

看起来没问题,但如果 new Foo() 成功而 shared_ptr 构造函数抛出异常,那个已经 new 出来的对象就没人管了——内存泄漏

正确的做法是用 std::make_sharedstd::make_unique

auto p = std::make_shared<Foo>(); // 原子性分配,异常安全

make_shared 把对象和控制块放在同一块内存里分配,不仅异常安全,还少了一次分配。

unique_ptr拷贝与数组管理常见误区示意图

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]); // 对,模板特化版本

这两个坑不会导致内存泄漏,但会让你花大把时间在编译提示或崩溃日志里排查。

C++智能指针选型速查表

面试官继续追问

如果你能把前面这几点说清楚,面试官通常还会追问两个问题:

第一,make_sharednew 构造的 shared_ptr 有什么区别?答案是 make_shared 只分配一次内存,而 new 方式可能分配两次(对象一次、控制块一次),而且异常安全性更差。

第二,weak_ptr.lock() 返回的是什么?答案是一个 shared_ptr,如果对象已经被释放则返回空指针。这是 weak_ptr 不增加引用计数的关键设计。

一句话总结

智能指针帮我们减少了大量手动管理内存的麻烦,但它们本身也有边界。循环引用、性能开销、异常安全、拷贝限制——这些都是资深程序员用得更谨慎的原因。

简单记:能独占就用 unique_ptr,必须共享才用 shared_ptr,相互引用用 weak_ptr 打破。不要为了“省事”而用错工具。

更多 C++ 与内存管理讨论,欢迎来 云栈社区 交流。




上一篇:DeepSeek Flash 调价公布:缓存命中输入降 60%,RAG 应用 API 成本大幅降低
下一篇:字节跳动绩效调整复盘:M+收紧、字节范纳入考核与I档退出信号
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-9-13 18:04 , Processed in 0.330143 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表