C++ 智能指针weak_ptr用途浅析
我们知道C++智能指针有shared_ptr
和 unique_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;
}
如果我们运行这段代码可以发现,没有任何输出,析构函数并没有被调用?内存就这样被泄露了?我们来分析一下这段代码:
std::shared_ptr<Node> Root = std::make_shared<Node>(200);
: Root的引用计数为1.std::shared_ptr<Node> Left = std::make_shared<Node>(100);
: Left引用计数为1.std::shared_ptr<Node> Right = std::make_shared<Node>(300);
: Right 引用计数为1.Root->LeftChild = Left;
: Left引用计数为2.Root->RightChild = Right;
: Right引用计数为2.Left->Parent = Root;
: Root引用计数为2.Right->Parent = Root;
: Root引用计数为3.
离开作用域的时候:
- Root被释放,引用计数减1 为2.
- Left被释放,引用计数为1.
- 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.
来源:CSDN
作者:xiangbaohui
链接:https://blog.csdn.net/xiangbaohui/article/details/103628485