|
![]() | 作者: tysx [tysx]
![]() |
登录 |
大型面向对象应用程序中,经常涉及到例外的处理。例外的处理机理可以看成为对程序源代码进行编译时的类型诊断(compile-time type checking)的补充和进一步支持。使用例外处理使得应用方案的设计更方便、具体。正因为例外的处理对大型应用工程有重大影响,所以对例外处理的研究就更有必要。 C++提供了一个非常好的例外处理方法。通过这种方法,一个被调用函数告知(inform)调用函数某种错误出现在被调用函数中。C++中的例外处理可以涵盖到对任何错误的处理,不论是内存分配失败还是程序运行过程中类型转换错误。例外处理提供了一种将控制和信息从错误发生点切换到例外处理点的方法。当一个函数出现一个错误而它自身不能解决时,这个函数提出(throw)一个例外(exception), 希望它的直接或间接调用函数能够处理这个错误。一个函数可以通过接收(catch)一个例外来表明它希望处理这种例外。 1. C++对例外处理的支持 C++提供了三个关键词来对例外进行处理。 (1) try--可能提出例外的某段程序必须以try开始。紧跟着try的是一段包含在花括号中的程序。这段程序表明它可以诊断有否任何例外提出。 (2) throw--一个例外是通过关键词throw来提出的。提出一个例外将程序的控制点切换到例外处理句柄(exception handler)。同时在转换过程中,一个例外对象也被从例外提出点传送到例外处理点。例外对象的类型决定了哪一个处理句柄可以接受这一例外。 (3) catch--处理例外的程序必须以catch开始。跟随在catch后面的是一段包含在花括号中的程序。它的 结构为 catch (/* 例外类型 */) { /* 例外处理 */ } 这个结构称为例外句柄(exception handler)。它只能紧随在try程序块后或者另外一个例外句柄后面。 2. try-throw-catch 工作机理 提出一个例外将程序的控制点切换到最临近的合适类型的例外处理句柄。如此同时,一个例外对象也被从此控制点传送到例外句柄。这个对象的静态类型决定了哪一个句柄来接收和处理这个例外。下面的例子给出了如何提出和处理一个例外的例子。 typedef void (*CO_PFN)(); void fn() { cout << "in fn()" << endl; //其它表达语句 } void f(int i) { try { if (i == 0) throw "Help!"; if (i == 13) throw 13; if (i == 169) throw fn; } catch (const char* pChar) { cout << "Please " << pChar << endl; //其它表达语句 } catch(int v) { cout << "oops..." << v << endl; //其它表达语句 } catch (CO_PFN pfn) { pfn(); } //其它表达语句 return; } int main() { f(0); f(13); f(169); return 0; } 函数f(0)在执行下列语句 throw "Help"; 时将控制点切换到(const char *)类型的例外处理句柄。因此你见到下列输出结果。 Please Help! 类似地,函数f(13)和f(169)的输出结果为 oops...13 in fn() 所以,C++提供了一种当例外出现时非常巧妙地中断程序流程的方法。 当例外被提出时,程序采取步骤: (1) 寻找类型匹配的句柄。 (2) 如果找到了一个合适的句柄,程序自动清除堆栈并将控制点切换到此句柄。 (3) 如果没有找到一个合适的句柄,程序执行terminate()函数来中断运行。 当然如果没有例外被提出,程序以正常方式执行。 3. 面向对象的例外 尽管C++允许给出任何类型的例外,但是提出类(class)类型的例外更有用。一种类型E的例外可以被类型T的处理句柄接收和处理,假若类型E是以类型T为基类派生来的。这样就使得你能够通过多形性(polymorphism)设计出一种统一的实用例外类类型。 class CoWindowException { public: CoWindowException(); virtual int repaint(); }; class CoDialogException:public CoWindowException { public: CoDialogException(); virtual int repaint(); }; showWindow() { //某些表达语句 //这里出现错误 throw CoDialogException(); //其它表达语句 } void f() { try { showWindow(); } catch (CoWindowException& WinExc) { WinExc.repaint(); } } 在上述例子中,f()函数在调用showWindow()函数时,showWindow()函数提出了CoDialogException类例外。 但是f()函数中的CoWindowException类例外处理句柄将调用CoDialogException类中的虚拟函数repaint(),而不是CoWindowException类中的。 当提出一个类类型的例外时,这个类的拷贝构造子在提出点被用来初始化一个临时对象。如果你将这个类的拷贝构造子定义为非公用的(non-public),这个类就不能被用来作为例外类。这是因为编译器没有办法找到一个公用的(public)拷贝构造子。这一点在你建立类仓库时很有用。假若你建立了一系列类和相应的例外类,但你不想将其中的部分例外类提供给客户使用,而想留作内部使用,你可以用这种方法来实现。 4. 例外的再提出(rethrowing) 有时候会发生一个例外处理句柄接收了一个例外却发现不能处理这个例外的情况。这时,这个例外可以被再提出以便于其它句柄能够更好的处理。例外的再提出可以通过一个空的throw表达语句来实现。但是,这种表达语句只能出现于一个例外处理句柄中。例如, void f() { try { showWindow(); //提出CoDialogException类例外 } catch (CoWindowException& WinExc) { WinExc.repaint(); throw; //例外再提出 } } void g() { try { f(); } catch (CoDialogException& DialogExc) { /*例外处理语句*/ } catch (CoWindowException& WindowExc) { /*例外处理语句*/ } } 上述例子中,尽管CoDialogException类例外是由函数f()中的CoWindowException类处理句柄再提出的,但是它仍然由函数g()中的CoDialogException类例外处理句柄来处理。 此外,任何例外都可以通过一种特殊的接收方式catch(...)来接收和处理。例如下面例子中的f()函数可以接收任何例外并再提出。 void f() { try { showWindow(); } catch(...) //接收任何例外 { //某些处理语句 throw; } } 值得注意的是,例外的再提出并不对例外对象进行进一步的拷贝。 5. 在例外对象中携带更多的信息 例外对象如同其它类对象一样可以携带信息。所以一个例外对象可以被用来将一些有用信息从提出点携带到接收处理点。这些信息可以是当程序在运行过程中出现非正常情况时程序使用者想要知道的。例如一个程序使用者可能想知道一个矢量的下标,当这个矢量下标超限时。 class CoVector { public: CoVector(int); class Range; int& operator[] (int i); protected: int* pInt_; int theSize; }; class CoVector::Range { public: CoVector::Range(int); int index_; }; CoVector::Range::Range(int i):index_(i) { /* ... */ } int& CoVector::operator[](int i) { if (0 <= i && i << theSize_) return pInt_[i]; throw Range(i); } void f(const CoVector& v) { try { int temp = v[169]; } catch (const CoVector::Range& r) { cout << "bad index = " << r.index_ << endl; } } 实际上,catch后面括弧中的表达语句实际上类似于函数的参数定义。 6. 句柄的次序 例外处理句柄有先后次序之分。因为一个派生(derived)例外类对象可以被几个句柄接收,所以在排列例外处理句柄顺序时应该特别小心。另外,一个类型严格匹配的处理句柄并不比一个需要类型转换的处理句柄更有优先权。例如下面的例子就很糟糕。 class CoWindow { /* ... */ }; class CoButton:public CoWindow { /* ... */ }; void f(int v) { typedef void (*PCF)(const char*); try { if (v) throw &v; //其它表达语句 } catch (void * pVoid) { /* ... */ } catch (PCF pFunction) { /* ... */ } catch (const CoWindow& win) { /* ... */ } catch (const CoButton& button) { /* ... */ } catch (...) { /* ... */ } return; } 在上面例子中,(void *)处理句柄不可能允许它后面的PCF处理句柄被调用。类似的,因为CoWindow类处理句柄将会接收任何CoWindow类及它的衍生类对象,所以CoButton类的处理句柄也不会被调用。依赖于你所使用的编译器,有的编译器可能在编译时警告你一个从类B派生来的类D句柄放在类B句柄后面。但是,如果一个接收任何例外的句柄catch(...)不是最后一个句柄,编译器会给出编译错误。 7. 例外提出过程中的对象构造和析构 当一个程序由于例外而中断时,所有的从try开始构造的自动变量类的对象都会被清除、释放。这种调用自动变量类的析构函数的过程称为堆栈清除(stack unwinding)。下面给出了一个实例。 class CoClass { public: int v_; CoClass(int v = 0) : v_(v) { cout << "CoClass(int): " << v_ << endl; } ~CoClass() { cout << "~CoClass(): " << v_ << endl; } }; class CoError { public: int v_; CoError(int v = 0):v_(v) { cout << "CoError(int): " << v_ << endl; } CoError(const CoError& ve):v_(ve.v_) { cout << "CoError(const CoError&): " << v_ << endl; } ~CoError() { cout << "~CoError(): " << v_ << endl; } }; int f(int v) { if (v == 13) { CoClass vc(0); throw CoError(v); } return v; } int main() { try { CoClass vc(169); f(13); } catch (const CoError& e) { cout << "Caught : " << e.v_ << endl; } return 0; } 这个例子给出了下面输出结果。 CoClass(int): 169 CoClass(int): 0 CoError(int): 13 CoError(const CoError&): 13 ~CoError(): 13 ~CoClass(): 0 ~CoClass(): 169 Caught : 13 ~CoError(): 13 当一个例外在一个类对象的构造过程中被提出时,如果这个类对象中包含了其它成员类对象,那么那些在例外提出前完成了构造的成员类对象的析构函数会被调用来清除已构造了的成员对象。而那些没有完成构造的成员类对象的析构函数不会被调用。例如,如果在构造一个类对象的数组过程中一个例外被提出,那么只有那些完成了构造的对象的析构函数才被调用来进行堆栈清除。 接收一个析构函数提出的例外也是可能的。将调用析构函数的函数放在try后面的程序块中,并且提供适当的类型句柄就可以了。 8. 函数的例外规范 一个函数的参数串称之为这个函数的签名(signature),因为它经常被用来区别一个函数实体。一个函数由四个部分组成。(1)返回类型。(2)函数名。(3)函数签名。(4)函数体。前三个部分常称作为函数原型(function prototype).函数原型提供给了编译器关于这个函数的类型信息。编译器通过这些信息进行类型诊断。通常一个函数的使用者只需要知道这个函数原型就可以了。但是如果使用者想写这个函数所提出的所有例外句柄,那么这个使用者就需要知道这些例外类型。 要解决这个问题,一个函数原型或者定义(definition)可以包括例外规范(exception specifications)。例外规范出现在一个函数定义后面。它列出了这个函数可以直接或间接提出的例外。例如下面给出了一些函数的原型 void f() throw(v1, v2, v3); void g(); void h() throw(); void e() throw(WClass *); f()的定义表明了这个函数可以提出类型v1,v2和v3,以及它们的派生类型。如果函数f()提出除此以外的其它例外,程序在运行中会产生一个运行错误。函数g()的定义没有包含例外规范。这意味着它可以提出任何例外。而函数h()则不能提出任何例外。函数e()可以提出WClass类的指针型例外,或者由WClass派生来的类指针例外。 一个函数后面的例外规范并不属于这个函数指针中的一部分。然而当一个指向函数的指针赋值给指向另一个函数的指针时,只有在被赋值的指针函数包含了赋值指针函数的例外时才允许。例如 void (*pf1)(); //没有例外规范 void (*pf2)() throw(int); //只能提出整型例外 void f() { pf1 = pf2; //可以,pf1要求比较宽松 pf2 = pf1; //错误,pf2要求更严格 } 下面函数 void f() throw(v1,v2,v3) { SomeAction(); } 相当于 void f() { try { SomeAction(); } catch(v1 v) { throw; //在提出 } catch(v2 v) { throw; //在提出 } catch(v3 v) { throw; //在提出 } catch(...) { unexpected(); //定义在头文件中 } } 当一个函数提出的例外没有包括在这个函数的例外规范中时,程序会自动调用函数unexpected()来处理这个例外。函数unexpected()的缺省行为是调用使用者通过set_unexpected()函数注册的函数。假如没有任何函数通过set_unexcepted()来注册,函数unexpected()则调用函数terminate()。这三个函数的原型如下。 void unexpected(); typedef void (*unexpected_handler)(); unexpected_handler set_unexpected(unexpected_handler) throw(); void terminate(); 函数set_unexpected()的返回值是前一次通过set_unexpected()注册的函数指针。 函数terminate()可以通过unexpected()来调用,或者当找不到一个例外句柄时程序自动调用。terminate()函数的缺省行为是调用函数abort()。这个缺省函数即刻中止程序运行。你可以改变当一个例外没有出现于函数的例外规范中时的程序中止方式。如果你不想你的程序通过调用abort()来中止,你可以定义另外一个函数来替代它。这样的函数通过set_terminate()函数来注册并由terminate()调用。函数set_terminate()的定义如下 typedef void (*terminate_handler)(); terminate_handler set_terminate(terminate_handler) throw(); 这个函数的返回值是前一次通过函数set_terminate()来注册的函数指针。 头文件定义了处理例外的函数,类及其数据成员和成员函数。 9. 具有例外规范的虚拟函数 一个虚拟函数也可以定义例外规范。但是,一个子类中的虚拟函数提出的例外不能比父类中的虚拟函数提出的例外多。也就是说,一个子类中的虚拟函数规范不能比父类中的虚拟函数规范更宽松。而一个子类中的虚拟函数提出的例外比父类中的虚拟函数提出的例外少则是允许的。例如 class CoWindow { public: virtual void show() throw(int); virtual void getFocus() throw(int, char); }; class CoDialogBox:public CoWindow { public: virtual void show() throw(int, char); //错误,例外规范较宽松 virtual void getFocus() throw(int); //可以,例外规范更严历 }; 编译器针对CoDialog::show()会给出编译错误,因为在运行时这个函数既可以提出int类型例外也可以提出char类型例外。而在父类定义中,这个虚拟函数只能提出int类型例外。另一方面,CoDialog::getFocus()是允许的,因为它的规范比父类中的虚拟函数的例外规范要求更严历。 10. C++中的其它错误处理方法 以上,我们讨论了C++中的例外处理。C++同时提供了一些其它错误处理方法。这里略述如下。 (1) 使用assert()来测试编程和设计错误。如果测试结果为非,程序中止运行。为了保证程序能够运行,程序源代码中的错误必须纠正。这在程序调试时非常有用。 (2) 简单忽略错误。这一方法对个人开发的私用程序是可以的。但是对一个面向市场、大众的软件产品却是致命的。 (3) 中止程序运行。此方法中断程序运行,防止错误结果产生。实际上这种方法对某些类型错误,特别是非致命性的(nonfatal)错误,不惜为一种好的选择。但是这种方法对于重要的应用程序是不合适的。 (4) 设置一些错误指示,例如errno。但是问题是你不可能在程序的每一点都来对这些错误指示做出检查。 (5) 对错误进行检查。如果出现错误,向用户提供一个关于这个错误的信息,并调用exit()将错误代码传送给运行环境。 (6) setjump和longjump。这使得程序很容易切换到错误处理点。如果没有setjump/longjump,程序必须执行几个返回语句才能回到最初调用函数。使用setjump/longjump则很容易切换到错误处理句柄。但是这也带来一定危险,因为在这个切换过程中,C++并没有调用析构函数来进行堆栈清除。 (7) 某些特定的错误有自己的处理句柄。例如,当使用new操作符分配内存空间失败时,程序会自动调用new_handler()函数来处理这个错误。而这个函数的行为可以通过函数set_new_handler()注册的函数来改变。 11. 小结 C++提供了多种方法来处理程序错误。最常用的方法是解释系统错误代码,并在错误发生的地方对错误进行处理。这种方法的优点是程序员可以在错误发生点对错误进行直接诊断和处理,并决定这种诊断和处理是否适当。但是这个方法的缺点也是明显的。这就是错误处理代码和程序应用代码混合在一起。这使得程序员很难将精力集中在程序应用代码上。同时也降低了程序的可读性,增加了程序的维护费用。而C++中的例外处理则允许将错误处理代码与程序应用代码分开。这增加了程序的可读性和维护性。使用例外处理的另一个好处是接收所有类型的例外,或者一定类型的所有例外,或者相关类型的所有例外成为可能。例外处理使得程序能够接收和处理所有的错误而不让它们出现。如果一个程序对一个致命错误(fatal error)没有提供合适的处理句柄,这个程序就会停止运行。 摘自:计算机世界网 |
地主 发表时间: 04/06 20:10 |
|
20CN网络安全小组版权所有
Copyright © 2000-2010 20CN Security Group. All Rights Reserved.
论坛程序编写:NetDemon
粤ICP备05087286号