编写智能指针实现对象托管(一)

来源:互联网 发布:video.js使用方法 编辑:程序博客网 时间:2024/05/21 21:47

一、遇到的麻烦

  在C++代码中,经常需要包含标准库头文件memory,使用std::shared_ptr和std::weak_ptr模板,来实现对象的自动释放。
然而在处理循环引用时,两者需要结合使用起来使用,仍然颇为费神。特别是在编写树或图时,一不小心就内存泄漏。比如这一段代码执行起来就会耗尽内存:
#include<memory>struct Node {    std::shared_ptr<Node> head;    std::shared_ptr<Node> next;};int main() {    for (int i = 1; i; ++i) {        std::shared_ptr<Node>root = std::shared_ptr<Node>(new Node);        root->next = std::shared_ptr<Node>(new Node);        root->next->head = root;    }}
  使用C#和C++/CLR一段时间之后,我越发觉得自动的内存管理机制能大大提高程序编写效率。在投入.NET怀抱前,我决定尝试在C++中编写智能指针来实现内存托管,主要解决循环引用的问题。

二、std::shared_ptr和std::weak_ptr

  先研究std::shared_ptr和std::weak_ptr的工作机制,这一定能对我的编写有所启发。这里先假设字母T代表一种类型(比如int)
  shared_ptr<T>、weak_ptr<T>对象都包含两个成员:对象指针、计数指针。
  在shared_ptr<T>对象初始化时,需要传入一个T对象指针,它的对象指针成员会被赋值为传入的指针,而且会自动新建一块内存空间用来计数,计数指针成员就指向这块内存空间(暂时称之为计数对象),如下图。
  当把一个shared_ptr<T>对象赋值给多个shared_ptr<T>对象时,将使得其它shared_ptr<T>对象的成员也指向同一个T对象和计数对象。这时可以认为这些shared_ptr<T>对象都指向了同一个T对象,如下图:
  weak_ptr<T>对象也是一样的结构,只不过shared_ptr<T>对象在构造、赋值、析构时,会同时修改计数对象的强引用计数、弱引用计数,而weak_ptr<T>在构造、赋值、析构时只修改计数对象的弱引用计数部分。
  通过计数对象,可以知道当前有多少个shared_ptr<T>对象指向了T对象,又有多少个weak_ptr<T>对象指向了T对象。
  当某个shared_ptr<T>对象析构(或者被赋予其它值)发现计数对象中的强引用计数变为0时,就会把对象释放掉;当shared_ptr<T>、weak_ptr<T>析构(或者被赋予其它值)发现计数对象的弱引用计数变为0时,就会把计数对象释放掉。
  从shared_ptr<T>对象访问T对象时,并不需要判断计数对象的强引用是否为0,因为该shared_ptr<T>的存在就证明了T对象未被析构(除非对象指针为空)。但若是想从weak_ptr<T>获取shared_ptr<T>,就需要判断对象是否还存在了,若发现强引用计数为0,从指向该计数对象的weak_ptr<T>获取shared_ptr<T>都会得到一个空的shared_ptr<T>。(不能从weak_ptr<T>对象直接访问T对象)
  T对象和计数对象虽然是1对1的关系,但是他们是分离的,这不仅能满足weak_ptr的需求(T对象释放后计数对象不一定会被释放),也使得shared_ptr、weak_ptr对对象T没有任何限制。然而这种被称为“非侵入式”的方法又导致了另一个问题:无法仅从T对象本身或者仅从计数对象本身直接获取一个合法的shared_ptr<T>对象。

三、对象托管初步构想

  说到托管,我会情不自禁地联想到.NET的垃圾回收,想到三代回收机制。然而,当我仔细琢磨时,却发现这在C++中根本行不通!这是因为C++的对象根本经不起移动。试想,当某个对象的成员函数执行到一半时,对象自己突然就被移动到其它地方去了,而this指针没法跟着变,意味着接下来所有成员全部都无效了!这后果是非常严重的!因此,接下来要考虑的内容跟数据的移动没有任何关系!
  为了解决循环引用的问题,我打算使用后台线程执行“标记-清除”法,每一轮的大致思路是:锁全局->从根节点出发扫描标记对象->找出未标记对象->解锁->释放对象。这就对智能指针工作机制提出了几个要求:
1.必须提供与对象关联的标记变量,用以执行标记-清除。
2.必须提供遍历对象托管成员的方法。不然,扫描标记对象时,就无法对盘根错节的成员关系进行标记,也就无法解决循环引用的问题。
3.为了防止标记和扫描过程中发生变动,必须进行锁全局,意味着所有托管成员的赋值方法都需要经过全局变量判断。
  上述3点中,第1点和第2点都对对象本身做出了某些限制。如果说第一点可以参照shared_ptr的做法,把标记变量放到计数对象中,那第二点就完全没有办法在“非侵入式”的模式下完成了:至少需要规定一个接口(虚函数),或者某种统一的查找成员的方法。第3点可以在智能指针本身的赋值等函数中实现。
  然后,后台扫描线程需要明确两个集合:根节点对象集合、所有托管对象集合。C++标准库中并没有方便的办法可以获取栈信息,因此想通过判断一个智能指针是否在栈上构造来获取根节点集合的话是行不通的,这时候我想到了使用特定的函数来构建托管对象,使用这个函数构造的对象都添加到根节点。而托管对象的集合则可以比较方便获取:可以在智能指针构造的时候统一添加进这个集合。
  最后,对象托管应该是一个“可选”项,即使它不是一个可有可无的东西,也是可以从解决方案中轻松包含或者剔除掉的。
(未完待续)
1 0
原创粉丝点击