如果有人问起C++和C到底有那些本质上的不同点?主人公阿愚当然也会有自己的一份理解,他会毫不犹豫回答出:“与C相比,C++至少引入了两项重要技术,其一就是对面向对象的全面支持;还有一项就是C++优良的异常处理模型”。是的,这两项技术对构建出一个优良的可靠复杂的软件系统都太重要了。可这两项技术之间又有何关系呢?非常客观公正的说,它们之间的关系实在是太紧密了,两者相互支持和依赖,是构建优良可靠复杂的软件系统最不可缺乏的两个东东。
用对象来描述程序中出现的异常
虽然前几篇文章的内容中列举的一些小例子程序大多都是throw一些如int、double类型的异常,但程序员朋友都很熟悉,实际开发环境中所抛出的异常都是一个个代表抽象数据类型的对象,如C++标准库中的std::exception(),MFC开发库中Cexception等。用对象来描述的我们程序中的出现异常的类型和异常信息是C++异常处理模型中最闪光之处,而且这一特点一直沿用到java语言的异常处理模型中。
为什么要用对象来描述程序中出现的异常呢?这样做的优势何在?主人公阿愚不喜欢穷摆出什么大道理,还是老办法,从具体的实例入手。由于异常有许许多多种类型,如有致命的错误、一般的错误、警告或其它事件通知等,而且不同类型的异常有不同的处理方法,有的异常是不可恢复的,而有的异常是可以恢复的(专业术语叫做“重入”吧!哈哈,主人公阿愚有时也会来点文绉绉的东西),所以程序员在开发系统时就必须考虑把各种可能出现的异常进行分类,以便能够分别处理。下面为一个应用系统设计出一个对异常进行分类的简单例子,如下:
从上面的异常分类来看,它有明显的层次性和继承性,这恰恰和面向对象的继承思想如出一辙,因此用对象来描述程序中出现的异常是再恰当不过的了。而且可以利用面向对象的特性很好的对异常进行分类处理,例如有这样一个例子:
void OpenFile(string f)
{
try
{
// 打开文件的操作,可能抛出FileOpenException
}
catch(FileOpenException& fe)
{
// 处理这个异常,如果这个异常可以很好的得以恢复,那么处理完毕后函数
// 正常返回;否则必须重新抛出这个异常,以供上层的调用函数来能再次处
// 理这个异常对象
int result = ReOpenFile(f);
if (result == false) throw;
}
}
void ReadFile(File f)
{
try
{
// 从文件中读数据,可能抛出FileReadException
}
catch(FileReadException& fe)
{
// 处理这个异常,如果这个异常可以很好的得以恢复,那么处理完毕后函数
// 正常返回;否则必须重新抛出这个异常,以供上层的调用函数来能再次处
// 理这个异常对象
int result = ReReadFile(f);
if (result == false) throw;
}
}
void WriteFile(File f)
{
try
{
// 往文件中写数据,可能抛出FileWriteException
}
catch(FileWriteException& fe)
{
// 处理这个异常,如果这个异常可以很好的得以恢复,那么处理完毕后函数
// 正常返回;否则必须重新抛出这个异常,以供上层的调用函数来能再次处
// 理这个异常对象
int result = ReWriteFile(f);
if (result == false) throw;
}
}
void Func()
{
try
{
// 对文件进行操作,可能出现FileWriteException、FileWriteException
// 和FileWriteException异常
OpenFile(…);
ReadFile(…);
WriteFile(…);
}
// 注意:FileException是FileOpenException、FileReadException和FileWriteException
// 的基类,因此这里定义的catch(FileException& fe)能捕获所有与文件操作失败的异
// 常。
catch(FileException& fe)
{
ExceptionInfo* ef = fe.GetExceptionInfo();
cout << “操作文件时出现了不可恢复的错误,原因是:”<< fe << endl;
}
}
通过上面简单的例子可以看出,利用面向对象的方法,确实能很好地对异常进行分类处理,分层处理,如果异常能得以恢复的尽可能去实现恢复,否则向上层重新抛出异常表明当前的函数不能对这里异常进行有效恢复。同时特别值得一提的是,上层的catch block利用申明一个基类的异常对象作为catch关键字的参数,使得提供了对多种类型的异常对象的集中处理方法,这就是上一篇文章中所提到的除了catch(…)以外,还有其它的来实现对多种类型的异常对象的集中处理方法,而且利用对象基类的方法显然要比catch(…)优雅很多,方便很多,要知道在catch(…)的异常处理模块中是没有多少办法获取一些关于异常出现时异常具体信息的,而对象基类的方法则完全不同,异常处理模块可以访问到真正的异常对象。
现在回想一下上一篇文章中提出的那个问题?就是既然有其它很好的方法(利用类的继承性)来可以代替catch(…)提供的异常集中处理,那为什么C++标准中还偏要提供catch(…)这样一种奇怪的语法呢?其实这还是由于C++本身一些特点所决定的,因为大家都知道,C++在业界有很多的版本,更重要的是没有一个统一的标准开发类库,或者说没有统一的标准开发环境,虽然存在标准C库和标准C++库,但这远远不够,构成不了一个完整的开发支撑环境,因此在许多重要的开发库中都各自为政,它们在自己的开发库都各自定义了一套对异常进行分类支持的库。因此应用程序的开发环境往往都同时需要依赖于几个基础开发库之上(例如MFC + XML4C + Standard C++),这样对开发人员而言,便没有一个共同的异常对象的基类,所以C++标准中便提供了catch(…)来捕获所有异常,这确实是一种不得已而为之的折衷方法(哈哈!这个理解完全是主人公阿愚自己一相情愿,一个人胡思乱想而出来的,朋友们如有不同的意见可以和阿愚一起讨论!),另外JAVA中则不会出现这种情况,因为JDK是统一的,所有的异常对象都是从java.lang.Throwable接口继承而来的,因此只要在程序的入口函数中catch(java.lang.Throwable all),便可以捕获所有的异常。所以在JAVA的异常处理模型中没有类似C++那样一个catch(…)的东东,完全没必要。
异常处理中采用面向对象技术还有哪些好处呢?
上面讲到,用对象来描述程序中出现的异常除了能很好地分层处理异常外,还有那些好处呢?当然除了好处大大的外,好处也是多多的,例如:
(1) 面向对象的实现中,一般都很好的实现了对象的RTTI技术,如果异常用对象来表示,那么就可以很好完成异常对象的数据类型匹配,还有就是函数的多态,例如上面的那个例子中,即便是到了catch(FileException& fe)异常处理模块中,也能知道到底是出现了那种具体的异常,是FileOpenException呢?还是其它的异常?
(2) 面向对象的实现中,一般都很好的实现了对象的构造、对象的销毁、对象的转存复制等等,所以这也为异常处理模型中,异常对象的转存复制和对象销毁提供了很好的支持,容易控制;
(3) 其它的吗?暂时没有,可能还有没想到的。
在异常处理的分层管理下,异常对象的重新抛出往往非常常见,本文中刚才的例子就有这样的情况,但当时仅一笔带过而已,为了表明对它的重视,下一篇文章重点讨论一下异常对象的rethrow处理。
评论