C++11 shared_ptr(智能指针)详解

shared_ptr为共享指针,意味我们共同指向一块空间;里面采用引用计数,当有别的shared_ptr指向我这块空间时,就增加引用计数,当引用计数减为0的时候,才释放这块空间。

要确保用 new 动态分配的内存空间在程序的各条执行路径都能被释放是一件麻烦的事情。C++ 11 模板库的 <memory> 头文件中定义的智能指针,即 shared _ptr 模板,就是用来部分解决这个问题的。

只要将 new 运算符返回的指针 p 交给一个 shared_ptr 对象“托管”,就不必担心在哪里写delete p语句——实际上根本不需要编写这条语句,托管 p 的 shared_ptr 对象在消亡时会自动执行delete p。而且,该 shared_ptr 对象能像指针 p —样使用,即假设托管 p 的 shared_ptr 对象叫作 ptr,那么 *ptr 就是 p 指向的对象。

通过 shared_ptr 的构造函数,可以让 shared_ptr 对象托管一个 new 运算符返回的指针,写法如下:

shared_ptr<T> ptr(new T);  // T 可以是 int、char、类等各种类型

此后,ptr 就可以像 T* 类型的指针一样使用,即 *ptr 就是用 new 动态分配的那个对象。

多个 shared_ptr 对象可以共同托管一个指针 p,当所有曾经托管 p 的 shared_ptr 对象都解除了对其的托管时,就会执行delete p
示例1:shared_ptr实现

template<class T>
class SharedPtr
{
public:
      SharedPtr(T* ptr)
           :_ptr(ptr)
           , _pCount(new int(1))
      {}

      SharedPtr(const SharedPtr<T>& ap)
           :_ptr(ap._ptr)
           , _pCount(ap._pCount)
      {
           ++(*_pCount);
      }

      SharedPtr<T>& operator=(const SharedPtr<T>& ap)
      {
           if (_ptr != ap._ptr)
           {
                 if (--(*_pCount) == 0)
                 {
                      delete _ptr;
                      delete _pCount;
                 }
                 _ptr = ap._ptr;
                 _pCount = ap._pCount;
                 ++(*_pCount);
           }
           return *this;
      }
      ~SharedPtr()
      {
           if (--(*_pCount) == 0)
           {
                 delete _ptr;
                 delete _pCount;
                 cout << "~SharedPtr()" << endl;
           }
      }

      T& operator*()
      {
           return *_ptr;
      }

      T* operator->()
      {
           return _ptr;
      }
protected:
      T* _ptr;
      int* _pCount;
};


void TestSharedPtr()
{
      SharedPtr<int> sp1(new int(10));
      SharedPtr<int> sp2(sp1);
      sp1 = sp2;
}

shared_ptr使用注意事项:不能用下面的方式使得两个 shared_ptr 对象托管同一个指针

  1. A* p = new A(10);
  2. shared_ptr <A> sp1(p), sp2(p);

sp1 和 sp2 并不会共享同一个对 p 的托管计数,而是各自将对 p 的托管计数都记为 1(sp2 无法知道 p 已经被 sp1 托管过)。这样,当 sp1 消亡时要析构 p,sp2 消亡时要再次析构 p,这会导致程序崩溃。

还有一个就是循环引用的问题:

shared_ptr的主要优点是当不再使用时会自动释放相关的内存。
但是如果我们不仔细使用shared_ptr,那么这个优势就会变成一个劣势。 我们来看看:
假设我设计一个二叉树,并在其中包含一个指向左右子节点的指针。

#include <iostream>
#include <memory>
 
class Node {
  int value;
 public:
  std::shared_ptr<Node> leftPtr;
  std::shared_ptr<Node> rightPtr;
  Node(int val) : value(val) {
    std::cout << "Constructor" << std::endl;
  }
  ~Node() {
    std::cout << "Destructor" << std::endl;
  }
};
 
int main() {
  std::shared_ptr<Node> ptr = std::make_shared<Node>(4);
  ptr->leftPtr = std::make_shared<Node>(2);
  ptr->rightPtr = std::make_shared<Node>(5);
 
  return 0;
}
上面的例子运行正常。
调用3次构造函数和3次析构函数。这意味着完整的内存被删除。
但是,如果我们添加另一个小的需求,即每个节点将包含一个指向父节点的指针。 那么它会导致shared_ptr的问题。

查看修改过的代码

#include <iostream>
#include <memory>
 
class Node {
  int value;
 public:
  std::shared_ptr<Node> leftPtr;
  std::shared_ptr<Node> rightPtr;
  std::shared_ptr<Node> parentPtr;
  Node(int val) : value(val) {
    std::cout << "Constructor" << std::endl;
  }
  ~Node() {
    std::cout << "Destructor" << std::endl;
  }
};
 
int main() {
  std::shared_ptr<Node> ptr = std::make_shared<Node>(4);
  ptr->leftPtr = std::make_shared<Node>(2);
  ptr->leftPtr->parentPtr = ptr;
  ptr->rightPtr = std::make_shared<Node>(5);
  ptr->rightPtr->parentPtr = ptr;
  std::cout << "ptr reference count = " << ptr.use_count() << std::endl;
  std::cout << "ptr->leftPtr reference count = " << ptr->leftPtr.use_count() << std::endl;
  std::cout << "ptr->rightPtr reference count = " << ptr->rightPtr.use_count() << std::endl;
  return 0;
}
输出:
Constructor
Constructor
Constructor
ptr reference count = 1
ptr->leftPtr reference count = 1
ptr->rightPtr reference count = 1

现在构造函数会被调用3次,但是不会调用析构函数,这意味着内存泄漏。
导致这个shared_ptr问题的原因是循环引用,即:
如果两个对象使用shared_ptrs互相引用,那么当超出范围时,都不会删除内存。
发生这种情况是因为shared_ptr在其析构函数中递减关联内存检查的引用计数之后,如果count为0,则删除该内存,如果大于1,则意味着其他shared_ptr正在使用此内存。
但是在这种情况下,会发现这些shared_ptr在析构函数中count的值始终大于0。
让我们重新确认下上面的例子:
当ptr的析构函数被调用时,
·将引用计数减去1。
·然后检查当前计数是否为0,但是是2,因为左侧子元素和右侧子元素都具有引用父项的shared_ptr对象,即ptr。
·只有当ptr的内存被删除时,左右子节点才会被删除,但是由于引用计数大于0,这种情况不会发生。
·因此ptr和其子节点的内存都不会被删除。所以没有析构函数被调用。

那么,如何解决这个问题呢?
答案是使用weak_ptr
Weak_ptr允许共享,但不拥有一个对象。 它的对象是由shared_ptr创建的。
std::shared_ptr<int> ptr = std::make_shared<int>(4);
std::weak_ptr<int> weakPtr(ptr);weak_ptr<int>
对于weak_ptr对象,我们不能直接使用运算符*和 - >来访问关联的内存。首先,我们必须通过调用weak_ptr对象的的lock()函数来创建一个shared_ptr,这样只有我们可以使用它。
查看如下的例子
#include <iostream>
#include <memory>
 
int main() {
  std::shared_ptr<int> ptr = std::make_shared<int>(4);
  std::weak_ptr<int> weakPtr(ptr);
  std::shared_ptr<int> ptr_2 = weakPtr.lock();
  if (ptr_2)
    std::cout << (*ptr_2) << std::endl;
  std::cout << "Reference Count :: " << ptr_2.use_count() << std::endl;
  if (weakPtr.expired() == false)
    std::cout << "Not expired yet" << std::endl;
 
  return 0;
}
关键点:如果shared_ptr已经被删除,lock()会返回空的shared_ptr

使用weak_ptr改进我们的二叉树示例:
#include <iostream>
#include <memory>
 
class Node {
  int value;
 public:
  std::shared_ptr<Node> leftPtr;
  std::shared_ptr<Node> rightPtr;
  //只需要把shared_ptr改为weak_ptr;
  std::weak_ptr<Node> parentPtr;
  Node(int val) : value(val) {
    std::cout << "Constructor" << std::endl;
  }
  ~Node() {
    std::cout << "Destructor" << std::endl;
  }
};
 
int main() {
  std::shared_ptr<Node> ptr = std::make_shared<Node>(4);
  ptr->leftPtr = std::make_shared<Node>(2);
  ptr->leftPtr->parentPtr = ptr;
  ptr->rightPtr = std::make_shared<Node>(5);
  ptr->rightPtr->parentPtr = ptr;
  std::cout << "ptr reference count = " << ptr.use_count() << std::endl;
  std::cout << "ptr->leftPtr reference count = " << ptr->leftPtr.use_count() << std::endl;
  std::cout << "ptr->rightPtr reference count = " << ptr->rightPtr.use_count() << std::endl;
  return 0;


}
输出:
Constructor
Constructor
Constructor
ptr reference count = 1
ptr->leftPtr reference count = 1
ptr->rightPtr reference count = 1
Destructor
Destructor
Destructor
 

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值