在 C++11 及以后,智能指针已成为资源管理的核心工具。本文聚焦两大主流实现——std::unique_ptr 与 std::shared_ptr,从语义、性能、安全性等维度对比它们,并给出在实际项目中的最佳实践。
1. 语义区别
- std::unique_ptr
- 所有权独占:指针拥有唯一所有者,无法复制,只能移动。
- 自动释放:所有者销毁时自动调用删除器,适合单一对象或链表、树等结构。
- std::shared_ptr
- 共享所有权:可以多处复制,内部维护引用计数。计数为零时销毁。
- 多线程安全:引用计数自增/自减是原子操作,适合共享资源。
2. 性能考量
| 特性 | unique_ptr | shared_ptr |
|---|---|---|
| 额外开销 | 0(仅指针) | 2 个引用计数(一个强计数、一个弱计数) |
| 线程开销 | 无 | 原子计数操作,轻量但仍有开销 |
| 内存占用 | 8/16 B(64/32 bit) | 24/32 B(64/32 bit)+ 管理块 |
当不需要共享所有权时,unique_ptr 是首选,既安全又高效。
3. 错误使用场景
-
误用 shared_ptr 造成循环引用:
struct Node { std::shared_ptr <Node> next; std::weak_ptr <Node> prev; // 必须弱引用 };若
prev也是shared_ptr,两个节点会互相持有,导致内存泄漏。 -
在多线程里使用 unique_ptr 共享数据:
unique_ptr不是线程安全的,若跨线程使用必须使用std::shared_ptr或外部同步。 -
与 C API 交互时忘记自定义删除器:
std::unique_ptr<FILE, decltype(&fclose)> fp(fopen(...), fclose);unique_ptr的删除器默认是delete,不适用于 C 资源。
4. 最佳实践
-
优先使用 unique_ptr
- 对象生命周期由创建者决定,避免不必要的引用计数。
- 通过
std::move将所有权转移给容器或返回值。
-
在需要共享所有权时才使用 shared_ptr
- 例如 GUI 事件回调、缓存共享、资源池。
- 如果可能,使用
std::weak_ptr防止循环引用。
-
自定义删除器
- 对于自定义资源或 C 风格对象,传入删除器或使用
std::unique_ptr<... , Deleter>。
- 对于自定义资源或 C 风格对象,传入删除器或使用
-
与 std::make_unique / std::make_shared 结合使用
- 避免多次
new,保证异常安全。
- 避免多次
-
考虑 move-only 对象
std::vector<std::unique_ptr<T>>需要使用emplace_back(std::make_unique<T>(...))。std::vector<std::shared_ptr<T>>可以直接push_back。
5. 代码示例
5.1 资源管理
// 文件句柄管理
class File {
std::unique_ptr<FILE, decltype(&fclose)> file_;
public:
explicit File(const char* path) :
file_(fopen(path, "r"), fclose) {
if (!file_) throw std::runtime_error("open failed");
}
FILE* get() const { return file_.get(); }
};
5.2 共享资源示例
class Widget {
public:
Widget() { std::cout << "Widget created\n"; }
~Widget() { std::cout << "Widget destroyed\n"; }
};
void worker(std::shared_ptr <Widget> w) {
std::cout << "Worker use widget\n";
}
int main() {
auto w = std::make_shared <Widget>();
std::thread t1(worker, w);
std::thread t2(worker, w);
t1.join(); t2.join(); // 共享所有权,引用计数自动管理
}
5.3 防止循环引用
struct Node {
std::shared_ptr <Node> next;
std::weak_ptr <Node> prev; // 关键:使用 weak_ptr
};
void link(Node* a, Node* b) {
a->next = std::shared_ptr <Node>(b);
b->prev = std::weak_ptr <Node>(a);
}
6. 结语
在 C++ 编程中,智能指针的正确使用是提高代码安全性和可维护性的关键。
unique_ptr适合拥有唯一所有者的资源,既简单又高效。shared_ptr在需要共享所有权时才使用,并配合weak_ptr防止循环引用。
遵循上述最佳实践,可在保持代码简洁的同时,避免内存泄漏和资源竞争问题。