究竟什么时候用shared_ptr,什么时候用unique_ptr?
扫描二维码
随时随地手机看文章
最近,有同学来问我,想了解C++的三种智能指针的使用场景,在项目中应该如何选择?
首先要了解这三种智能指针的特点,std::unique_ptr、std::shared_ptr和std::weak_ptr:
std::unique_ptr
std::unique_ptr是一种独占所有权的智能指针,意味着同一时间内只能有一个unique_ptr指向一个特定的对象。
当unique_ptr被销毁时,它所指向的对象也会被销毁。
使用场景:
- 当你需要确保一个对象只被一个指针所拥有时。
- 当你需要自动管理资源,如文件句柄或互斥锁时。
- 当你不确定用哪种智能指针时,优先选择unique_ptr就没毛病。
示例代码:
#include #include class Test { public: Test() { std::cout << "Test::Test()\n"; } ~Test() { std::cout << "Test::~Test()\n"; } void test() { std::cout << "Test::test()\n"; } }; int main() { std::unique_ptr ptr(new Test()); ptr->test(); // 当ptr离开作用域时,它指向的对象会被自动销毁 return0; }
std::shared_ptr
std::shared_ptr是一种共享所有权的智能指针,多个shared_ptr可以指向同一个对象。内部使用引用计数来确保只有当最后一个指向对象的shared_ptr被销毁时,对象才会被销毁。
使用场景:
- 当你需要在多个所有者之间共享对象时。
- 当你需要通过复制构造函数或赋值操作符来复制智能指针时。
示例代码:
#include #include class Test { public: Test() { std::cout << "Test::Test()\n"; } ~Test() { std::cout << "Test::~Test()\n"; }void test() { std::cout << "Test::test()\n"; } }; int main() { std::shared_ptrptr1(new Test()); std::shared_ptrptr2 = ptr1; ptr1->test(); // 当ptr1和ptr2离开作用域时,它们指向的对象会被自动销毁 return0; }
std::weak_ptr
std::weak_ptr是一种不拥有对象所有权的智能指针,它指向一个由std::shared_ptr管理的对象。weak_ptr用于解决shared_ptr之间的循环引用问题。
是另外一种智能指针,它是对 shared_ptr 的补充,std::weak_ptr 是一种弱引用智能指针,用于观察 std::shared_ptr 指向的对象,而不影响引用计数。它主要用于解决循环引用问题,从而避免内存泄漏,另外如果需要追踪指向某个对象的第一个指针,则可以使用 weak_ptr。
可以考虑在对象本身中维护一个指向第一个 shared_ptr 的弱引用(std::weak_ptr)。当创建对象的第一个 shared_ptr 时,将这个 shared_ptr 赋值给对象的 weak_ptr 成员。这样,在需要时,可以通过检查对象的 weak_ptr 成员来获取指向对象的第一个 shared_ptr(如果仍然存在的话).
使用场景:
- 当你需要访问但不拥有由shared_ptr管理的对象时。
- 当你需要解决shared_ptr之间的循环引用问题时。
- 注意weak_ptr肯定要和shared_ptr搭配使用。
示例代码:
#include #include class Test { public: Test() { std::cout << "Test::Test()\n"; } ~Test() { std::cout << "Test::~Test()\n"; }void test() { std::cout << "Test::test()\n"; } }; int main() { std::shared_ptrsharedPtr(new Test()); std::weak_ptrweakPtr = sharedPtr; if (auto lockedSharedPtr = weakPtr.lock()) { lockedSharedPtr->test(); }// 当sharedPtr离开作用域时,它指向的对象会被自动销毁 return0; }
这三种智能指针各有其用途,选择哪一种取决于你的具体需求。
1)智能指针方面的建议:
- 尽量使用智能指针,而非裸指针来管理内存,很多时候利用RAII机制管理内存肯定更靠谱安全的多。
- 如果没有多个所有者共享对象的需求,建议优先使用unique_ptr管理内存,它相对shared_ptr会更轻量一些。
- 在使用shared_ptr时,一定要注意是否有循环引用的问题,因为这会导致内存泄漏。
- shared_ptr的引用计数是安全的,但是里面的对象不是线程安全的,这点要区别开。
2)为什么std::unique_ptr可以做到不可复制,只可移动?
因为把拷贝构造函数和赋值运算符标记为了delete,见源码:
template <typename _Tp, typename _Tp_Deleter = default_delete> class unique_ptr { // Disable copy from lvalue. unique_ptr(const unique_ptr&) = delete; template<typename _Up, typename _Up_Deleter> unique_ptr(const unique_ptr<_Up, _Up_Deleter>&) = delete; unique_ptr& operator=(const unique_ptr&) = delete; template<typename _Up, typename _Up_Deleter> unique_ptr& operator=(const unique_ptr<_Up, _Up_Deleter>&) = delete; };
3)shared_ptr的原理:
每个 std::shared_ptr 对象包含两个成员变量:一个指向被管理对象的原始指针,一个指向引用计数块的指针(control block pointer)。
引用计数块是一个单独的内存块,引用计数块允许多个 std::shared_ptr 对象共享相同的引用计数,从而实现共享所有权。
当创建一个新的 std::shared_ptr 时,引用计数初始化为 1,表示对象当前被一个 shared_ptr 管理。
- 拷贝 std::shared_ptr:当用一个 shared_ptr 拷贝出另一个 shared_ptr 时,需要拷贝两个成员变量(被管理对象的原始指针和引用计数块的指针),并同时将引用计数值加 1。这样,多个 shared_ptr 对象可以共享相同的引用计数。
- 析构 std::shared_ptr:当 shared_ptr 对象析构时,引用计数值减 1。然后检测引用计数是否为 0。如果引用计数为 0,说明没有其他 shared_ptr 对象指向该资源,因此需要同时删除原始对象(通过调用自定义删除器,如果有的话)。
4)智能指针的缺点
- 性能开销,需要额外的内存来存储他们的控制块,控制块包括引用计数,以及运行时的原子操作来增加或减少引用技术,这可能导致裸指针的性能下降。
- 循环引用问题,如果两个对象通过成员变量shared_ptr相互引用,并且没有其他指针指向这两个对象中的任何一个,那么这两个对象的内存将永远不会被释放,导致内存泄露。
#include #include class B;// 前向声明 class A { public: std::shared_ptr b_ptr; ~A() { std::cout << "A has been destroyed."<< std::endl; } }; class B { public: std::shared_ptr a_ptr; ~B() { std::cout << "B has been destroyed."<< std::endl; } }; int main() { std::shared_ptr a = std::make_shared(); std::shared_ptr b = std::make_shared(); a->b_ptr = b; // A 引用 B b->a_ptr = a; // B 引用 A // 由于存在循环引用,A 和 B 的析构函数将不会被调用,从而导致内存泄漏 return0; }
- 智能指针不一定适用于所有场景:有一些容器类,内部实现依赖于裸指针,另外在考虑某些性能关键场景下,使用裸指针可能更合适。但绝大多数场景,用智能指针就OK。
选型建议
- 默认选择unique_ptr,因为它性能最优,且语义清晰,比如局部动态对象。
- 当你发现unique_ptr使用受限,那大概率就是有需要共享的需求,需要多个模块或对象需共享同一资源时(如全局配置、线程间共享数据),使用shared_ptr,但要注意循环引用的问题。
- 优先使用make_unique和make_shared构造对应的智能指针,具备异常安全性。
- 避免裸指针和智能指针混用,容易出现double free等问题。
- unique_ptr放心使用,并没有额外开销。
- shared_ptr 的引用计数可能引发原子操作开销,除非对性能有非常极致的要求,否则没必要在意这点开销。也要注意循环引用会导致内存泄漏。





