日期:2023-01-24 阅读量:0次 所属栏目:计算机应用
摘 要:在C++编程过程中,关于内存比较容易出现的错误就是内存泄漏和野指针,这两个问题实际上都是因为对于对象的生命周期管理不当导致的。如果一个对象其生命周期应该结束,但是对象没有释放,那么导致了内存泄漏的错误,如果一个对象的生命周期还没有结束,但是对象已经被释放,那么往往导致野指针异常,所以对于程序中对象的生命周期作一个好的规划是一个合格的C++程序员必须具备的能力。本文通过介绍和分析几种对象生命周期的管理方式,试图给出一种综合管理程序中对象系统生命周期的方法。
关键词:C++;内存泄漏; 野指针;对象生命周期;隐性内存泄漏;JAVA;垃圾回收;引用计数
1.对象之间的引用关系
在C++中,对象有多种关系,包括继承,包含,当然还有引用,本文重点讨论的是引用关系。一般的,有对象A和B,如果A对象为了实现其功能需要依赖B,但同时B并不是A的一部分,即B的内存空间并不包含于A之中,我们认为A引用了B。A引用B,意为着在A需要的时候,A可以找到B,那么最常见的方法,就是A拥有B的指针。
1) 引用关系可以是动态的,在这种情况,A和B的生命周期是独立的,甚至A和B的引用关系也是动态的,就是说A引用了B,如果B释放了,A就可以不引用B,当然即使B不释放,由于实际业务逻辑的需要,A也可以在程序运行的过程中不再引用B。
2) 引用关系可以是双向,即A引用了B,B同时也可以引用A,A和B相互拥有对方的指针。
3) 对于多个对象的引用关系可以构成一个有向图,如A,B,C三个对象,A可以同时引用B,C。B和C也是。
4) 引用关系是可以自环的,也就是说A还可以引用A自己。
2.几种对象生命周期的管理方式
2.1依靠业务逻辑来维护
事实上,程序中的对象按照业务逻辑的要求都会有一个明确的释放的时机的,比如视图和文档,视图对象可以在用户关闭对应窗口时释放,而文档对象可以在所有对应的视图对象是放时释放。所以对于一个简单系统,可以直接根据业务逻辑编写代码,这样的代码高效,自然,让人容易理解,但是随着需求的增加,系统规模越来越大,需要遵守的业务规则也越来越多,比如在上面的例子中增加一个关闭文档的操作,在用户使用这个命令的时候,直接关闭文档,当然这个时候视图可能是开着的,所以需要同时关闭,这样视图对象的释放条件就变成了用户关闭对应的窗口,或者用户关闭对应的文档,这是两个条件的逻辑组合。在一个复杂的系统中,一个对象是否需要释放可能依赖于很多这样的条件才能做出判断,这样如果程序都是这样根据业务逻辑来判断,难免出错,所以笔者认为需要设计一种机制,来抽象2和简化对对象生命周期的管理,所以单纯依靠业务逻辑来维护对象生命周期的方式,仅适用于简单对象系统。
2.2释放通知
视图和文档是相互引用的,既然是这样,那么如果文档对象释放了,就通知相关的视图对象,如果视图对象暂时不释放,可以将对于文档对象的引用清除掉。反之如果视图对象释放了,也是这样。这个模型可以这样抽象,定义一个如下的基类:
class FreeNotification {
public:
void regFreeNotification(FreeNotification * lpObj){
//向m_aryFreeNotiificationObjects列表中添加一个对象
…;
}
void unregFreeNotification(FreeNotification * lpObj){
//从m_aryFreeNotiificationObjects列表中删除一个对象
;
}
virtual void freeNotification(FreeNotification * lpObj) = 0;
private:
CArray m_aryFreeNotiificationObjects;
};
然后文档和视图都从这个类继承,当文档和视图创建以后,需要引用的话,先说单向的,视图引用文档:
(1)视图得到文档的指针;
(2)视图调用文档的regFreeNotification方法,参数就是自己,将自己加入到文档对象的释放通知列表中,就是说如果文档对象释放,需要通知列表中的所有对象。
(3)现在视图对象可以安心的拥有文档对象的指针了。
(4)如果文档对象释放了,需要调用释放通知列表中的所有对象,当然也包括视图,因为他刚才已经注册了。通知的方法就是调用列表中对象的freeNotification方法,在视图的这个方法中将清除对于文档的引用。
(5)如果视图对象先释放,就执行文档对象的unregFreeNotification方法,将自己从列表中移除。
这种方法在Delphi语言的VCL库中被采用,VCL的TComponent类上抽象了上述的三个方法(名称不同),于是TComponent类及其子类的对象之间都是用这种方式相互引用,并确保不发生野指针的问题。
这种方法的缺点是高成本,效率中等,适合于简单,高等级对象使用。
还有一个问题就是子类必须实现各自的freeNotification方法。
释放通知机制的最大意义在于对于对象引用以及后续的释放环节进行了抽象,使得这个机制和业务逻辑本身无关,这样使得开发人员只要严格遵守这个机制,那么不管业务逻辑多么复杂,都可以保证不出现野指针的问题,从而大大减轻了开发人员的工作量,提高了开发效率和程序质量。
2.3引用计数
释放通知是一个很好的机制,但是显然还好有一些笨重,比如每一个对象需要维护一个列表,是否可以进一步抽象呢,在《依靠业务逻辑来维护》一节中提到其实每一个对象是由其合理的释放时机的,这个是由业务逻辑决定的,但是随着业务逻辑的复杂化,确定这个时机的条件会复杂化,即出现多个条件,往往这些条件都满足才可以释放,于是判断这些条件成为了问题的关键,我们可以从这里开始抽象,其实对于对象的释放,我们不关心具体的业务条件是什么,只关系这个条件是否满足释放的要求。于是引用计数的机制应运而生了,每一个对象有一个计数器,如果有一个释放的条件不满足就加一,满足了就减一,如果计数器为0就说明所有的条件都满足了,对象可以释放了。抽象如下基类:
class RefCountObject {
public:
void addRef(){
m_dwRefCount ++;
}
void release(){
m_dwRefCount --;
if (m_dwRefCount == 0)
delete this;
}
private:
DWORD m_dwRefCount;
};
让文档继承这个类,如果视图引用了文档,那么视图调用文档的addRef方法,如果视图关闭了,就调用文档的release方法,如果没有其他视图使用这个文档,那么加一,减一的效果是0,于是文档对象释放,但是如果这个过程中还有其他视图使用
了文档,那么那个视图也执行了addRef,这样文档对象不会释放,继续供那个视图使用,直到那个视图也执行了release方法。
这种方式简单,高效,可以使用大规模对象系统,但是有若干问题:
(1)上例中文档对象是不可以自己主动释放的,即使业务上要求文档关闭,文档也不能关闭,但是可以将文档的内容保存并置标志,表明已经关闭,这时如果视图再来调用文档的方法,可以返回错误,已经关闭。还可以使用事件的机制辅助完成,就是关闭时发事件给视图,让他解除引用,这个又有点像释放通知。
(2)循环引用的问题,上例中只是讨论了视图引用文档,但是如果文档也引用了视图,那么就会出现两个对象相互引用,这时如果没有外力的作用,两个对象的引用计数都不会变为0,于是就出现了内存泄漏,这是我们不愿意看到的。
总的来说,引用计数也是一种机制,和业务逻辑无关,在微软的COM中就采用这种方式。
2.4垃圾回收
其实避免野指针的最简单的办法是对象不释放,但是如果不释放会导致内存泄漏,野指针是一个致命的问题,一旦出现,程序就异常退出了,但是内存泄漏程序不会马上有问题,而是资源逐渐耗尽,最后崩溃,所以一个简单的想法是先不管资源消耗的问题,先避免野指针,对象就是只申请不释放,当出现内存不足的时候再来想办法,办法就是内存不足时在现有对象中查找哪些对象已经没有人使用了,这样的对象就是垃圾,把它们释放掉,回收的内存空间给新的对象使用。如何查找垃圾呢,可以采取如下算法:
(1) 将所有的对象都标记为垃圾;
(2) 从全局变量和局部变量开始查找,被全局变量和局部变量引用的对象不是垃圾。
(3) 然后再查找不是垃圾对象的成员变量,被不是垃圾的对象引用的对象也不是垃圾。
(4) 重复步骤三,直到不再有新的非垃圾的对象被发现。
(5) 剩下的都是垃圾。
算法很简单,但是这里有一个问题,在C++中,全局变量,局部变量,成员变量都是对象吗,好像不是,所以这个算法就有困难了,我们怎么知道一个变量是不是一个对象指针呢,所以这种方法需要语言级的支持,只有JAVA,JS这样的全部都是对象的语言才适用,在C++中似乎没有办法使用,当然局部的一组对象,然后C++给它专门营造一个环境这样做也是可以的。或者用C++做一个JS的解释器,JS脚本创建的对象就可以这样做,但是对于一般的C++编程意义不大。
当然垃圾回收是现在最好的内存管理机制,几乎不需要人工干预就可以即避免了野指针,也不内存泄漏,当然代价也不小,至于效率吗,以前觉得做垃圾回收的时候程序会卡一下,但是现在JAVA虚拟机的回收机制越来越完善了,所以效率也不是问题了。
值得注意的是隐性的内存泄漏,即使使用垃圾回收隐性的内存泄漏还是无法解决的。
3.总结
首先对象的生命周期是由业务逻辑决定,但是当业务逻辑越来越复杂以后,具体到某个对象其释放的条件可能很复杂,于是需要引进一些机制来专门管理对象的生命周期,对于对象释放条件进行抽象,就有了释放通知,引用计数,垃圾回收等机制。在上述的几种机制中,没有一种可以解决一切问题,都有优缺点,排除垃圾回收,其他的在C++中都有广泛引用。就笔者个人的经验而言用的较多的是引用计数,因为简单高效,但是也经常为循环引用的问题纠结。所以具体的办法就是以引用计数为基础,根据业务逻辑在适当的地方加入释放通知,但是不使用上文中提到的基类,而是主要使用事件通知,事件本身并不专门是为了对象释放设计的,而是为了业务上的需要,做通知用的,但是本质上对象的生命周期是业务决定的,如果一组对象相互引用,必定有业务上的需求,那么在业务上也一定有一个时机可以让这一组对象都释放掉。只要能找到这个时间点,用事件触发,使引用循环打破,就可以释放全部对象。
最后需要说明的是笔者在这里主张引入对象生命周期管理的机制,是为了减少一些内存问题的发生,提高程序质量和开发效率,但是这些机制远没有到可以一劳永逸的解决内存问题的时候,简单的讲上述的机制都不能解决隐性的内存泄漏的问题,比如对一个集合不断的创建元素,当元素不再有用的时候也不从集合中移除,这样上述的任何一种机制都会认为元素是有用的,都不会释放,于是导致内存泄漏,所以现阶段程序员除了关注业务逻辑,还是要关注一些编程上的事务性工作的。没有银弹! 本文链接:http://www.qk112.com/lwfw/jsjlw/jisuanjiyingyong/242914.html