C++ 智能指针weak_ptr用途浅析

蹲街弑〆低调 提交于 2019-12-21 05:06:52

C++ 智能指针weak_ptr用途浅析

我们知道C++智能指针有shared_ptrunique_ptr,这两种指针基本就可以胜任堆内存的管理了,那么C++为什么还要提出weak_ptr呢?

weak_ptr这个东西到底有什么用途呢?

1. weak_ptr的特性

weak_ptr也是指向shared_ptr指向的对象,但是并不管理引用计数和内存,操作如下:

在这里插入图片描述

所以,如果要使用weak_ptr,必须锁定为shared_ptr

shared_ptr<element_type> lock() const noexcept;

从上面我们也可以知道,weak_ptr的初始化也是从shared_ptr来的,如下:

//default (1)	
constexpr weak_ptr() noexcept;

//copy (2)	
weak_ptr (const weak_ptr& x) noexcept;
template <class U> weak_ptr (const weak_ptr<U>& x) noexcept;

//from shared_ptr (3)	
template <class U> weak_ptr (const shared_ptr<U>& x) noexcept;

从这里来看,weak_ptr完全就是一个shared_ptr鸡肋功能啊,一点用的没有。

真是这样的吗?我们看一下下面分析。

2. 二叉树的实现

2.1 问题

这里我们实现一个简单的二叉树,就三个节点,根,左子树,右子树(二叉树是一种非常查用的数据结构,这里使用这个例子,说明在我们实际情况中,这种场景其实非常多)。

struct Node
{
	std::shared_ptr<Node> Parent;
	std::shared_ptr<Node> LeftChild;
	std::shared_ptr<Node> RightChild;
	int Data;
	//
	Node(int d) : Data(d) {}
	~Node()
	{
		std::cout << "~Node() called" << std::endl;
	}
};

void Tree()
{
	std::shared_ptr<Node> Root = std::make_shared<Node>(200);
	std::shared_ptr<Node> Left = std::make_shared<Node>(100);
	std::shared_ptr<Node> Right = std::make_shared<Node>(300);

	Root->LeftChild = Left;
	Root->RightChild = Right;

	Left->Parent = Root;
	Right->Parent = Root;
}

如果我们运行这段代码可以发现,没有任何输出,析构函数并没有被调用?内存就这样被泄露了?我们来分析一下这段代码:

在这里插入图片描述

  1. std::shared_ptr<Node> Root = std::make_shared<Node>(200); : Root的引用计数为1.
  2. std::shared_ptr<Node> Left = std::make_shared<Node>(100); : Left引用计数为1.
  3. std::shared_ptr<Node> Right = std::make_shared<Node>(300); : Right 引用计数为1.
  4. Root->LeftChild = Left; : Left引用计数为2.
  5. Root->RightChild = Right; : Right引用计数为2.
  6. Left->Parent = Root; : Root引用计数为2.
  7. Right->Parent = Root; : Root引用计数为3.

离开作用域的时候:

  1. Root被释放,引用计数减1 为2.
  2. Left被释放,引用计数为1.
  3. Right被释放,引用计数为1.

也就是说,所有的引用计数都不为0,没有任何对象被释放;这个也就是典型的环形引用;也就是说,shared_ptr在环形引用中会导致引用计数循环使用

解决办法两种。

2.2 方案1

void Tree()
{
	std::shared_ptr<Node> Root = std::make_shared<Node>(200);
	std::shared_ptr<Node> Left = std::make_shared<Node>(100);
	std::shared_ptr<Node> Right = std::make_shared<Node>(300);

	Root->LeftChild = Left;
	Root->RightChild = Right;

	Left->Parent = Root;
	Right->Parent = Root;

	Root->LeftChild.reset();
	Root->RightChild.reset();

	Left->Parent.reset();
	Right->Parent.reset();
}

此时执行结果:

~Node() called
~Node() called
~Node() called

这种办法很明确,Root快要释放的时候,左右的清理,同样左右子树也一样,不过这种方法也太扯淡了,下面提供另外一种办法。

2.3 方案2

我们可以这么理解,对于一个节点来说,只要不是根,那么父节点应该是存在的,子节点就不一定,所以我们可以将子节点声明为弱引用:

struct Node
{
	std::shared_ptr<Node> Parent;
	std::weak_ptr<Node> LeftChild;
	std::weak_ptr<Node> RightChild;
	int Data;

	//
	Node(int d) : Data(d) {}
	~Node()
	{
		std::cout << "~Node() called" << std::endl;
	}
};

void Tree()
{
	std::shared_ptr<Node> Root = std::make_shared<Node>(200);
	std::shared_ptr<Node> Left = std::make_shared<Node>(100);
	std::shared_ptr<Node> Right = std::make_shared<Node>(300);

	Root->LeftChild = Left;
	Root->RightChild = Right;

	Left->Parent = Root;
	Right->Parent = Root;
}

此时执行结果:

~Node() called
~Node() called
~Node() called

至于引用计数,大家可以自己算一算。当然这里,也可以直接将parent作为弱引用,理解不同随意设置都可以。

3. 总结

当然上面树的例子只是一个说明,真实场景可能有很多会有类型情况。总之shared_ptr在环形引用中,带来了循环引用的弊端,所以,需要将其中一个设置为弱引用(根据实际情况确定,一般将管理者使用shared_ptr,其他引用管理者的使用weak_ptr,这么看刚刚的树的weak_ptr属性刚好设置反了)。

在这里插入图片描述
因为循环引用,析构的时候只能释放一次引用计数;而实际的引用计数为2.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!