unique_ptr 还是 shared_ptr?C++ 智能指针选型与内存泄漏实战分析

unique_ptr 还是 shared_ptr?C++ 智能指针选型与内存泄漏实战分析

很多人学完智能指针会有一种错觉:

"用了智能指针就不会内存泄漏了。"

现实是:智能指针只是把"谁负责释放"写进了类型里 ,如果你所有权设计错了,照样泄漏。shared_ptr 甚至会因为"太好用",让你把本该独占的资源搞成共享,再把共享搞成环。


一、先记住一句话

默认用 unique_ptr;只有"真的有多个所有者"才用 shared_ptr;weak_ptr 不是用来替代它们的,是用来打破共享关系的。

智能指针的本质不是"自动 GC",而是 RAII + 所有权语义。


二、unique_ptr:独占所有权

语义

  • 一个对象,同一时刻只有一个主人

  • 不能拷贝,只能 std::move

  • 离开作用域自动析构

  • 开销约等于裸指针

    auto res = std::make_unique(42);
    res->do_something();
    // 函数结束自动 delete

适合什么场景

  • 工厂函数返回值

  • 类成员持有资源

  • 树形结构:父拥有子

  • 容器里放"独占对象"

    class Session {
    std::unique_ptr buf_;
    };

传递规则

目的 写法
调用方继续拥有 void f(const T&) / T*
函数内部借用 void f(const std::unique_ptr<T>&) 不常见,通常用引用
把所有权转出去 std::unique_ptr<T> f()
接收所有权 void f(std::unique_ptr<T>)
转移所有权 f(std::move(p))

如果你发现自己在到处拷贝资源,先别用 shared_ptr,先问:

"这东西到底该谁拥有?"


三、shared_ptr:共享所有权,但有代价

语义

  • 多个 shared_ptr 共同拥有一个对象

  • 拷贝时强引用计数 +1

  • 最后一个 shared_ptr 销毁时,对象才析构

  • 控制块里有:强引用计数、弱引用计数、deleter、allocator

    auto a = std::make_shared();
    auto b = a; // 引用计数 = 2

代价是什么

  • 原子引用计数操作
  • 额外控制块
  • 对象析构时机不再"一眼看懂"
  • 容易写出隐藏的共享关系

所以:shared_ptr 不是"更安全的 unique_ptr",而是另一套所有权模型。


四、怎么选:一张决策表

场景 选谁
一个所有者 unique_ptr
函数返回动态对象 unique_ptr
类成员拥有资源 unique_ptr
父子/树/组合关系 父用 unique_ptr,子回指父用 weak_ptr 或裸指针
缓存、全局配置、多模块共享 shared_ptr
观察者模式 被观察用 shared_ptr,观察者用 weak_ptr
图形/节点双向引用 一边 shared_ptr,一边 weak_ptr
只借来看一眼 裸指针 / weak_ptr,不要抢所有权

经验法则:

复制代码
能 unique 就 unique
unique 写不下去,再考虑 shared
shared 出现环,再用 weak 破环

五、最经典的坑:shared_ptr 循环引用

错误示例

复制代码
struct Node {
    std::string name;
    std::shared_ptr<Node> next;
    std::shared_ptr<Node> prev;
};

auto a = std::make_shared<Node>("A");
auto b = std::make_shared<Node>("B");

a->next = b;
b->prev = a;

引用计数变化:

复制代码
创建 a:a.use_count() = 1
创建 b:b.use_count() = 1

a->next = b :b = 2
b->prev = a :a = 2

离开作用域:

复制代码
局部变量 a 销毁:a 计数 2 -> 1
局部变量 b 销毁:b 计数 2 -> 1

两个计数都还剩 1,谁都不肯死。

结果:

  • 析构函数不调用
  • 内存泄漏
  • 长时间运行服务内存慢慢涨

这就是 shared_ptr 最讽刺的地方:

你用了"智能指针",反而更难发现泄漏,因为你以为自己已经安全了。


六、正确做法:一强一弱

复制代码
struct Node {
    std::string name;
    std::shared_ptr<Node> next;   // 强引用:我拥有下一个
    std::weak_ptr<Node> prev;     // 弱引用:我只是知道上一个
};

使用:

复制代码
auto a = std::make_shared<Node>("A");
auto b = std::make_shared<Node>("B");

a->next = b;
b->prev = a; // weak_ptr,不增加引用计数

离开作用域时:

复制代码
b 的强引用计数:1 -> 0,b 析构
a 的强引用计数:1 -> 0,a 析构

weak_ptr 不拥有对象,只是"观察"。要用的时候必须 lock():

复制代码
if (auto p = node->prev.lock()) {
    std::cout << p->name << "\n";
} else {
    // 前驱已经没了
}

不要先 expired() 再 lock() ,中间可能瞬间失效。直接用 lock() 判空就行。


七、实战场景 1:父子对象

复制代码
class Child;

class Parent {
public:
    std::unique_ptr<Child> child;
    ~Parent() { std::cout << "Parent destroyed\n"; }
};

class Child {
public:
    std::weak_ptr<Parent> parent; // 回指父对象
    ~Child() { std::cout << "Child destroyed\n"; }
};

如果 Child::parent 也用 shared_ptr:

复制代码
Parent 拥有 Child
Child 又拥有 Parent

直接环死。

正确模型:

复制代码
Parent 拥有 Child
Child 观察 Parent

八、实战场景 2:观察者模式

复制代码
struct Subject {
    std::vector<std::weak_ptr<Observer>> observers;

    void notify() {
        for (auto it = observers.begin(); it != observers.end(); ) {
            if (auto ob = it->lock()) {
                ob->on_event();
                ++it;
            } else {
                it = observers.erase(it); // 观察者已死,清理
            }
        }
    }
};

如果用 shared_ptr 存观察者:

  • 观察者想死也死不了
  • Subject 偷偷"保活"了所有订阅者
  • 长连接系统里非常常见

九、实战场景 3:缓存系统

复制代码
class Cache {
    std::map<int, std::weak_ptr<BigObject>> items_;
public:
    std::shared_ptr<BigObject> get(int id) {
        if (auto it = items_.find(id); it != items_.end()) {
            if (auto obj = it->second.lock()) {
                return obj; // 还活着,直接返回
            }
            items_.erase(it); // 已经释放,清理脏条目
        }
        auto obj = std::make_shared<BigObject>(id);
        items_[id] = obj;
        return obj;
    }
};

缓存如果用 shared_ptr:

  • 对象永远被缓存按住
  • 缓存越大,泄漏越像"正常内存增长"

十、还有这些坑,比循环引用更隐蔽

1. 同一个裸指针构造两个智能指针

复制代码
Resource* raw = new Resource;
std::shared_ptr<Resource> a(raw);
std::shared_ptr<Resource> b(raw); // 双重释放

✅ 永远用:

复制代码
auto a = std::make_shared<Resource>();

2. get() 出来的裸指针又被别人管理

复制代码
auto p = std::make_unique<Resource>();
Resource* raw = p.get();
std::shared_ptr<Resource> q(raw); // 崩

规则:

get() 只借不转手,永远不要用它再去构造智能指针。


3. lambda 里捕获 shared_ptr 导致对象死不了

复制代码
auto self = shared_from_this();
timer.async_wait([self](auto) {
    self->do_something();
});

如果这个回调一直没触发,或者线程池长期持有:

  • 对象永远不析构
  • 看起来像业务对象"该释放却还在"

异步代码里这是高频事故。


4. make_shared 的隐藏副作用

复制代码
auto p = std::make_shared<BigObject>();
std::weak_ptr<BigObject> w = p;
p.reset();

你以为 BigObject 内存马上归还:

  • 对象析构函数会跑
  • 但整块内存(对象 + 控制块)要等最后一个 weak_ptr 也死掉才释放

如果 weak_ptr 长期存活,大对象内存不会立刻回收。

这种场景可以考虑:

复制代码
std::shared_ptr<BigObject> p(new BigObject);

对象和控件块分开分配,对象内存可以更早归还。


十一、调试内存泄漏时看什么

1. 看析构有没有跑

复制代码
~Node() {
    std::cout << "Node destroyed\n";
}

如果程序结束没打印,基本就是:

  • 还有 shared_ptr 活着
  • 或者循环引用
  • 或者回调/容器持有了

2. 看 use_count()

复制代码
std::cout << p.use_count() << "\n";

注意:

use_count() 只能辅助调试,不能拿来写业务逻辑。


3. 工具

  • Valgrind / massif
  • heaptrack
  • AddressSanitizer
  • Visual Studio CRT leak detection
  • 长期服务:监控 RSS / 常驻内存曲线

循环引用型泄漏的特征是:

复制代码
分配只增不减
析构日志越来越少
use_count 永远 > 0

十二、现代 C++ 的"铁律"

复制代码
1. 不写裸 new / delete
2. 默认 unique_ptr
3. 真共享才 shared_ptr
4. 回指 / 观察 / 缓存用 weak_ptr
5. 用 make_unique / make_shared
6. 不要从 get() 再造智能指针
7. 双向关系先画所有权,再写代码

十三、一句话收尾

unique_ptr 说的是:这东西归我,我死了它就死。

shared_ptr 说的是:大家一起养,最后一个人走才收拾。

weak_ptr 说的是:我只是看看,不负责养,也不拦你死。

大多数 C++ 项目的真实问题,不是"不会用 shared_ptr",而是"太早用 shared_ptr"。

如果你愿意,我可以把这篇文章再整理成一版「面试速答版」:

unique_ptr / shared_ptr / weak_ptr 三句话讲清 + 5 个高频泄漏 case + 面试官最爱追问的点。

相关推荐
老板一杯拿铁1 小时前
Java 异常体系详解:从字节码原理到最佳实践
java
蜗牛互联网1 小时前
Java 17 HttpClient调用文件转写API的超时与失败回退
java·人工智能·后端
禾小西2 小时前
07丨Redis 哨兵机制:主库故障后,如何恢复服务?
java·开发语言·redis
我不是阵雨2 小时前
JDK 21虚拟线程Pinning陷阱:一文拔钉解困
java·开发语言
小卡车5552 小时前
java反射、自定义注解的应用(自定义分页)
java
徐小黑ACG2 小时前
Golang 基础05 结构体struct
开发语言·算法·golang
优橙教育3 小时前
零基础学AI应用开发要多久?3个月能到什么水平
服务器·开发语言·网络·php
BLUcoding3 小时前
接口服务公网超时排查记录:SecureRandom 阻塞问题分析与解决
java·linux·springboot·aes·securerandom
SEO_juper3 小时前
用 Python 写一个 GEO 可见性检查脚本:你的网站现在能被 AI 引用吗
开发语言·人工智能·爬虫·python·seo·外贸独立站