- 浏览: 15822 次
- 性别:
- 来自: 成都
最新评论
构造函数和析构函数分别管理对象的建立和释放,负责对象的诞生和死亡的过程。当一个对象诞生时,构造函数负责创建并初始化对象的内部环境,包括分配内存、创建内部对象和打开相关的外部资源,等等。而当对象死亡时,析构函数负责关闭资源、释放内部的对象和已分配的内存。
在对象生死攸关的地方,如果程序代码出现问题,常常会发生内存泄漏,从而产生可能危害系统运行的孤魂野鬼。大量的事实表明,业务逻辑代码写得非常严谨的程序在运行中仍然发现存在内存泄露,大都是构造和析构部分的代码存在问题。
而许多程序员都习惯于面向对象的编程,需要时就建立一个对象,不用时就将其释放。这样的习惯简化了我们的思路,正是面向对象编程思想带来的好处。也许由于太习惯了,很多程序员都忽略了在对象生死的瞬间也可能产生异常的问题,这种现象却值得我们去认真反思。
其实,对象生死间的异常问题是一个充满争议的问题。甚至不同的编程语言,在对象生死间的异常问题上也持不同的态度。
C++语言说:一个对象在出生的过程中发生异常问题,那这个对象就是一个没有生命的怪胎。既然它不是一个完整的对象,就根本不存在析构或释放的 说 法。因此,C++在执行构造函数过程中产生异常时,是不会调用对象的析构函数的,而仅仅清理和释放产生异常前的那些C++管理的变量空间等,之后就把异常 抛给程序员处理。
DELPHI (Object Pascal)语言认为:对象虽然在出生过程中出现异常,但它已经具有部分生命。既然是有生命的东西,都应该有死亡的权利。因此,DELPHI在执行构造函数时产生异常,一定会先调用该对象的析构函数,然后再抛出异常给程序员去处理。
那么,谁的观点对?谁的观点错?
我想,这个问题争论上九九八十一天也未必有结果。因此,我们不必纠缠于观点的争论。只要我们知道了不同编程程语言有不同的处理方法就够了,当我们用哪种语言来编程时就尊重该种语言的观点,这才是务实的程序员应该做的!
对于C++语言来说,由于构造函数产生异常时不会调用对应的析构函数,那么在构造函数里发生异常前的代码所创建的其他东西就不能被析构函数内的相关释放代码所释放。例如:
class TMyObject
{
private:
TOtherObject * OtherObject;
public:
TMyObject()
{
OtherObject = new TOtherObject();
...... //这后面的代码发生异常将导致OtherObject不会被释放!
}
~TMyObject()
{
......
delete OtherObject; //构造函数发生异常时析构函数根本不会被调用,此代码也不会被执行!
}
}
回想一下自己的程序中是否也存在类似的代码?如果是这样,那可就要注意了,这样的代码在C++中是不安全的!
那么,应该怎样写才安全呢?
事实上,如果在C++的构造函数里创建了其他东西,你就必须考虑构造函数发生异常的情况。在构造函数中发生异常时,已经创建的东西必须被释放 掉,然后再重新抛出异常给上层调用代码处理,这才是C++构造函数中正确的异常处理方法。因此,前面的构造函数应该改写成下面的形式:
TMyObject()
{
OtherObject = new TOtherObject();
try
{
...... //这里的代码发生异常。
}
catch(...)
{
delete OtherObject; //确保发生异常时,能释放掉已建立的东西。
throw; //再次抛出异常给上层调用代码处理。
};
}
如果,一个构造函数要创建很多其他东西的话,就应该编写相应的try try try ... catch catch catch形式的嵌套代码(或者相同逻辑的代码)来确保构造函数的正确性。当你看到某位C++程序员在构造函数中写了一大堆壮观的try try try ... catch catch catch代码时,请相信我说的话:他一定是一位非常严谨的C++程序员。
不过,有些聪明的C++程序员还找到另外一种不用try...catch来处理构造异常的方法,那就是C++标准类库中的那个著名的auto_ptr模板 类。auto_ptr又常常被称为智能指针,它巧妙地利用C++退出作用域时会自动释放变量的机制,来清理其维护的对象。再看改写的代码:
#include <memory>
#include <iostream>
class TMyObject
{
private:
std::auto_ptr<TOtherObject> OtherObject;
public:
TMyObject()
{
OtherObject = std::auto_ptr<TOtherObject>(new TOtherObject());
...... //这后面的代码发生异常,也可以确保OtherObject会被自动释放!
}
~TMyObject()
{
//一旦将对象交给auto_ptr来维护,就永远不要自己释放该对象。因此,这里什么都不用写。
}
}
这样的代码那么简洁而且有效,也便于阅读和维护。
为什么这样的机制有效?因为,这里的OtherObject是被定义为一个成员变量而不是指针。从C++创建对象的机制上来说,一定会先分配对 象空间和创建成员对象,然后才调用对象的构造函数,进入构造函数的作用域。一 旦构造函数发生异常,必然退出构造函数的作用域,C++自然会释放成员对象和空间。而OtherObject成员变量被释放时,会调用auto_ptr的 析构函数,从而成功释放其管理的真正对象。
不过,atuo_ptr在使用过程中也有些副作用。比如,你把一个auto_ptr赋值给另一个auto_ptr,前一个auto_ptr就会 变成null值,这不符合正常的赋值语义。这是由于auto_ptr重载了赋值操作符的缘故,不懂auto_ptr实现原理的人就常常犯null指针错 误。使用auto_ptr还有一句名言:“别把一个对象赋给两个auo_ptr变量”,因为这会导致两次释放一个对象的错误。不管怎样,如果嫌 try... catch麻烦,使用auto_ptr来保证不发生内存泄漏也是一个非常不错的选择。就看你喜欢不喜欢了。
相比之下,DELPHI语言处理构造函数的异常就简单多了。因为DELPHI保证在构造函数发生异常后,会调用析构函数。不过并非所有的析构函数都满足这一条件,只有Destroy可以,而且它是一个虚函数。
TMyObject = class
private
OtherObject: TOtherObject;
public
constructor Create;
destructor Destroy; override;
end;
constructor TMyObject.Create;
begin
OtherObject := TOtherObject.Create;
...... //这后面的代码发生异常可以保证析构函数Destroy被调用,从而释放OtherObject!
end;
destructor TMyObject.Destroy;
begin
......
OtherObject.Free; //这里OtherObject将被正确释放!
end;
因为DELPHI的“构造异常时确保析构的机制”是非常基础的代码,它只能给根类TObject设计一个虚析构函数Destroy来作为析构函数调用。也就是说,如果你自己写的析构函数不是从Destroy重载的,“构造异常时确保析构的机制”将失效!
同时,DELPHI在根类TObject中提供了Free方法来方便对象释放。这个Free方法保证即使对象并为被创建(对象指针为nil), 调用此方法也不会出错。这样,构造函数里面创建语句就可以很简洁地与析构函数的释放语句对应起来,方便我们看代码。看来,设计这个Free方法还是用心良 苦啊!
那么关于析构函数中的异常又会怎样呢?
对象在死亡的过程中发生异常又引出一个有趣的问题,“想死死不了”或者“死了一半又不能死了”!那么,这个对象到底是死了还是活着?这种既死又活的对象,就像量子理论中的那只“薛定谔的猫”一样有趣。的确存在,却难以琢磨!
为此,C++根本不去纠缠这种复杂的问题,而是采用最简单的办法:如果析构函数抛出异常,将直接导致当前执行线程异常终止!如果是主线程中发生析构异常,程序立即退出!
C++的这一做法是可以理解的,当代码已经走进无法想通的死胡同,对象只能疯掉,从而毁灭整个程序世界!看来并非芸芸众生才有无法逃脱的苦海,其实运行中的程序进程也有解不开的心结!
所以,“永远不要在析构函数中抛出异常”成了编写C++代码的一条铁律!
而在DELPHI中,析构函数发生异常和其他代码发生异常没有什么不同,发生异常之后的代码将不会被执行(finally部分的除外),异常将由能找到的上层异常处理代码来处理!如果异常最终没有被处理,才导致当前线程终止。
那么,DELPHI的处理方式比C++好些吗?
我们来看下面的代码:
destructor TMyObject.Destory;
begin
C.Free;
B.Free; //释放B发生异常,将导致后面的代码不会被执行,A不会被释放!
A.Free;
end;
因此,严格地说,析构函数应该写成下面的形式:
destructor TMyObject.Destory;
begin
try
C.Free;
finally
try
B.Free; //这里发生异常,也可以强行释放A
finally
A.Free;
end;
end;
end;
但这样的代码感觉怪怪的。而且,虽然释放B时发生异常也可以确保释放后面的A,但B到底是释放了还是没释放?是不是也会有内存泄漏呢?这就要看 B的析构函数是怎么写的,问题将一直追溯下去。照这样下去,那些try...fianlly仅仅能保证尽可能释放对象,而根本没法保证不发生内存泄漏。
看来这个析构函数异常不是一时半会能想通的问题,DELPHI的方法也并不比C++高明。因此,“永远不要在析构函数中抛出异常”依然可以作为一个简单的定律!
既然能保证编写的每一个析构函数都不发生异常,那么那些try...finally也就没有必要了。
有时候,面对复杂和困惑的纠缠,只要牢记简单的原则,也能泰然处之。
这世界有太多不如意,但你的生活还是要继续,太阳每天依旧要升起,希望永远种在你心里...
转自:http://blog.csdn.net/leadzen/archive/2007/09/13/1783116.aspx
发表评论
-
Visual C++ 2010(2008)创建Ribbon界面
2011-07-01 22:28 1055http://hayyoungsue.blog.163.com ... -
C++0x FAQ中文版
2011-06-27 21:47 801http://space.itpub.net/trackbac ... -
Windows用户模式与内核模式
2011-06-01 09:18 959从Intel80386开始,出于安 ... -
Windows用户模式与内核模式
2011-06-01 09:18 11从Intel80386开始,出于安 ... -
MFC 消息类型
2011-03-30 10:19 7751、命令消息(WM_COMMAND) ... -
Css处理负数需要position的配合【及IE6CSS负数遮挡处理】
2010-12-30 22:25 1420有8个像素是负数,在IE6下会被遮挡,因此这个时候,必须用po ... -
NHibernate之旅系列文章导航
2010-12-10 22:35 723http://www.cnblogs.com/lyj/arch ... -
NUnit2.0详细使用方法 (二)
2010-12-10 22:33 895TestFixtureSetUp/TestFixtureTea ... -
NUnit2.0详细使用方法 (一)
2010-12-10 22:24 8001. TDD的简介 首先什么是TDD呢?Kent Beck ... -
JAVA基础:Hibernate外键关联与HQL语法
2010-12-05 23:22 850例如对于TUser类 1.实体查询 String hql ... -
Fck编辑器的完整详解
2010-11-17 15:07 921javascript调用方式: --------------- ... -
java抽象类和接口和继承之间关系
2010-11-09 14:52 1410有时候,我们可能想要构造一个很抽象的父类对象,它可能仅仅代表一 ...
相关推荐
产品上市生死劫--经销商开发.doc
标准大战--企业生死劫.doc
生死簿-修改版.zip是一个包含了学生管理系统改进版本的压缩文件,它主要利用MySQL数据库来存储和管理数据。这个系统提供了一个基础的平台,用于模拟生死簿的概念,即记录和管理个体信息,尤其适用于教学环境或者小...
【经营管理】标准大战--企业生死劫.doc
青海省2015年中西医结合医师:脉象次数与疾病生死2014-08-23试题.doc
生死忍者是一款基于HTML5技术开发的小游戏,它可以在各种平台和设备上运行,包括NAS(Network Attached Storage,网络附加存储)上的WebStation服务器。HTML5作为一种强大的网页开发标准,使得游戏无需安装插件或...
互联网金融合规生死劫.pptx
标准大战——企业生死劫.doc
《球球大作战-生死追逐》是一款专为少儿设计的编程学习项目,旨在通过趣味性的游戏制作,激发孩子们对编程的兴趣。该项目使用了Scratch这一图形化编程工具,让编程变得简单易懂,适合初学者入门。Scratch是由麻省...
【中国汽车业的生死劫】 中国汽车业自1953年开始发展,历经40余年,已经成为全球汽车生产大国,1999年产量接近180万辆,占世界汽车总产量的1/30,列居世界十大汽车生产国之列。汽车产业在中国国民经济中的地位不断...
造纸行业深度研究报告:后外废时代废纸系演进之路,海外布局看盛衰,成本优势定生死-0602-华创证券-22页.pdf
【大盘深V生死劫:对冲基金经理视角下的市场解析】 在金融市场中,股市的大起大落往往牵动着每一个投资者的心。2015年6月6日,一篇由对冲基金经理熊鹏撰写的分析文章揭示了A股市场的暴涨暴跌背后的玄机。这篇文章...
文中提到的生鲜电商陷入生死劫,冷链物流并非绊脚石,实际上指出了一个行业内的普遍认识:生鲜电商的困局并非由冷链物流直接导致,而是另有深层次的原因。 生鲜电商所涉及的物流运输环节,尤其是冷链物流,其成本并...
这些启动函数的主要任务是初始化C/C++运行时库,执行全局和静态C++对象的构造函数。 侯捷老师在《深入浅出MFC》中提到的“引爆器”是指Application Object,即`CMyWinApp theApp`。这个特殊的C++对象在程序启动时由...
不可不知的生死游戏---关系到生死存亡的大事件---python之约瑟夫生死游戏 的源码实现不了的可以直接下载
- **创建过程**:包括分配内存、初始化零值、构造函数执行等步骤。 - **内存布局**:对象在堆内存中的分布和结构。 - **对象的访问定位**:如何通过引用找到堆中的对象。 - **类加载器**:负责加载类到JVM中。 ### ...
当C/C++运行时库的启动函数调用了所有全局和静态的C++类对象的构造函数后,实际上就触发了整个MFC应用程序的启动流程,即所谓的“引爆器”。 #### 二、MFC文档视图结构内幕 MFC中的文档视图结构是其最具特色的设计...
面向对象程序设计是一种编程范式,它以对象作为基本的构造单元,强调数据结构与操作数据的方法相结合。在本主题中,我们将其应用于围棋游戏,特别是实现数气和禁手吃子的规则。数气是围棋中计算棋子生死的重要环节,...
`josering`类的构造函数接收两个参数`x`和`y`,分别代表总人数`n`和出列数`m`。在构造函数内部,使用循环创建了一个单循环链表,并确保最后一个节点的`next`指针指向链表的头部。 5. **链表操作** `getnum()`方法...
在此过程中,C/C++运行时库的启动函数被调用,这一函数负责初始化C/C++运行时环境,并为所有全局和静态的C++对象调用构造函数。 - 对于典型的ANSI GUI应用程序,启动函数通常是`int WinMainCRTStartup(void);`。 ...