天天看點

C++11智能指針(五):shared_ptr的循環引用的問題及weak_ptr

shared_ptr的主要優點是當不再使用時會自動釋放相關的記憶體。

但是如果我們不仔細使用shared_ptr,那麼這個優勢就會變成一個劣勢。 我們來看看:

假設我設計一個二叉樹,并在其中包含一個指向左右子節點的指針。

C++11智能指針(五):shared_ptr的循環引用的問題及weak_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