欢迎阅读本系列文章的第二篇,我们将继续探讨与 shared_ptr
相关的主题。上一篇文章我们介绍了 shared_ptr
的强大功能,但也提到了它可能面临的一个问题 —— 循环引用。当两个或多个对象之间相互持有 shared_ptr
的引用时,就会形成循环引用,导致这些对象无法被正确释放,从而引发内存泄漏。
在本文中,我们将深入讨论循环引用问题,并引入另一个智能指针类——weak_ptr
。weak_ptr
是 shared_ptr
的伙伴,它可以帮助我们解决循环引用问题,并且不会增加引用计数,以避免对象无法释放的情况。
通过学习 shared_ptr
和 weak_ptr
的组合使用,我们将能够更好地管理动态分配的对象,避免内存泄漏,并提高代码的健壮性和可维护性。敬请期待本文的剖析和示例,希望能给您带来更深入的了解和实践经验。
当使用 std::shared_ptr
时,循环引用是一种常见的问题。循环引用指的是两个或多个对象彼此持有 shared_ptr
的引用,形成一个环状依赖关系。这种情况下,即使没有外部引用指向这些对象,它们的引用计数也无法降为零,从而导致内存泄漏。
循环引用可能会导致内存泄漏的发生,因为每个对象都会持有对其他对象的引用,导致它们的引用计数无法归零。当没有外部引用指向这些对象时,它们的析构函数不会被调用,从而导致资源无法正确释放。
首先我们来看一段代码,这段代码就明显存在着循环引用。
struct ListNode
{
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode(){ cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
✅循环引用分析:
node1
和node2
两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete
。node1
的_next
指向node2
,node2
的_prev
指向node1
,引用计数变成2。node1
和node2
析构,引用计数减到1,但是_next
还指向下一个节点。但是_prev
还指向上一个节点。_next
析构了,node2
就释放了。_prev
析构了,node1
就释放了。_next
属于node
的成员,node1
释放了,_next
才会析构,而node1
由_prev
管理,_prev
属于node2
成员,所以这就叫循环引用,谁也不会释放。为了解决循环引用问题,可以使用 std::weak_ptr
。std::weak_ptr
是一种弱引用,它可以指向 std::shared_ptr
持有的对象,但不会增加对象的引用计数。这样,即使存在循环引用,通过使用 std::weak_ptr
可以打破循环引用,使对象的引用计数能够正确降为零,从而触发析构函数的调用。
std::weak_ptr
是 C++11 标准库中提供的一种弱引用智能指针,它可以指向 std::shared_ptr
所管理的对象,但不会增加对象的引用计数。因此,当使用 std::weak_ptr
时,如果 std::shared_ptr
对象被释放或者过期,std::weak_ptr
将自动失效,避免了循环引用导致的内存泄漏问题。
std::weak_ptr官方文档
和 shared_ptr
、unique_ptr
相比,weak_ptr
模板类提供的成员方法不多,下表罗列了常用的成员方法及各自的功能。
成员方法 | 功能 |
---|---|
operator= | 重载 = 赋值运算符,使得 std::weak_ptr 指针可以直接被 std::weak_ptr 或者 std::shared_ptr 类型指针赋值。 |
swap(x) | 其中 x 表示一个同类型的 std::weak_ptr 类型指针,该函数可以互换两个同类型 std::weak_ptr 指针的内容。 |
reset() | 将当前 std::weak_ptr 指针置为空指针。 |
use_count() | 查看指向和当前 std::weak_ptr 指针相同的 std::shared_ptr 指针的数量。 |
expired() | 判断当前 std::weak_ptr 指针是否过期(指针为空,或者指向的堆内存已经被释放)。 |
lock() | 如果当前 std::weak_ptr 已经过期,则该函数会返回一个空的 std::shared_ptr 指针;反之,该函数返回一个和当前 std::weak_ptr 指向相同的 std::shared_ptr 指针。 |
注意:weak_ptr
模板类没有重载 *
和 ->
运算符,因此 weak_ptr
类型指针只能访问某一 shared_ptr
指针指向的堆内存空间,无法对其进行修改。
创建 std::weak_ptr
指针的方式和创建 std::shared_ptr
的方式类似。下面列举了三种常见的创建 std::weak_ptr
的方式:
std::shared_ptr
创建可以通过将 std::shared_ptr
赋值给 std::weak_ptr
来创建一个弱引用指针,例如:
std::shared_ptr<int> sptr = std::make_shared<int>(42);
std::weak_ptr<int> wptr(sptr);
上述代码中,我们首先创建了一个 std::shared_ptr
对象 sptr
,它指向一个动态分配的 int
类型对象。然后,我们将 sptr
赋值给 std::weak_ptr
对象 wptr
,创建了一个弱引用指针。
std::shared_ptr
转换可以通过 std::shared_ptr
的 weak_ptr
成员函数,将 std::shared_ptr
转换为 std::weak_ptr
,例如:
std::shared_ptr<int> sptr = std::make_shared<int>(42);
std::weak_ptr<int> wptr = sptr->weak_ptr();
上述代码中,我们首先创建了一个 std::shared_ptr
对象 sptr
,它指向一个动态分配的 int
类型对象。然后,我们调用 sptr
的 weak_ptr()
成员函数,将 sptr
转换为 std::weak_ptr
对象 wptr
,创建了一个弱引用指针。
std::weak_ptr
的构造函数可以直接使用 std::weak_ptr
的构造函数,创建一个空的弱引用指针,例如:
std::weak_ptr<int> wptr;
上述代码中,我们直接创建了一个空的 std::weak_ptr
对象 wptr
,它不持有任何对象的引用。
使用 std::weak_ptr
修改上面的代码,可以将 _prev
和 _next
成员变量改为 std::weak_ptr
类型。这样可以避免循环引用,同时仍然可以访问链表中的前一个节点和后一个节点
struct ListNode
{
int _data;
weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;
~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
// 使用 weak_ptr.lock() 获取 shared_ptr 对象
shared_ptr<ListNode> node1Next = node1->_next.lock();
shared_ptr<ListNode> node2Prev = node2->_prev.lock();
if (node1Next)
cout << "node1 next data: " << node1Next->_data << endl;
else
cout << "node1 next is nullptr" << endl;
if (node2Prev)
cout << "node2 prev data: " << node2Prev->_data << endl;
else
cout << "node2 prev is nullptr" << endl;
return 0;
}
运行上述代码,可以得到如下输出:
1
1
2
2
node1 next data: 0
node2 prev data: 0
在上面的代码示例中,我们创建了两个节点 node1
和 node2
,并通过 std::weak_ptr
进行相互引用。
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
接下来,我们设置节点之间的关系。修改前的代码如下:
node1->_next = node2;
node2->_prev = node1;
我们将 _next
和 _prev
成员变量的类型从 shared_ptr
改为 weak_ptr
:
weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;
这样就建立了节点之间的弱引用关系,避免了循环引用。
接下来,我们可以通过 lock()
方法将 std::weak_ptr
转换为 std::shared_ptr
,以访问所管理的对象。
shared_ptr<ListNode> node1Next = node1->_next.lock();
shared_ptr<ListNode> node2Prev = node2->_prev.lock();
如果 std::weak_ptr
不过期(即所管理的对象还存在),lock()
方法会返回一个有效的 std::shared_ptr
对象,否则返回空指针。
最后,我们可以使用这些 std::shared_ptr
对象来访问链表中的前驱和后继节点的数据。
if (node1Next)
cout << "node1 next data: " << node1Next->_data << endl;
else
cout << "node1 next is nullptr" << endl;
if (node2Prev)
cout << "node2 prev data: " << node2Prev->_data << endl;
else
cout << "node2 prev is nullptr" << endl;
通过这种方式,我们可以安全地访问链表中的前驱和后继节点,而不会导致循环引用和内存泄漏。
template<class T>
class weak_ptr
{
public:
// 默认构造函数,将_ptr成员指针初始化为nullptr
weak_ptr()
:_ptr(nullptr)
{}
// 接受shared_ptr参数的构造函数,将_ptr成员指针初始化为shared_ptr所管理对象的指针
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
// 重载*运算符,返回所管理对象的引用
T& operator*()
{
return *_ptr;
}
// 重载->运算符,返回所管理对象的指针
T* operator->()
{
return _ptr;
}
// 返回所管理对象的指针
T* get()
{
return _ptr;
}
private:
T* _ptr; // 所管理对象的指针
};
这段代码是一个简化版的 weak_ptr
类的实现,提供了一些基本的功能。
首先,我们可以看到 weak_ptr
类有一个默认构造函数和一个接受 shared_ptr
参数的构造函数。默认构造函数将 _ptr
成员指针初始化为 nullptr
,而接受 shared_ptr
参数的构造函数将 _ptr
成员指针初始化为 shared_ptr
所管理对象的指针。
接下来,我们可以看到 weak_ptr
重载了 *
和 ->
运算符,使得可以像使用指针一样,通过 weak_ptr
对象访问所管理的对象。operator*()
返回所管理对象的引用,operator->()
返回所管理对象的指针。
同时,weak_ptr
还提供了一个 get()
方法,返回 _ptr
指针,即所管理对象的指针。这允许用户直接访问 _ptr
指针,但需要注意,这种直接访问可能会导致悬空指针问题,因为 _ptr
指针可能已经无效(所管理对象已被释放)。
注意:这个简化版的 weak_ptr
实现没有考虑线程安全性。在实际应用中,weak_ptr
需要与其他智能指针共同使用,比如 shared_ptr
、unique_ptr
,并且需要考虑线程安全性和异常安全性。
感谢您对博主文章的关注与支持!另外,我计划在未来的更新中持续探讨与本文相关的内容,会为您带来更多关于C++以及编程技术问题的深入解析、应用案例和趣味玩法等。请继续关注博主的更新,不要错过任何精彩内容!