上一篇文章中咱们提到了用智能指针构建二叉树来减轻咱们的工做负担。今天咱们来讨论下稍微复杂的状况下如何借助智能指针管理资源。html
通常来讲,当咱们在程序中使用了智能指针后就无需亲自过问资源管理的问题了。然而随着数据结构和算法逐渐变得复杂,资源之间的关系也可能再也不是简单的共享,好比下面的例子。c++
如今为了方便删除咱们二叉树的某些节点,咱们须要每一个节点都包含本身的父节点的信息,也许你会写成以下的样子:程序员
struct BinaryTreeNode: public std::enable_shared_from_this<BinaryTreeNode> { using NodeType = std::shared_ptr<BinaryTreeNode>; explicit BinaryTreeNode(const int value = 0) : value_{value}, left{NodeType{}}, right{NodeType{}} {} // 插入/搜索/删除 void insert(const int value); NodeType search(int value); NodeType max(); NodeType min(); void remove(int value); void ldr(); void layer_print(); int value_; NodeType parent; // 危险!请勿模仿 NodeType left; NodeType right; private: // methods };
这样改写后的insert
方法在插入节点时须要附加上父节点信息,不过这一步很简单:算法
void BinaryTreeNode::insert(const int value) { if (value < value_) { if (left) { left->insert(value); } else { left = std::make_shared<BinaryTreeNode>(value); // 添加指向父节点的智能指针 left->parent = shared_from_this(); } } if (value > value_) { if (right) { right->insert(value); } else { right = std::make_shared<BinaryTreeNode>(value); right->parent = shared_from_this(); } } }
你可能会以为这有什么复杂的,管理资源仍是一如既往的轻松。缓存
然而你错了,虽然从编译到运行咱们的程序都没有肉眼可见的缺陷,然而咱们用valgrind
诊断一下就能发现问题了:bash
valgrind ./a.out
做为对比这是修复后的运行状况:数据结构
可见相比正常状况,有一半的智能指针并没被释放,而咱们的层级打印正好正好将全部元素复制了一遍,所以你可能已经意识到了,咱们的节点最终并无被释放,可是节点的副本却被释放掉了!(valgrind对于内存池等缓存技术存在必定的误报,但据我所知对于libstdc++的shared_ptr并未使用这类技术)数据结构和算法
这是为何呢?答案很简单,在insert
中咱们制造了循环引用。下面咱们拿根节点和它的左子节点作个演示:this
首先是根节点和其左子节点,在没创建节点关系前二者引用计数都为1,接着咱们创建关系:设计
这种现象其实就是循环引用问题的一种。 如今问题变得明了了,咱们是从根节点释放资源的,根节点释放后接着释放它的子节点,可是如今根节点的计数是2,在用户持有的根节点超出做用域时它的引用计数减去1,变成了1,资源不会被释放,从而形成了内存泄漏,这就是valgrind发出抱怨的缘由。
解决办法也很简单,由于叶子节点始终是引用计数为1的,因此先从叶子节点开始释放人工解开循环引用便可,然而这样又要手动管理内存与咱们“自动”的初衷背道而驰,并且从叶子节点向上释放资源也不够直观,很容易出错。
所以还有一条路:std::weak_ptr
。
weak_ptr如其名,是弱引用,不会增长智能指针的引用计数,它能够从shared_ptr构造也能够转换为shared_ptr。
weak_ptr是专门为了相似上一节的状况而设计的,当两个数据对象之间互相存在引用关系时,若是双方都使用shared_ptr为表明的强引用势必会出现麻烦(主流的c++实现都没有gc,并且编译器也不会帮你自动切断循环,所以出问题后每每致使内存泄露,并且这类问题较为隐蔽因此经常会折磨那些粗心的程序员),这就须要将一方的引用形式改成弱引用来避免出现问题,这里即是weak_ptr。弱引用并不能保证引用的对象是可访问的,所以咱们选择子节点引用parent的形式为弱引用,由于子节点的生命周期是父节点管理的,父节点生命周期是上层节点或用户进行管理,不属于子节点应该干涉的范围内,所以最适合改成弱引用的形式。
如今咱们把结构体修正成以下的样子:
struct BinaryTreeNode: public std::enable_shared_from_this<BinaryTreeNode> { using NodeType = std::shared_ptr<BinaryTreeNode>; ... int value_; std::weak_ptr<BinaryTreeNode> parent; // 解决循环引用 NodeType left; NodeType right; private: // methods };
相应的,insert
中的shared_from_this
也应该修改成weak_from_this
。修改后的节点关系以下图:
如今咱们能够正常地依赖智能指针进行资源管理了。并且不再会听到valgrind的抱怨了。
所以咱们在使用智能指针时应该仔细地分析数据之间的关系,选择合理的方案,避免因误用而产生bug。