【C++入门到精通】智能指针 shared_ptr循环引用 | weak_ptr 简介及C++模拟实现 [ C++入门 ]

简介: 【C++入门到精通】智能指针 shared_ptr循环引用 | weak_ptr 简介及C++模拟实现 [ C++入门 ]

引言

欢迎阅读本系列文章的第二篇,我们将继续探讨与 shared_ptr 相关的主题。上一篇文章我们介绍了 shared_ptr 的强大功能,但也提到了它可能面临的一个问题 —— 循环引用。当两个或多个对象之间相互持有 shared_ptr 的引用时,就会形成循环引用,导致这些对象无法被正确释放,从而引发内存泄漏。

在本文中,我们将深入讨论循环引用问题,并引入另一个智能指针类——weak_ptr。weak_ptr 是 shared_ptr 的伙伴,它可以帮助我们解决循环引用问题,并且不会增加引用计数,以避免对象无法释放的情况。

通过学习 shared_ptr 和 weak_ptr 的组合使用,我们将能够更好地管理动态分配的对象,避免内存泄漏,并提高代码的健壮性和可维护性。敬请期待本文的剖析和示例,希望能给您带来更深入的了解和实践经验。

一、std::shared_ptr的循环引用

1. 概念

当使用 std::shared_ptr 时,循环引用是一种常见的问题。循环引用指的是两个或多个对象彼此持有 shared_ptr 的引用,形成一个环状依赖关系。这种情况下,即使没有外部引用指向这些对象,它们的引用计数也无法降为零,从而导致内存泄漏。

循环引用可能会导致内存泄漏的发生,因为每个对象都会持有对其他对象的引用,导致它们的引用计数无法归零。当没有外部引用指向这些对象时,它们的析构函数不会被调用,从而导致资源无法正确释放。

2. 示例分析

首先我们来看一段代码,这段代码就明显存在着循环引用。

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;
}

循环引用分析

  1. node1node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete
  2. node1_next指向node2node2_prev指向node1,引用计数变成2。
  3. node1node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
  4. 也就是说_next析构了,node2就释放了
  5. 也就是说_prev析构了,node1就释放了
  6. 但是_next属于node的成员,node1释放了,_next才会析构,而node1_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放

⭕让我们通过下面这个图片来说上面这个问题:

为了解决循环引用问题,可以使用 std::weak_ptr。std::weak_ptr 是一种弱引用,它可以指向 std::shared_ptr 持有的对象,但不会增加对象的引用计数。这样,即使存在循环引用,通过使用 std::weak_ptr 可以打破循环引用,使对象的引用计数能够正确降为零,从而触发析构函数的调用。

二、std::weak_ptr

1. 简介

std::weak_ptr 是 C++11 标准库中提供的一种弱引用智能指针,它可以指向 std::shared_ptr 所管理的对象,但不会增加对象的引用计数。因此,当使用 std::weak_ptr 时,如果 std::shared_ptr 对象被释放或者过期,std::weak_ptr 将自动失效,避免了循环引用导致的内存泄漏问题。

🔴std::weak_ptr官方文档

2. weak_ptr模板类提供的成员方法

shared_ptr<T>unique_ptr<T> 相比,weak_ptr<T> 模板类提供的成员方法不多,下表罗列了常用的成员方法及各自的功能。

成员方法 功能
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<T> 模板类没有重载 *-> 运算符,因此 weak_ptr 类型指针只能访问某一 shared_ptr 指针指向的堆内存空间,无法对其进行修改

3. 使用示例

(1)weak_ptr指针的创建

创建 std::weak_ptr 指针的方式和创建 std::shared_ptr 的方式类似。下面列举了三种常见的创建 std::weak_ptr 的方式:

  1. 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,创建了一个弱引用指针。

  1. std::shared_ptr 转换

可以通过 std::shared_ptrweak_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,创建了一个弱引用指针。

  1. 使用 std::weak_ptr 的构造函数

可以直接使用 std::weak_ptr 的构造函数,创建一个空的弱引用指针,例如:

std::weak_ptr<int> wptr;

上述代码中,我们直接创建了一个空的 std::weak_ptr 对象 wptr,它不持有任何对象的引用。

(2)完整示例(解决上面循环引用问题)

使用 std::weak_ptr 修改上面的代码,可以将 _prev_next 成员变量改为 std::weak_ptr<ListNode> 类型。这样可以避免循环引用,同时仍然可以访问链表中的前一个节点和后一个节点

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

在上面的代码示例中,我们创建了两个节点 node1node2,并通过 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<ListNode> 改为 weak_ptr<ListNode>

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;

通过这种方式,我们可以安全地访问链表中的前驱和后继节点,而不会导致循环引用和内存泄漏。

4. C++模拟实现

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_ptrunique_ptr,并且需要考虑线程安全性和异常安全性

温馨提示

感谢您对博主文章的关注与支持!另外,我计划在未来的更新中持续探讨与本文相关的内容,会为您带来更多关于C++以及编程技术问题的深入解析、应用案例和趣味玩法等。请继续关注博主的更新,不要错过任何精彩内容!

再次感谢您的支持和关注。期待与您建立更紧密的互动,共同探索C++、算法和编程的奥秘。祝您生活愉快,排便顺畅!


目录
相关文章
|
7月前
|
存储 算法
算法入门:专题一:双指针(有效三角形的个数)
给定一个数组,找出能组成三角形的三元组个数。利用“两边之和大于第三边”的性质,先排序,再用双指针优化。固定最大边,左右指针从区间两端向内移动,若两短边之和大于最长边,则中间所有组合均有效,时间复杂度由暴力的O(n³)降至O(n²)。
|
11月前
|
存储 安全 编译器
c++入门
c++作为面向对象的语言与c的简单区别:c语言作为面向过程的语言还是跟c++有很大的区别的,比如说一个简单的五子棋的实现对于c语言面向过程的设计思路是首先分析解决这个问题的步骤:(1)开始游戏(2)黑子先走(3)绘制画面(4)判断输赢(5)轮到白子(6)绘制画面(7)判断输赢(8)返回步骤(2) (9)输出最后结果。但对于c++就不一样了,在下五子棋的例子中,用面向对象的方法来解决的话,首先将整个五子棋游戏分为三个对象:(1)黑白双方,这两方的行为是一样的。(2)棋盘系统,负责绘制画面。
155 0
|
存储 缓存 C++
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
C++ 标准模板库(STL)提供了一组功能强大的容器类,用于存储和操作数据集合。不同的容器具有独特的特性和应用场景,因此选择合适的容器对于程序的性能和代码的可读性至关重要。对于刚接触 C++ 的开发者来说,了解这些容器的基础知识以及它们的特点是迈向高效编程的重要一步。本文将详细介绍 C++ 常用的容器,包括序列容器(`std::vector`、`std::array`、`std::list`、`std::deque`)、关联容器(`std::set`、`std::map`)和无序容器(`std::unordered_set`、`std::unordered_map`),全面解析它们的特点、用法
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
|
存储 分布式计算 编译器
C++入门基础2
本内容主要讲解C++中的引用、inline函数和nullptr。引用是变量的别名,与原变量共享内存,定义时需初始化且不可更改指向对象,适用于传参和返回值以提高效率;const引用可增强代码灵活性。Inline函数通过展开提高效率,但是否展开由编译器决定,不建议分离声明与定义。Nullptr用于指针赋空,取代C语言中的NULL。最后鼓励持续学习,精进技能,提升竞争力。
|
存储 NoSQL 编译器
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
指针是一个变量,它存储另一个变量的内存地址。换句话说,指针“指向”存储在内存中的某个数据。
532 7
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
|
存储 安全 编译器
【C++打怪之路Lv1】-- 入门二级
【C++打怪之路Lv1】-- 入门二级
170 0
|
自然语言处理 编译器 C语言
【C++打怪之路Lv1】-- C++开篇(入门)
【C++打怪之路Lv1】-- C++开篇(入门)
240 0
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
498 12