选择异常处理的编程方法的具体原因如下:
1、把错误处理和真正的工作分开来;
2、代码更易组织,更清晰,复杂的工作任务更容易实现;
3、毫无疑问,更安全了,不至于由于一些小的疏忽而使程序意外崩溃了;
4、由于C++中的try catch可以分层嵌套,所以它提供了一种方法使得程序的控制流可以安全的跳转到上层(或者上上层)的错误处理模块中去。(不同于return语句,异常处理的控制流是可以安全地跨越一个或多个函数)。
5、还有一个重要的原因就是,由于目前需要开发的软件产品总是变得越来越复杂、越来越庞大,如果系统中没有一个可靠的异常处理模型,那必定是一件十分糟糕的局面。
异常处理仅仅通过类型而不是通过值来匹配的,否则又回到了传统的错误处理技术上去了,所以catch块的参数可以没有参数名称,只需要参数类型,除非要使用那个参数。
关键字
1、 try
2、 catch
3、 throw
其中关键字try表示定义一个受到监控、受到保护的程序代码块;关键字catch与try遥相呼应,定义当try block(受监控的程序块)出现异常时,错误处理的程序模块,并且每个catch block都带一个参数(类似于函数定义时的数那样),这个参数的数据类型用于异常对象的数据类型进行匹配;而throw则是检测到一个异常错误发生后向外抛出一个异常事件,通知对应的catch程序块执行对应的错误处理。
1、还是给一个例子吧!如下:
int main()
{
cout << "In main." << endl;
//定义一个try block,它是用一对花括号{}所括起来的块作用域的代码块
try
{
cout << "在 try block 中, 准备抛出一个异常." << endl;
//这里抛出一个异常(其中异常对象的数据类型是int,值为1)
//由于在try block中的代码是受到监控保护的,所以抛出异常后,程序的
//控制流便转到随后的catch block中
throw 1;
cout << "在 try block 中, 由于前面抛出了一个异常,因此这里的代码是不会得以执行到的" << endl;
}
//这里必须相对应地,至少定义一个catch block,同样它也是用花括号括起来的
catch( int& value )
{
cout << "在 catch block 中, 处理异常错误。异常对象value的值为:"<< value << endl;
}
cout << "Back in main. Execution resumes here." << endl;
return 0;
}
2、语法很简单吧!的确如此。另外一个try block可以有多个对应的catch block,可为什么要多个catch block 呢?这是因为每个catch block匹配一种类型的异常错误对象的处理,多个catch block呢就可以针对不同的异常错误类型分别处理。毕竟异常错误也是分级别的呀!有致命的、有一般的、有警告的,甚至还有的只是事件通知。例子如下:
int main()
{
try
{ cout << "在 try block 中, 准备抛出一个int数据类型的异常." << endl;
throw 1;
cout << "在 try block 中, 准备抛出一个double数据类型的异常." << endl;
throw 0.5;
}
catch( int& value )
{
cout << "在 catch block 中, int数据类型处理异常错误。”<< endl;
}
catch( double& d_value )
{
cout << "在 catch block 中, double数据类型处理异常错误。”<< endl;
}
return 0;
}
3、一个函数中可以有多个trycatch结构块,例子如下:
int main()
{
try
{
cout << "在 try block 中, 准备抛出一个int数据类型的异常." << endl;
throw 1;
}
catch( int& value )
{
cout << "在 catch block 中, int数据类型处理异常错误。”<< endl;
}
//这里是二个trycatch结构块,当然也可以有第三、第四个,甚至更多
try
{
cout << "在 try block 中, 准备抛出一个double数据类型的异常." << endl;
throw 0.5;
}
catch( double& d_value )
{
cout << "在 catch block 中, double数据类型处理异常错误。”<< endl;
}
return 0;
}
4、上面提到一个try block可以有多个对应的catch block,这样便于不同的异常错误分类处理,其实这只是异常错误分类处理的方法之一(暂且把它叫做横向展开的吧!)。另外还有一种就是纵向的,也即是分层的、trycatch块是可以嵌套的,当在低层的trycatch结构块中不能匹配到相同类型的catch block时,它就会到上层的trycatch块中去寻找匹配到正确的catch block异常处理模块。例程如下:
int main()
{
try
{
//这里是嵌套的trycatch结构块
try
{
cout << "在 try block 中, 准备抛出一个int数据类型的异常." << endl;
throw 1;
}
catch( int& value )
{
cout << "在 catch block 中, int数据类型处理异常错误。”<< endl;
}
cout << "在 try block 中, 准备抛出一个double数据类型的异常." << endl;
throw 0.5;
}
catch( double& d_value )
{
cout << "在 catch block 中, double数据类型处理异常错误。”<< endl;
}
return 0;
}
5、讲到是trycatch块是可以嵌套分层的,并且通过异常对象的数据类型来进行匹配,以找到正确的catch block异常错误处理代码。这里就不得不详细叙述一下通过异常对象的数据类型来进行匹配找到正确的catch block的过程。
(1)首先在抛出异常的trycatch块中查找catch block,按顺序先是与第一个catch block块匹配,如果抛出的异常对象的数据类型与catch block中传入的异常对象的临时变量(就是catch语句后面参数)的数据类型完全相同,或是它的子类型对象,则匹配成功,进入到catch block中执行;否则到二步;
(2)如果有二个或更多的catch block,则继续查找匹配第二个、第三个,乃至最后一个catch block,如匹配成功,则进入到对应的catch block中执行;否则到三步;
(3)返回到上一级的trycatch块中,按规则继续查找对应的catch block。如果找到,进入到对应的catch block中执行;否则到四步;
(4)再到上上级的trycatch块中,如此不断递归,直到匹配到顶级的trycatch块中的最后一个catch block,如果找到,进入到对应的catch block中执行;否则程序将会执行terminate()退出。另外分层嵌套的trycatch块是可以跨越函数作用域的,例程如下:
void Func() throw()
{
//这里实际上也是嵌套在里层的trycatch结构块
try
{
cout << "在 try block 中, 准备抛出一个int数据类型的异常." << endl;
//由于这个trycatch块中不能找到匹配的catch block,所以
//它会继续查找到调用这个函数的上层函数的trycatch块。
throw 1;
}
catch( float& value )
{
cout << "在 catch block 中, int数据类型处理异常错误。”<< endl;
}
}
int main()
{
try
{
Func();
cout << "在 try block 中, 准备抛出一个double数据类型的异常." << endl;
throw 0.5;
}
catch( double& d_value )
{
cout << "在 catch block 中, double数据类型处理异常错误。”<< endl;
}
catch( int& value )
{
//这个例子中,Func()函数中抛出的异常会在此被处理
cout << "在 catch block 中, int数据类型处理异常错误。”<< endl;
}
return 0;
}
6、刚才提到,嵌套的trycatch块是可以跨越函数作用域的,其实这里面还有另外一层涵义,就是抛出异常对象的函数中并不一定必须存在 trycatch块,它可以是调用这个函数的上层函数中存在trycatch块,这样这个函数的代码也同样是受保护、受监控的代码;当然即便是上层调用函数不存在trycatch块,也只是不能找到处理这类异常对象错误处理的catch block而已,例程如下:
void Func() throw()
{
//这里实际上也是嵌套在里层的trycatch结构块
//由于这个函数中是没有trycatch块的,所以它会查找到调用这个函数的上
//层函数的trycatch块中。
throw 1;
}
int main()
{
try
{
//调用函数,注意这个函数里面抛出一个异常对象
Func();
cout << "在 try block 中, 准备抛出一个double数据类型的异常." << endl;
throw 0.5;
}
catch( double& d_value )
{
cout << "在 catch block 中, double数据类型处理异常错误。”<< endl;
}
catch( int& value )
{
//这个例子中,Func()函数中抛出的异常会在此被处理
cout << "在 catch block 中, int数据类型处理异常错误。”<< endl;
}
//如果这里调用这个函数,那么由于main()已经是调用栈的顶层函数,因此不能找到对应的catch block,所以程序会执行terminate()退出。
Func();
// [特别提示]:在C++标准中规定,可以在程序任何地方throw一个异常对象,并不要求一定只能是在受到try block监控保护的作用域中才能抛出异常,但如果在程序中出现了抛出的找不到对应catch block的异常对象时,C++标准中规定要求系统必须执行terminate()来终止程序。因此这个例程是可以编译通过的,但运行时却会异常终止。这往往给软件系统带来了不安全性。与此形成对比的是java中提供的异常处理模型却是不允许出现这样的找不到对应catch block的异常对象,它在编译时就给出错误提示,所以java中提供的异常处理模型往往比C++要更完善,后面的章节会进一步对这两种异常处理模型进行一个详细的分析比较。
return 0;
}
朋友们!C++中的异常处理模型的语法很简单吧!就是那么(one、two、three、…哈哈!数数呢!)简单的几条规则。怪不得主人公阿愚这么快就喜欢上她了,而且还居然像一个思想家一样总结出一条感想:好的东西往往都是简单的,简单就是美吗!哈哈!还挺臭美的。
为什么要用catch(…)这个东东?程序员朋友们也许会说,这还有问吗?这篇文章的一开始不就讲到了吗?catch(…)能够捕获多种数据类型的异常对象,所以它提供给程序员一种对异常对象更好的控制手段,使开发的软件系统有很好的可靠性。因此一个比较有经验的程序员通常会这样组织编写它的代码模块,如下:
void Func()
{
try
{
// 这里的程序代码完成真正复杂的计算工作,这些代码在执行过程中
// 有可能抛出DataType1、DataType2和DataType3类型的异常对象。
}
catch(DataType1& d1)
{
}
catch(DataType2& d2)
{
}
catch(DataType3& d3)
{
}
// 注意上面try block中可能抛出的DataType1、DataType2和DataType3三种类型的异常对象在前面都已经有对应的catch block来处理。但为什么还要在最后再定义一个catch(…) block呢?这就是为了有更好的安全性和可靠性,避免上面的try block抛出了其它未考虑到的异常对象时导致的程序出现意外崩溃的严重后果,而且这在用VC开发的系统上更特别有效,因为catch(…)能捕获系统出现的异常,而系统异常往往令程序员头痛了,现在系统一般都比较复杂,而且由很多人共同开发,一不小心就会导致一个指针变量指向了其它非法区域,结果意外灾难不幸发生了。catch(…)为这种潜在的隐患提供了一种有效的补救措施。
catch(…)
{
}
}
还有,特别是VC程序员为了使开发的系统有更好的可靠性,往往在应用程序的入口函数中(如MFC框架的开发环境下 CXXXApp::InitInstance())和工作线程的入口函数中加上一个顶层的trycatch块,并且使用catch(…)来捕获一切所有的异常,如下:
BOOL CXXXApp::InitInstance()
{
if (!AfxSocketInit())
{
AfxMessageBox(IDP_SOCKETS_INIT_FAILED);
return FALSE;
}
AfxEnableControlContainer();
// Standard initialization
// If you are not using these features and wish to reduce the size
// of your final executable, you should remove from the following
// the specific initialization routines you do not need.
#ifdef _AFXDLL
Enable3dControls(); // Call this when using MFC in a shared DLL
#else
Enable3dControlsStatic(); // Call this when linking to MFC statically
#endif
// 注意这里有一个顶层的trycatch块,并且使用catch(…)来捕获一切所有的异常
try
{
CXXXDlg dlg;
m_pMainWnd = &dlg;
int nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
// TODO: Place code here to handle when the dialog is
// dismissed with OK
}
else if (nResponse == IDCANCEL)
{
// TODO: Place code here to handle when the dialog is
// dismissed with Cancel
}
}
catch(…)
{
// dump出系统的一些重要信息,并通知管理员查找出现意外异常的原因。同时想办法恢复系统,例如说重新启动应用程序等
}
// Since the dialog has been closed, return FALSE so that we exit the
// application, rather than start the application's message pump.
return FALSE;
}
通过上面的例程和分析可以得出,由于catch(…)能够捕获所有数据类型的异常对象,所以在恰当的地方使用catch(…)确实可以使软件系统有着更好的可靠性。这确实是大家使用catch(…)这个东东最好的理由。但不要误会的是,在C++异常处理模型中,不只有catch(…)方法能够捕获几乎所有类型的异常对象(也许有其它更好的方法,在下一篇文章中主人公阿愚带大家一同去探讨一下),可C++标准中为什么会想到定义这样一个 catch(…)呢?有过java或C#编程开发经验的程序员会发现,在它们的异常处理模型中,并没有这样类似的一种语法,可这里不得不再次强调的是,java中的异常处理模型是C++中的异常处理模型的完善改进版,可它反而没有了catch(…),为何呢?还是先去看看下一章吧,“C++的异常处理和面向对象的紧密关系”。也许大家能找到一个似乎合理的原因。
如果有人问起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)
{ /
/ 处理这个异常,如果这个异常可以很好的得以恢复,那么处理完毕后函数正常返回;否则必须重新抛出这个异常,以供上层的调用函数来能再次处理这个异常对象 i
nt 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(…)的东东,完全没必要。