Skip to content

15.7 std::shared_ptr 的循环依赖问题,介绍 std::weak_ptr

By Alex on March 21st, 2017 | last modified by Alex on January 23rd, 2020

翻译by dashjay 2020.07.18

在之前的课程中,我们看到了 std::shared_ptr 如何允许我们有多个职能指正共同拥有同样的资源,然而,在具体的情况下,这可能带来很多问题,思考如下例子,这时候两个对象中的智能指针分别指向了其他对象。

#include <iostream>
#include <memory> // for std::shared_ptr
#include <string>

class Person
{
 std::string m_name;
 std::shared_ptr<Person> m_partner; // initially created empty

public:

 Person(const std::string &name): m_name(name)
 {
  std::cout << m_name << " created\n";
 }
 ~Person()
 {
  std::cout << m_name << " destroyed\n";
 }

 friend bool partnerUp(std::shared_ptr<Person> &p1, std::shared_ptr<Person> &p2)
 {
  if (!p1 || !p2)
   return false;

  p1->m_partner = p2;
  p2->m_partner = p1;

  std::cout << p1->m_name << " is now partnered with " << p2->m_name << "\n";

  return true;
 }
};

int main()
{
 auto lucy = std::make_shared<Person>("Lucy"); // create a Person named "Lucy"
 auto ricky = std::make_shared<Person>("Ricky"); // create a Person named "Ricky"

 partnerUp(lucy, ricky); // Make "Lucy" point to "Ricky" and vice-versa

 return 0;
}

在上个面的例子中,我们使用 make_shared() 动态分配了两个 Persons,“Lucy” 和 “Ricky”(来保证他们在main()结束前被销毁)。紧接着,我们让他们成为搭档。这个操作设置 "Lucy" 的 std::shared_ptr 指向 “Ricky”,并且使得 “Ricky” 内的 std::shared_ptr指向 “Lucy”。共享指针被设计用来共享的,因此这样很棒,分别设置两个 Persion 为对方的搭档。

然而执行起来,这个程序没有按照期望执行:

Lucy created
Ricky created
Lucy is now partnered with Ricky

嗯,使得。没有销毁发生,发生了什么?

partnerUp() 被调用后,有两个只能指针指向了 “Ricky”,两个智能指针指向了 “Lucy”。

在函数的结尾,“ricky” 的共享指针先离开作用于。当那发生时,“ricky” 检查是否有任何其他的共享指针共用拥有 “Ricky”。确实有(“lucy‘s m_partner”)。因为这,它没有释放“Ricky”(如果它释放了,那么 “Lucy” 的 m_partner 就会变成悬空指针)。在这种情况下,我们现在有一个只能指针指向 “Ricky”(“Lucy” 的 m_partner)和两个只能指针指向 “Lucy”(变量 lucy 和 “Ricky” 的 m_partner)。

紧接着 lucy 的共享指针离开作用域,并且同样的事情发生了,智能指针 lucy 检查是否有其他智能指正共同拥有 “Lucy”,确实存在(“Ricky” 的 m_partner),因此 “Lucy” 不会被释放。在这种情况下,有一个智能指针指向 “Lucy”(“Ricky” 的 m_partner),和一个智能指针指向 “Ricky”(“Lucy” 的 m_partner)。

紧接着,程序结束 ———— “Lucy” 或 “Ricky” 都没有被释放!本质上 “Lucy” 最后保证了 “Ricky” 不被销毁,而且 “Ricky” 最后保证了 “Lucy” 不被销毁。

It turns out that this can happen any time shared pointers form a circular reference. 这会发生在任何时候,只要智能指针出现循环引用。

循环依赖

循环依赖(也叫 cyclical reference)是一系列引用中,每一个对象引用了下一个,而最后一个对象引用了第一个,造成循环。这个引用不需要是确切的 C++ 引用 ———— 它可以是指针,唯一的ID,或者任何特殊对象。

在智能指针(shared pointers)这种情况下,循环引用就会指针引起的。

这正好发生在以上的情况下:“Lucy” 指向了 “Ricky”,然后 “Ricky” 指向了 “Lucy”。如果有三个指针,你会得到相似的结果就是 A指向B,B指向C,C指向A。在环中,智能指针的效果依然是保证下一个对象的存活 ———— 最后一个对象保证了第一个对象的存活,因此,没有对象会被释放,因为他们都任为其他对象仍然需要它!

一个 reductive 的情况

事实证明,循环引用的问题甚至在只有一个 std::shared_ptr 的情况下成立。 ———— 一个 std::shared_ptr 引用的对象包含了一个循环。尽管这在实际情况下很不常见,我们将会展示给你,为了进一步理解:

#include <iostream>
#include <memory> // for std::shared_ptr

class Resource
{
public:
 std::shared_ptr<Resource> m_ptr; // initially created empty

 Resource() { std::cout << "Resource acquired\n"; }
 ~Resource() { std::cout << "Resource destroyed\n"; }
};

int main()
{
 auto ptr1 = std::make_shared<Resource>();

 ptr1->m_ptr = ptr1; // m_ptr is now sharing the Resource that contains it

 return 0;
}

在上方的例子中,当 ptr1 离开作用于时,他不会释放 Resource 因为 Rresource 的 m_ptr 正在共享 Resource。紧接着没有谁会被删除 (m_ptr 从未离开作用于,所以它没有机会被删除)。因此,这个程序输出:

Resource acquired

就是这样

std::weak_ptr 用来做什么

std::weak_ptr 就是用来解决上方描述的循环依赖的问题。一个 “std::waek_ptr” 是一个观察者 ———— 它可以访问和同样的对象就像 std::shared_ptr 那样,但它不被认为是一个拥有者。记住,当一个智能指针离开作用域,它仅仅思考是否有其他的 std::shared_ptr 共同持有这个对象,std::weak_ptr 不算数!

让我们使用 weak_ptr 来解决之前的 Persion 问题:

#include <iostream>
#include <memory> // for std::shared_ptr and std::weak_ptr
#include <string>

class Person
{
 std::string m_name;
 std::weak_ptr<Person> m_partner; // note: This is now a std::weak_ptr

public:

 Person(const std::string &name): m_name(name)
 {
  std::cout << m_name << " created\n";
 }
 ~Person()
 {
  std::cout << m_name << " destroyed\n";
 }

 friend bool partnerUp(std::shared_ptr<Person> &p1, std::shared_ptr<Person> &p2)
 {
  if (!p1 || !p2)
   return false;

  p1->m_partner = p2;
  p2->m_partner = p1;

  std::cout << p1->m_name << " is now partnered with " << p2->m_name << "\n";

  return true;
 }
};

int main()
{
 auto lucy = std::make_shared<Person>("Lucy");
 auto ricky = std::make_shared<Person>("Ricky");

 partnerUp(lucy, ricky);

 return 0;
}

这段代码表现正常:

Lucy created
Ricky created
Lucy is now partnered with Ricky
Ricky destroyed
Lucy destroyed

从功能上来讲,它同有问题的例子工作起来几乎一样。然而,现在当 ricky 离开作用域时,它检查发现没有其他 std::shared_ptr 指向了 “Ricky”(“Lucy” 的std::weak_ptr 不算)。因此,它会释放 “Ricky”。同样的情况发生在 lucy 上。

使用 std::weak_ptr

std::weak_ptr 的缺点是 std::weak_ptr 不能直接被使用(它没有 -> 操作符)。要使用 std::weak_ptr 你必须先转化它成为一个 std::shared_ptr。要完成转化,你可以使用 lock() 成员函数,这是之前的例子经过更新:

#include <iostream>
#include <memory> // for std::shared_ptr and std::weak_ptr
#include <string>

class Person
{
 std::string m_name;
 std::weak_ptr<Person> m_partner; // note: This is now a std::weak_ptr

public:

 Person(const std::string &name) : m_name(name)
 {
  std::cout << m_name << " created\n";
 }
 ~Person()
 {
  std::cout << m_name << " destroyed\n";
 }

 friend bool partnerUp(std::shared_ptr<Person> &p1, std::shared_ptr<Person> &p2)
 {
  if (!p1 || !p2)
   return false;

  p1->m_partner = p2;
  p2->m_partner = p1;

  std::cout << p1->m_name << " is now partnered with " << p2->m_name << "\n";

  return true;
 }

 const std::shared_ptr<Person> getPartner() const { return m_partner.lock(); } // use lock() to convert weak_ptr to shared_ptr
 const std::string& getName() const { return m_name; }
};

int main()
{
 auto lucy = std::make_shared<Person>("Lucy");
 auto ricky = std::make_shared<Person>("Ricky");

 partnerUp(lucy, ricky);

 auto partner = ricky->getPartner(); // get shared_ptr to Ricky's partner
 std::cout << ricky->getName() << "'s partner is: " << partner->getName() << '\n';

 return 0;
}

输出:

Lucy created
Ricky created
Lucy is now partnered with Ricky
Ricky's partner is: Lucy
Ricky destroyed
Lucy destroyed

在使用 partner 变量的时候,我们无需关心循环引,因为他就是一个函数内的局部变量。他会最终离开作用域,然后引用数量会减少1.

结论

当你需要多个智能指针管理同一个资源时,可以用std::shared_ptr 。资源将会在最后一个 std::shared_ptr离开作用域时销毁。

当你需要一个只能指针来访问另一个智能指针时,可以用std::weak_ptr ,但是不会得到资源的所有权