为了解决 new 出的对象被忘记 delete 导致的内存泄漏问题,C++11 引入了智能指针。核心思想是 RAII——将裸指针的生命周期绑定到栈对象的生命周期上,栈对象析构时自动释放堆内存。
unique_ptr —— 独占所有权
unique_ptr 独占对象所有权,不可拷贝(拷贝构造和拷贝赋值被 = delete),只能移动。这一限制使得它的开销和裸指针完全一致——没有引用计数,没有控制块,sizeof 就是裸指针大小。
| |
使用场景: 工厂函数返回值、容器中存放对象、PIMPL 惯用法、函数内的局部动态分配。
shared_ptr —— 共享所有权
shared_ptr 允许多个指针共享同一个对象,通过引用计数管理生命周期。当最后一个 shared_ptr 析构时,对象被释放。
控制块(Control Block)
shared_ptr 的底层核心是一个控制块,分配在堆上,包含两个计数:
| |
每个 shared_ptr 占两个指针大小(指向对象的指针 + 指向控制块的指针),共 16 字节(64 位系统)。
控制块的创建时机很关键:用裸指针构造 shared_ptr 时创建控制块。因此如果用一个裸指针构造多个 shared_ptr,会创建多个独立的控制块,导致 double-free:
| |
正确做法是用 make_shared 或从一个已有的 shared_ptr 拷贝:
| |
make_shared 还有额外的优势:它将托管对象和控制块合并在一次内存分配中,不仅减少分配次数,还能提升缓存局部性。代价是:如果有 weak_ptr 存在,即使 shared_ptr 全部析构了,整块内存也要等 weak_ptr 全消亡才能释放(因为对象和控制块是同一块内存)。
shared_from_this
当一个类需要在成员函数中返回自身的 shared_ptr 时,不能直接用 shared_ptr<T>(this)(会创建重复控制块),而应该继承 enable_shared_from_this<T> 并调用 shared_from_this():
| |
enable_shared_from_this 内部存了一个 weak_ptr,shared_from_this() 从中构造出 shared_ptr。注意:调用 shared_from_this() 时对象必须已被 shared_ptr 管理,否则抛 bad_weak_ptr 异常。
weak_ptr —— 打破循环引用
weak_ptr 不增加强引用计数,它指向一个被 shared_ptr 管理的对象,但不拥有它。主要用途是打破循环引用:
| |
典型场景:双向关联(parent-child、observer 模式)、缓存系统、异步回调中避免延长对象生命期。
使用 weak_ptr 访问对象时,必须先调用 lock() 尝试提升为 shared_ptr:
| |
性能对比
| 类型 | sizeof | 构造/析构 | 解引用 | 拷贝 | 典型开销 |
|---|---|---|---|---|---|
unique_ptr | 裸指针大小(8B) | O(1) | 等同于裸指针 | 不可拷贝,移动 O(1) | 零额外开销(无自定义删除器时) |
shared_ptr | 2×裸指针(16B) | 原子操作 ±1 | 等同于裸指针 | 原子 inc(+1) | 控制块堆分配 + 原子引用计数 |
weak_ptr | 2×裸指针(16B) | 不修改计数 | 需 lock() 提升 | — | 需要通过控制块访问 |
几个关键点:
shared_ptr的引用计数操作是原子的(线程安全),原子操作比普通++/--贵几十个 CPU cycle。如果确认单线程使用,可以用boost::local_shared_ptr或直接传引用避免拷贝。- 解引用不受影响——无论是
unique_ptr还是shared_ptr,ptr->foo()和裸指针一样快,开销只在构造/析构/拷贝/赋值时。 make_shared优于new+ 构造shared_ptr:减少一次堆分配,对象和控制块在同一缓存行。
游戏服务端的选择建议
游戏服务端对延迟和内存高度敏感,选择策略如下:
能用
unique_ptr就不用shared_ptr。 大多数对象的生命周期是清晰的(谁创建谁销毁),独占所有权已经够用。unique_ptr零开销——不花一分钱就换来内存安全。需要共享时优先考虑传引用而非拷贝
shared_ptr。 每拷贝一次shared_ptr就是一次原子加操作,热路径上积少成多。异步回调 + LRU 缓存用
shared_ptr+weak_ptr组合。 例如玩家数据在内存中的缓存:主表用shared_ptr持有,其他地方用weak_ptr观察,如果玩家下线了,weak_ptr.lock()返回空,自然淘汰。避免在对象内部用
shared_ptr持有自己所属的容器/管理器。 这是循环引用的高发区,该用weak_ptr的地方不要偷懒。对象池 +
unique_ptr是很适合游戏服务器的模式。 预分配一批对象,unique_ptr管理生命周期,配合自定义删除器实现release回池而非真正delete。
Q&A
Q1: shared_ptr、unique_ptr、weak_ptr 各自的底层实现、使用场景和性能开销分别是什么?
unique_ptr: 独占所有权,底层就是裸指针(无自定义删除器时 sizeof 和裸指针一样),不可拷贝只能移动。零开销——解引用和裸指针完全一致。适用于:工厂函数返回、PIMPL、局部动态分配、对象池。
shared_ptr: 共享所有权,底层是一个指向对象的指针 + 一个指向控制块的指针(共 16 字节)。控制块里存着原子引用计数(strong_count)和弱引用计数(weak_count)。拷贝时原子 inc,析构时原子 dec 到 0 就释放对象。开销来自控制块的堆分配和原子操作。适用于:多所有者无法确定谁最后释放时。
weak_ptr: 不拥有对象,配合 shared_ptr 打破循环引用。底层和 shared_ptr 一样也是两个指针,通过控制块访问,使用时需 lock() 提升为 shared_ptr 检查对象是否存活。适用于:observer、缓存、双向关联中需要打破循环的场景。
Q2: 在游戏服务端中你会如何选择?
游戏服务端选择: 能用 unique_ptr 就不用 shared_ptr——独占所有权覆盖大部分场景且零开销;需要共享时优先传引用避免原子计数;异步回调/缓存用 shared_ptr + weak_ptr 组合;对象池配合 unique_ptr 的自定义删除器很实用。