现在的位置: 首页 > 综合 > 正文

GCC4.7.0库里的shared_ptr,weak_ptr和unique_ptr的简单讲解(抛砖求玉,有图有真相)

2014年03月13日 ⁄ 综合 ⁄ 共 3454字 ⁄ 字号 评论关闭

    实习了一个半月了,实习的日子过得真快.亮哥十分照顾,各种指点,还不单单是技术方面的,这就是传说中良师益友啊.实习一下,感觉视野开阔了一些,以前做的感觉都是像小孩子玩具一样,自娱自乐,现在回头去看感觉也挺水的-____-.

    现在基本写代码都是vim了,IDE感觉都是浮云.GDB调试渐入佳境.但是还是没有win下的调试器方便,gdbtui还好,但是始终不及OD的水平.调试的时候还是方便一点的好啊.linux下开发自我感觉算是入了门吧.(好吧,还是菜鸟级别)

    标准库第二版刚好看到智能指针,所以这两天抽空看看4.7.0库里shared_ptr和weak_ptr的实现,循惯例,上图先

    首先是shared_ptr:

    接下来是weak_ptr:

    智能指针由于引入了引用计数,所以需要考虑的一个情况是并发情况下智能指针引用计数更新的原子性,C++11对并发提供了原生的支持,同时加入了原子类型的支持.GCC4.7.0的库实现当中其实已经考虑了对并发情况的处理(没有考察之前的版本),比如说对指针的引用计数和弱引用计数都是使用_Atomic_word类型(应该是一种原子类型),在获取的时候使用类似__exchange_and_add_dispatch这种原子操作.在引用计数块上面使用了_Lock_policy,这些都对并发下的智能指针的健壮提供了保证.

    从上面的图可以看出,shared_ptr的成员包括1.一个指针,这个指向需要管理的内存区域,注意这部分是不支持数组类型的成员的,简单来说你可以传入一个int作为模板参数,但是不支持int[]这样的模板参数(unique_ptr支持),如果想支持数组类型的指针,需要自己传入deleter,就是释放内存的函数.简单来说,默认的删除是delete _M_ptr,而不是delete[] _M_ptr,所以数组形式的不能采用默认的deleter.2.一个__shared_count对象_M_refcount,这个对象当中封装了一些相关的操作,主要的成员(圆角矩形内)就是一个指向内存计数块的一个指针,就是图中的_Sp_counted_base*
_M_pi._Sp_counted_base是一个基类,继承产生了_Sp_counted_ptr,这个是最简单的一种情况,引用计数块当中除了两个引用计数,还有一个指向管理内存的一个指针,_Sp_counted_deleter则是传入了deleter(包括Alloc),而_Sp_counted_ptr_inplace是提供了Alloc,后面两个是比较高级的使用,Alloc主要是引用计数块对象的分配策略,而deleter是指定引用计数块对管理内存的释放行为(不单单是管理内存,你可以用shared_ptr管理文件句柄,而在deleter当中传入关闭文件句柄的动作).

    智能指针我个人觉得就是结合RAII和C++类编译行为的一个产物,简单来说就是对局部类变量编译时插入构造函数和析构函数(局部变量的生命周期),这样通过对这样的一个局部类变量的构造函数和析构函数的控制来管理这个局部类变量所拥有的资源,BS提到过的句柄类的实现(RAII的例子),感兴趣的同学可以找找看.回到shared_ptr上,我们现在有一个局部变量shared_ptr<T> sp;_M_refcount包括了一个引用计数块的指针,在这个sp生命周期开始的时候,sp当中的类成员_M_refcount进行创建.如果在_M_refcount的构造函数当中对引用计数块进行管理,比如没有的时候进行创建,有的时候增加引用计数.同样,在sp生命周期结束的时候,_M_refcount进行析构,在析构函数中进行引用计数减少,当引用计数为0时释放所管理的内存.这些都是RAII的思想.注意到,这个过程中有两个明显的分配过程,一个是_M_ptr管理的那部分内存,另一个是引用计数块_Sp_counted_Base*指向的那块内存.

    从两个图可以看到,在shared_ptr和weak_ptr当中都包含了一个指向管理内存的指针,shared_ptr当中由于存在解引用操作,所以这个指针_M_ptr是必须的,但是在weak_ptr当中,我认为这个指针不是必须的,因为weak_ptr并不存在解引用等相关操作,这个是一个设计的问题,或许是为了保持结构一致,更大的可能我觉得应该是提升效率,在shared_ptr当中一个解引用不需要通过引用计数块来拿到相应的指针,但是增加了一些必要的维护成本,比如reset的时候必须修改,在引用计数为0释放资源后必须相应设置指针为nullptr等.

    weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放.weak_ptr我觉得其实就是一个管理引用计数块的智能指针.它需要用shared_ptr来初始化,当_M_weak_count不为0时,引用计数块是不会被释放的,从weak_ptr当中可以判断它所关联的shared_ptr是否过期.同时如果还没过期则能够拿到相应的shared_ptr,这样是一个弱引用关系.

    GCC的库当中的实现对于构造函数的重载用到了极致,有时候构造函数的匹配让人烦躁.威哥说:试试看就知道啦,所以有不清楚的地方,果断开GDB跟一跟吧同学们.前几天看type_trait,感慨人类怎么可以写出这样的type_trait,无敌了- -,应该是从boost当中借鉴过来的吧我觉得.GCC库当中STL容器的实现跟STL源码剖析当中讲的还是有点出入的.前几天查到资料.在GCC的STL分配器(allocator)当中并没有使用内存缓存策略,主要考虑按照GCC的文档是由于,内存缓存的分配策略会影响相关内存分配问题的调试,故他使用的是默认的new分配策略.好吧,我其实觉得SGI的STL的allocator写的很帅.其实,这个就是缓存机制的应用,威哥又说:缓存机制是普遍适用的.膜拜一下威哥.

    末尾的吐槽:晚上jakin锅请吃云南菜.太辣了,不敢吃- -,前段时间得了慢性咽炎,一直反反复复没好.烦躁啊.现在深刻体会到身体健康才是王道!

--------------------------------我是分割线----------------------------------

update一下:

    关于unique_ptr,是用于取代c++98的auto_ptr的产物,在c++98的时候还没有移动语义(move semantics)的支持,因此对于auto_ptr的控制权转移的实现没有核心元素的支持,但是还是实现了auto_ptr的移动语义,这样带来的一些问题是拷贝构造函数和复制操作重载函数不够完美,具体体现就是把auto_ptr作为函数参数,传进去的时候控制权转移,转移到函数参数,当函数返回的时候并没有一个控制权移交的过程,所以过了函数调用则原先的auto_ptr已经失效了.在c++11当中有了移动语义,使用move()把unique_ptr传入函数,这样你就知道原先的unique_ptr已经失效了.移动语义本身就说明了这样的问题,比较坑爹的是标准描述是说对于move之后使用原来的内容是未定义行为,并非抛出异常,所以还是要靠人肉遵守游戏规则.再一个,auto_ptr不支持传入deleter,所以只能支持单对象(delete
object),而unique_ptr对数组类型有偏特化重载,并且还做了相应的优化,比如用[]访问相应元素等.

    相对shared_ptr,unique_ptr的性能消耗相对比较低,主要是两者适用的场景不同,一个是引用计数的,就是共享的场景,一个是唯一拥有权.引用计数的引入带来了一些内存开销和维护成本,所以在确定只是唯一拥有权时候,使用unique_ptr能够降低相应的性能开销.

    最后说明,智能指针并非是thread safe的,标准里面只提供了一些保证,具体还没看到,以后有机会再补充.按照对STL容器那样的保证,多线程读单线程写是没有问题的.暂时就先这么理解了.GCC的库是包含了一些并发同步和原子操作的实现,但是具体没有深入去研究,不知道保证的效果到何种程度.如果真的是高并发,还是要自己加锁吧我觉得.保险一点.

抱歉!评论已关闭.