std::shared_ptr、std::unique_ptr 和 std::weak_ptr 的区别及适用场景
在 C++11 及其之后的标准中,智能指针(Smart Pointers)是管理动态内存的核心工具。它们基于 RAII(资源获取即初始化)原则,能够自动释放内存,从而有效避免内存泄漏和悬空指针问题。
C++ 标准库提供了三种主要的智能指针:std::unique_ptr、std::shared_ptr 和 std::weak_ptr。它们的区别主要在于所有权模型和底层机制。
1. std::unique_ptr(独占所有权)
核心概念
unique_ptr 代表独占式(Exclusive)的所有权。同一时刻,只能有一个 unique_ptr 指向同一个动态分配的对象。当 unique_ptr 被销毁(离开作用域)时,它所指向的对象也会被自动销毁。
特点与机制
- 不可复制:不能通过赋值或拷贝构造函数复制
unique_ptr(禁用了 copy 语义)。 - 可移动:可以通过
std::move将所有权从一个unique_ptr转移到另一个(启用了 move 语义)。 - 零开销:在默认情况下,
unique_ptr的大小和裸指针(raw pointer)完全一样,几乎没有性能开销。
适用场景
- 局部动态对象:在函数内部需要动态分配内存,且不需要与其他部分共享时(这是最常见的场景)。
- 作为类的成员变量:当一个类需要管理一个生命周期与自己绑定的动态对象时。
- 工厂模式的返回值:函数返回一个动态创建的对象,并把内存管理权交接给调用者。
代码示例
cpp
#include <memory>
#include <iostream>
class MyClass {
public:
void doSomething() { std::cout << "Doing something\n"; }
};
void unique_ptr_demo() {
// 推荐使用 std::make_unique (C++14 起)
std::unique_ptr<MyClass> ptr1 = std::make_unique<MyClass>();
ptr1->doSomething();
// std::unique_ptr<MyClass> ptr2 = ptr1; // 错误:不能复制!
// 将所有权从 ptr1 转移给 ptr2
std::unique_ptr<MyClass> ptr2 = std::move(ptr1);
if (!ptr1) {
std::cout << "ptr1 is now empty.\n"; // ptr1 现在为空
}
ptr2->doSomething();
} // ptr2 离开作用域,自动 delete 其管理的 MyClass 对象
2. std::shared_ptr(共享所有权)
核心概念
shared_ptr 代表共享式(Shared)的所有权。多个 shared_ptr 可以同时指向同一个对象。它内部使用引用计数(Reference Counting)来管理内存。
特点与机制
- 引用计数:每当复制一个
shared_ptr时,内部的引用计数加 1;每当一个shared_ptr离开作用域被销毁时,引用计数减 1。 - 自动释放:当引用计数降为 0 时,说明没有指针再指向该对象,对象会被自动 delete。
- 性能开销:比
unique_ptr开销大。因为它需要额外分配一个控制块(Control Block)来存储引用计数,并且引用计数的增减是原子操作(Thread-safe),这会带来一定的性能损耗。
适用场景
- 复杂的生命周期:当对象的生命周期无法通过单纯的单一作用域确定,且多个组件都需要持有该对象时。
- 多线程共享数据:多个线程需要访问同一个对象,只要还有一个线程在使用,对象就不能被销毁。
- 底层数据结构:例如图(Graph)或某些复杂的树结构中,多个节点可能共享对某个子节点的引用。
代码示例
cpp
#include <memory>
#include <iostream>
void shared_ptr_demo() {
// 推荐使用 std::make_shared (只需一次内存分配,性能更好)
std::shared_ptr<int> ptr1 = std::make_shared<int>(100);
std::cout << "Count: " << ptr1.use_count() << "\n"; // 输出: 1
{
std::shared_ptr<int> ptr2 = ptr1; // 复制,共享所有权
std::cout << "Count: " << ptr1.use_count() << "\n"; // 输出: 2
} // ptr2 离开作用域,引用计数减 1
std::cout << "Count: " << ptr1.use_count() << "\n"; // 输出: 1
} // ptr1 离开作用域,引用计数变为 0,内存释放
3. std::weak_ptr(弱引用观察者)
核心概念
weak_ptr 是一种不控制对象生命周期的智能指针,它指向一个由 shared_ptr 管理的对象。它的存在是为了协助 shared_ptr 工作。
特点与机制
- 不增加引用计数:用
shared_ptr初始化一个weak_ptr,不会增加控制块中的“强引用计数(Strong Count)”,因此不会阻止对象的销毁。 - 防悬空(Dangling Pointer):它知道自己指向的对象是否还活着。
- 不能直接访问数据:
weak_ptr没有重载*和->运算符。要访问对象,必须调用.lock()方法,将其升级(提升)为一个shared_ptr。如果对象已被销毁,.lock()会返回一个空的shared_ptr。
适用场景
- 打破循环引用:这是
weak_ptr最重要的用途。如果对象 A 有一个shared_ptr指向对象 B,对象 B 也用shared_ptr指向对象 A,两者的引用计数永远不会降为 0,导致内存泄漏。将其中一个改为weak_ptr即可打破僵局。 - 观察者模式(Observer Pattern):订阅者只需“观察”发布者,但不应该延长发布者的生命周期。
- 缓存(Caching):保存指向可能被销毁对象的指针,使用前检查是否还在。
代码示例(打破循环引用)
cpp
#include <memory>
#include <iostream>
class B; // 前置声明
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed\n"; }
};
class B {
public:
// 如果这里用 std::shared_ptr<A>,将导致内存泄漏!
std::weak_ptr<A> a_ptr;
~B() { std::cout << "B destroyed\n"; }
void useA() {
// 使用前必须 lock 提升为 shared_ptr
if (std::shared_ptr<A> shared_a = a_ptr.lock()) {
std::cout << "A is still alive\n";
} else {
std::cout << "A has been destroyed\n";
}
}
};
void weak_ptr_demo() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a; // weak_ptr 赋值,不增加 A 的引用计数
} // 正常销毁 A 和 B
总结与最佳实践
核心对比表
| 智能指针 | 所有权模型 | 复制 / 移动 | 性能开销 | 主要解决的问题 |
|---|---|---|---|---|
std::unique_ptr |
独占 (1 对 1) | 不可复制 / 可移动 | 极低(同裸指针) | 替代手动 new/delete,管理局部动态内存 |
std::shared_ptr |
共享 (多 对 1) | 可复制 / 可移动 | 中等(需维护控制块和原子计数) | 复杂对象的生命周期管理,多组件共享数据 |
std::weak_ptr |
不拥有 (观察者) | 可复制 / 可移动 | 低(通常结合 shared_ptr 使用) | 打破 shared_ptr 循环引用,安全地观察对象 |
💡 日常开发建议(Rule of Thumb)
- 默认首选
std::unique_ptr:只要你不需要在多个地方共享所有权,就无脑用unique_ptr。它最轻量、最快、意图最清晰。 - 谨慎使用
std::shared_ptr:只有在真的无法确定对象何时该被销毁,且确实需要多个持有者共享生命周期时才用。滥用会导致系统性能下降和逻辑混乱。 - 永远使用
std::make_unique和std::make_shared:不要手动new再传给智能指针。make_系列函数不仅更安全(防止内存泄漏),对于shared_ptr来说性能也更好(减少一次内存分配)。 - 注意循环引用:如果设计中出现了互相持有的
shared_ptr,立刻检查是否应该把其中一个换成weak_ptr。
右滑查看面试常问