当前位置:首页 > 单片机 > 程序喵大人

最近,有同学来问我,想了解C++的三种智能指针的使用场景,在项目中应该如何选择?

首先要了解这三种智能指针的特点,std::unique_ptrstd::shared_ptrstd::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 管理。

  1. 拷贝 std::shared_ptr:当用一个 shared_ptr 拷贝出另一个 shared_ptr 时,需要拷贝两个成员变量(被管理对象的原始指针和引用计数块的指针),并同时将引用计数值加 1。这样,多个 shared_ptr 对象可以共享相同的引用计数。
  2. 析构 std::shared_ptr:当 shared_ptr 对象析构时,引用计数值减 1。然后检测引用计数是否为 0。如果引用计数为 0,说明没有其他 shared_ptr 对象指向该资源,因此需要同时删除原始对象(通过调用自定义删除器,如果有的话)。

4)智能指针的缺点

  1. 性能开销,需要额外的内存来存储他们的控制块,控制块包括引用计数,以及运行时的原子操作来增加或减少引用技术,这可能导致裸指针的性能下降。
  2. 循环引用问题,如果两个对象通过成员变量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; } 
  1. 智能指针不一定适用于所有场景:有一些容器类,内部实现依赖于裸指针,另外在考虑某些性能关键场景下,使用裸指针可能更合适。但绝大多数场景,用智能指针就OK。

选型建议

  1. 默认选择unique_ptr,因为它性能最优,且语义清晰,比如局部动态对象。
  2. 当你发现unique_ptr使用受限,那大概率就是有需要共享的需求,需要多个模块或对象需共享同一资源时(如全局配置、线程间共享数据),使用shared_ptr,但要注意循环引用的问题。
  3. 优先使用make_uniquemake_shared构造对应的智能指针,具备异常安全性。
  4. 避免裸指针和智能指针混用,容易出现double free等问题。
  5. unique_ptr放心使用,并没有额外开销。
  6. shared_ptr 的引用计数可能引发原子操作开销,除非对性能有非常极致的要求,否则没必要在意这点开销。也要注意循环引用会导致内存泄漏。


本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
关闭