内存管理与智能指针
内存管理与智能指针
C++中不具备对内存的自动管理机制,垃圾回收机制。
内存管理是一件复杂的事情和重要的事情,面对庞大的对象群,复杂的逻辑,
甚至多线程中,是不能保证对象的每一次使用都正确,申请的内存都能被合理的释放。
但是又必须确保这件事情的正确执行,否则系统将无法正常的工作。
最近在学习平台上的一些感觉非常棒的设计方法和机制的应用,
结果却不断的发现有太多的基于C++基础如模板,指针,虚函数等应用,
而我却不甚理解,也感觉到C++这门语言是何其强大和深奥,
不是学会了语法就能掌握的,灵活的应用才是最困难的和不易掌握的。
本篇将简单探究一下C++中所谓的智能指针问题。
一 Boost智能指针
参考:http://www.cnblogs.com/sld666666/archive/2010/12/16/1908265.html(Boost库没事用过)
一个智能指针就是一个C++对象,此对象的行为就像是一个指针,
具有指针的运算操作(如=,==,!=,->等操作);
但是它本身却可以在其不需要的时候其不需要的时候是一个模糊的概念:
指向的对象超出范围,被释放……)自动删除,最重要的就是这个自动删除,是怎么样做到自动删除。
具有的指针如下:
shared_ptr<T> |
内部维护一个引用计数器来判断此指针是不是需要被释放。是boost中最常用的智能指针了。 |
weak_ptr<T> |
弱指针,要和shared_ptr 结合使用 |
shared_array<T> |
和shared_ptr相似,但是访问的是数组 |
scoped_array<T> |
和scoped_ptr相似,但是访问的是数组 |
1 shared_ptr
其中有一点:在内部维护一个引用计数器,
当有一个指针指向这块内存区域时引用计数器 +1,
当指向这快内存取得指针销毁时 -1,
如果没有任何指针指向这块区域,引用计数器 = 0,将内存释放。
2 weak_ptr
weak_ptr是一个弱指针。shared_ptr被weak_ptr控制,
它可以通过weak_ptr的构造函数或者lock成员函数转化为shared_ptr。
weak_ptr的引用并不会增加的对对象内存区域引用计数器值的增加。
而是共享shared_ptr内存。所以无论构造还是析构一个weak_ptr对象都不会影响引用计数器。
作用就是:因循环引用计数器值始终不为0,导致对象不能被释放,利用weak_ptr特性可以打破对象的循环引用。
网上大多数介绍智能指针的,基本上都是局限于这个概念上——什么是智能指针以及如何使用,
而并没有介绍为何需要智能指针,智能指针的工作原理。
看了很多文章之后依然很晕,直到发现了这样一篇文章:
《当析构函数遇到多线程——C++中线程安全的对象回调》
链接如下:http://www.cnblogs.com/Solstice/archive/2010/02/10/dtor_meets_threads.html
下面做一个简单的总结。
二 智能指针工作原理
1 对象的创建于销毁
C++中对象的生命周期需要自己管理,庞大对象群,复杂逻辑,多线程致使对象的销毁时机变得模糊不清,
出现竞态条件(如果两个或者两个以上的线程同时访问相同的对象,或者访问不同步的共享状态,就会出现竞态条件)。
- 在释放一个对象时,怎么知道此对象不会在被使用到或者已经无用
- 在即将析构一个对象时,从何而知是否有另外的线程正在执行该对象的成员函数
- 如何保证在执行成员函数期间,对象不会在另一个线程被析构
- 在调用某个对象成员时,如何得知这个对象还活着,他的析构函数会不会刚执行到一半。
对象构造要做到线程安全,惟一的要求是在构造期间不要泄露this指针,即
- 不要再构造函数中注册任何回调
- 不要在构造函数中把this传给跨线程的对象
- 即便在构造函数最后一行也能做上面两件事
之所以这样规定,是因为在构造函数执行期间对象还没有完成初始化,
如果this被泄露给了其他对象(其自身创建的子对象除外),
那么别的线程有可能访问这个半成品对象,这样会造成难以预料的结果。
看到这里才明白在工作中的平台当中,为什么在所有对象的基类中有一个虚函数onInit,
需要使用宏创建其派生类一个对象时,便会执行onInit函数,
在执行onInit函数时可以判断对象是否被成功创建;
还有一个onDeinit(销毁对象时调用),构造函数完成成员变量的初始化,
二创建对象注册回调,都是在onInit函数中完成,这大概就是上面的意思。
二段式的对象创建:
在对象构造时,基类先于派生类构造,在构造函数最后一行时,
仅仅是将当前的类部分构造完成,而其派生类部分还没有构造。
所以此时对象是不完整的。
2 对象指针
使用动态方式创建的对象,怎么判断此对象还活着,光看指针是看不出来的,
对象被释放之后指针仍然指向原来的地址;指针就是指向了一块内存,
这块内存上的对象如果已经被销毁,那么根本就不能访问对象的任何成员(free之后的地址一样不能被访问),
指针不为空但是对象又不能访问那么对象的状态如何得知。
判断一个指针是不是野指针没有高效的办法。(原址上有创建了新的对象,具体类型又不得知)
在面向对象程序设计中,对象的关系主要有三种:composition,aggregation,association。
1 .Composition(组合/复合)关系在多线程里不会遇到麻烦,
因为对象x的生命期由其惟一的拥有者owner控制,onwer析构的时候会把x也析构掉,
从形式上看,x是owner的直接数据成员,或者scoped_ptr成员,或者onwer持有的容器元素。
2 Association(关联/联系)是一种宽泛的关系,他表示对象a用到了另一个对象b,
调用了后者的成员函数,从代码形式上看,a持有b的指针(或引用),但是b的生命周期不由a单独控制。
3 Aggregation(聚合)关系从形式上看与association相同,除了a和b有逻辑上的整体与部分关系。
如果b是动态创建,在整个程序结束前有可能被释放,那么就会出现竞态条件。
3 解决方法
只创建不销毁。程序使用一个对象池来暂存用过的对象,
下次申请对象时,如果对象池里有,就重复利用现有对象,
否则就新创建。对象用完放回对象池里。可以避免访问对象失效的情况发生。
这种山寨办法问题:
- 对象池的线程安全,如何安全的完整地把对象放回池子里,不会出现“部分放回”的竞态(线程A认为对象X放回了,线程B认为对象X还活着)
- 共享对象类型不止一种,重复实现对象池还是使用模板
- 会不会造成内存碎片和泄漏,因为对象池占用的内存只增不减,多个对象池不能共享内存。
对象的生命周期的管理问题,在我目前工作的平台是这样管理的:
以App为单位单独分配一块内存区域,属于App内的所有的独享共享这一块区域,
所有对象的建立必须在其父对象的基础之上。(显示方面)每一个显示单位以Page为基础,
所有的控件都必须挂在Page之上。App的一个Screen提供PageStack管理所有的page。
对象的创建过程如下:所有的支持实例化的类都从Object继承下来。
Object提供了m_parentObj; m_firstChildObj;m_lastChildObj;m_nextObj;m_prevObj;
成员将整个对象穿起来,构成一个对象树。
对象的释放可以自动管理,以Page为单位,当退出一个Page时,
将以Page作为父对象的所有对象都释放,其子对象下的子对象也依次循环释放。
当退出整个App时,App下的所有子对象全部释放,以此类推释放所有对象。
需要新的对象时并不是从这颗对象树中寻找,而是直接创建新的对象挂在父对象上,
手动释放的对象就会从此对象上删除。解决的是对象的创建和释放问题。
这在很大程度解决了部分对象有效性的问题,“只创建不销毁”。
通常一个Page退出时,基本上所有的对象都不在有用,可以全部释放掉了。
当然在将对象加入到对象树中可能出现竞态条件,但是在使用的平台上是一个多任务的实时系统没有线程概念,
而且各个任务之间的功能相对独,基本不存在对同一个对象操作的情况。
存在的情况时,对于对象树管理对象的释放工作,但是仍然是支持手动的去释放对象(有时候也是必要的),
其中很多对象都会注册一些事件消息回调,那么这样就仍然需要保证对象的有效性。
4 weak_ptr
为了保证对象的有效性,向上转换的安全性。平台使用自己定义的一套weak_ptr机制。
基本的原理如下:
A 原始场景
P1 释放了对象,P2是仍然指向一个被释放的对象,结果……
解决方法:不释放Obj或者在释放Obj的时候将所有指向它的指针都置NULL。这两种方法基本都是不可行的。
B 引入中间层Proxy,让Proxy对象去维护这个对象:
C 构造中间层管理对象的释放
D 平台上实现方式
E 这样一种实现过程中,将创建的对象构建VfxWeakPtr对象指针去维护,
而不用去关心对象何时应该释放,无需手动释放,确保在程序运行过程中能够得到有效的对象。
F 通过上述的方式,如果我们手动的释放了对象,
而没有去通知维护的中间层Proxy部分的指针置NULL,将仍然会存在问题。
所以需要的是一种既能保证自动释放又能保证手动释放,都不会出现问题。
可以看到 上述实现方式图:
VfxObject这个类有一个成员VfxWeakPtrFlag *m_weakPtrFlag;
其实就是用来构建维护对象的成员,他和SP1,SP2所指向的对象是相等的,
因为对象构建的SP1,SP2所创建的都是有VfxObject基类对象部分创建。(VfxObject是大多数类的基类)
在手动的释放对象时候,VfxObject对象的析构函数会使用指向维护自身的对象的中间层
去将中间层中指向实际对象的指针置NULL,保证中间层的有效性。