22-7 std::shared_ptr 与 std::weak_ptr 的循环依赖问题
在上节课中,我们了解到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() 动态分配了两个 Person 对象“Lucy”和“Ricky”(确保 lucy 和 ricky 在 main() 结束时被销毁)。随后将它们配对。这使得“Lucy”内部的std::shared_ptr指向“Ricky”,而“Ricky”内部的std::shared_ptr指向“Lucy”。共享指针本就用于共享,因此Lucy的共享指针和Ricky的m_partner共享指针同时指向“Lucy”(反之亦然)是完全正常的。
然而该程序运行结果与预期不符:

就这样。没有发生任何内存释放。噢,不妙。发生了什么?
在调用partnerUp()之后,存在两个指向“Ricky”的共享指针(ricky和Lucy的m_partner),以及两个指向“Lucy”的共享指针(lucy和Ricky的m_partner)。
在main()结束时,ricky共享指针首先超出作用域。此时ricky会检查是否存在其他共享指针共同拥有“Ricky”对象(确实存在Lucy的m_partner)。因此它不会释放“Ricky”(否则Lucy的m_partner将变成悬空指针)。此时,我们拥有一个指向“Ricky”的共享指针(Lucy的m_partner)和两个指向“Lucy”的共享指针(lucy和Ricky的m_partner)。
接着lucy共享指针退出作用域,同样的情况再次发生。共享指针lucy检查是否存在其他共享指针共同拥有Person“Lucy”。存在(Ricky的m_partner),因此“Lucy”未被释放。此时存在指向“Lucy”的共享指针(Ricky的m_partner)和指向“Ricky”的共享指针(Lucy的m_partner)。
随后程序结束——但Person类对象“Lucy”和“Ricky”均未被释放!本质上,“Lucy”阻止了“Ricky”的销毁,而“Ricky”同样阻止了“Lucy”的销毁。
这种情况在共享指针形成循环引用时随时可能发生。
循环引用
循环引用Circular reference(也称为周期性引用cyclical reference或循环cycle)是一系列相互引用的对象,其中每个对象引用下一个对象,而最后一个对象又回指第一个对象,从而形成引用循环。这些引用不必是实际的C++引用——它们可以是指针、唯一标识符或其他任何用于识别特定对象的机制。
在共享指针的语境中,这些引用即为指针。
这正是上文案例的体现:“Lucy”指向“Ricky”,而“Ricky”指向“Lucy”。若使用三个指针,当A指向B、B指向C、C指向A时,同样会形成循环。共享指针形成循环的实际效果是:每个对象都维持着下一个对象的生命周期——最后一个对象维持着第一个对象的生命周期。因此,该系列中的任何对象都无法被释放,因为它们都认为其他对象仍需要它!
一种可还原的情况
事实证明,这种循环引用问题甚至可能发生在单个 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作用域结束时,资源并未被释放,因为该资源的m_ptr正共享着该资源。此时释放资源的唯一方式是将m_ptr赋值为其他对象(使资源不再被共享)。但由于ptr1已超出作用域,我们无法访问m_ptr,因此再也无法执行此操作。该Resource已演变为内存泄漏。
因此程序输出:

就这样。
那么std::weak_ptr究竟有什么用?
std::weak_ptr的设计初衷正是为了解决上述“循环引用”问题。它本质上是观察者——能像std::shared_ptr(或其他std::weak_ptr)一样观察和访问同一对象,但不被视为所有者。请记住:当 std::shared_ptr 作用域结束时,它仅会检查其他 std::shared_ptr 是否共同拥有该对象。而 std::weak_ptr 不会被计入!
现在让我们用 std::weak_ptr 解决这个人物关系问题:
#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;
}
这段代码运行正常:

功能上,它与存在问题的示例几乎完全相同。然而,当 ricky 作用域结束时,系统会检测到没有其他 std::shared_ptr 指向“Ricky”(来自“Lucy”的 std::weak_ptr 不计入其中)。因此,它将释放“Ricky”的内存。Lucy 的情况也是如此。
使用 std::weak_ptr
std::weak_ptr 的一个缺点是无法直接使用(它们没有 operator-> 运算符)。要使用 std::weak_ptr,必须先将其转换为 std::shared_ptr,然后才能使用 std::shared_ptr。可通过 lock() 成员函数将 std::weak_ptr 转换为 std::shared_ptr。以下是更新后的示例代码,演示该转换过程:
#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;
}
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;
}
这将输出:

我们无需担心std::shared_ptr变量“partner”会引发循环依赖,因为它只是函数内的局部变量。该变量最终会在函数结束时超出作用域,其引用计数也会相应减少1。
使用 std::weak_ptr 避免悬空指针
假设一个普通的“笨指针”持有某个对象的地址,随后该对象被销毁。此时该指针便成为悬空指针,对其解引用将导致未定义行为。遗憾的是,我们无法判断持有非空地址的指针是否悬空。这正是普通指针危险性的重要根源。
由于 std::weak_ptr 不会强制保留所引用的资源,同样可能出现 std::weak_ptr 指向已被 std::shared_ptr 释放的资源的情况。然而,std::weak_ptr 暗藏妙招——由于它能访问对象的引用计数,因此可判断所指向对象是否有效!若引用计数非零,则资源仍有效;若引用计数为零,则资源已被销毁。
检测 std::weak_ptr 有效性的最简便方式是调用 expired() 成员函数:若指针指向无效对象则返回 true,否则返回 false。
以下示例清晰展示了这种行为差异:
// h/t to reader Waldo for an early version of this example
#include <iostream>
#include <memory>
class Resource
{
public:
Resource() { std::cerr << "Resource acquired\n"; }
~Resource() { std::cerr << "Resource destroyed\n"; }
};
// Returns a std::weak_ptr to an invalid object
std::weak_ptr<Resource> getWeakPtr()
{
auto ptr{ std::make_shared<Resource>() };
return std::weak_ptr<Resource>{ ptr };
} // ptr goes out of scope, Resource destroyed
// Returns a dumb pointer to an invalid object
Resource* getDumbPtr()
{
auto ptr{ std::make_unique<Resource>() };
return ptr.get();
} // ptr goes out of scope, Resource destroyed
int main()
{
auto dumb{ getDumbPtr() };
std::cout << "Our dumb ptr is: " << ((dumb == nullptr) ? "nullptr\n" : "non-null\n");
auto weak{ getWeakPtr() };
std::cout << "Our weak ptr is: " << ((weak.expired()) ? "expired\n" : "valid\n");
return 0;
}
这将输出:

getDumbPtr() 和 getWeakPtr() 都使用智能指针分配资源——该智能指针确保分配的资源将在函数结束时被销毁。当 getDumbPtr() 返回 Resource* 时,它会返回一个悬空指针(因为 std::unique_ptr 在函数结束时销毁了该资源)。当 getWeakPtr() 返回 std::weak_ptr 时,该弱指针同样指向无效对象(因为 std::shared_ptr 会在函数结束时销毁 Resource)。
在 main() 中,我们首先检测返回的普通指针是否为 nullptr。由于该指针仍持有已释放资源的地址,此检测将失败。main() 无法判断该指针是否悬空。在此情况下,由于指针已悬空,若尝试解引用将导致未定义行为。
接着我们检测 weak.expired() 是否为 true。由于 weak 所指向对象的引用计数为 0(因被引用的对象已被销毁),此检测结果为 true。主函数中的代码由此可判定 weak 指向无效对象,从而实现代码的条件化处理!
需注意:若 std::weak_ptr 已过期,则不应调用其 lock() 方法,因为被引用的对象已遭销毁,不存在可共享的对象。若强行对过期 std::weak_ptr 调用 lock(),将返回指向 nullptr 的 std::shared_ptr。
结论
当需要多个智能指针共同拥有资源时,可使用 std::shared_ptr。当最后一个 std::shared_ptr 脱离作用域时,资源将被释放。当需要智能指针查看并使用共享资源,但不参与该资源所有权时,可使用 std::weak_ptr。
测验时间
问题 #1
请修正“简化案例”部分中的程序,确保资源被正确释放。请勿修改 main() 中的代码。
为方便参考,程序代码如下:
#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;
}
显示答案:
#include <iostream>
#include <memory> // for std::shared_ptr and std::weak_ptr
class Resource
{
public:
std::weak_ptr<Resource> m_ptr {}; // use std::weak_ptr so m_ptr doesn't keep the Resource alive
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;
}

浙公网安备 33010602011771号