C++异常深入剖析
C++异常机制由两部分组成,即抛出端和处理端。当程序抛出一个异常,控制权不会再回到抛出端。
在抛出端,我们可以抛出一个数或对象,也可以抛出一个指针。
值得注意的是:被抛出的数、对象或指针都会产生一个副本,这个副本最后被传递给处理端。
完成抛出动作之后,原来的数、对象或指针会随之被销毁。
因此要保证此时指针所指向的内存区域不会被回收,否则,抛出的指针将指向一块无效内存区域。
另外,这个副本是对象的静态类型(不含有动态运行时特性)的拷贝构造函数创建出来的。
// 最后会在屏幕上打印出"A" // 表明在抛出端产生对象副本时,调用的是A的拷贝构造函数 class A { public: A(){} A(const A& a) { printf("A\n"); } ~A(){} }; class B : public A { public: B(){} B(const B& a) { printf("B\n"); } ~B(){} }; void throwFunction() { B b; A& a = b; throw a; // 此时会调用A的拷贝构造函数,而不是B的拷贝构造函数 } int main(int argc, char* argv[]) { try{ throwFunction(); } catch(const A& e) { // ... ... } return 0; }
++++++++++++++++++++++++++++++++++++++++++++
从以上我们得知,在throw XX时,会进行一次静态拷贝操作。
为了避免进行第二次拷贝,在处理端应使用引用接受被抛出来的对象,这个过程和函数参数传递类似。
如果程序没有处理异常,这该异常将由操作系统来接管,在Debug时,会弹出未处理的异常窗口,程序也因此中断运行。
需要注意的是,抛出端和处理端必须处于同一个模块内,异常才能被捕捉到。
例如:在try块中调用第三方dll的一个函数,并在其中发生了异常,那么catch块是捕捉不到该异常的。
关于处理端有五点值得注意:
(1) 对基本类型,不存在隐式转换
例如:throw出int类型的数时,catch(double a)捕捉块是捕捉不到该异常的。
(2) 在抛出对象时,对于多个catch块情况,应先将子类写在前,父类写在后
因为在匹配时,是从前往后匹配的。
(3) 当某一catch被匹配上
若在该catch块中直接throw,将导致该异常对象被重新抛出,此时不会进行拷贝。只是将该异常对象继续传递给下面的catch块。
若在该catch块中throw XX,将会对XX对象进行静态拷贝。并将拷贝后的副本继续传递给下面的catch块。
(4) catch(const void* a)可以捕捉任何指针类型的异常
(5) catch(...)可以捕捉任何异常,如果需要用到这个catch块,确保该块放在末尾
class A { public: A(){} ~A(){} }; class B : public A { public: B(){} ~B(){} }; void throwFunction() { B b; throw b; } int main(int argc, char* argv[]) { try{ throwFunction(); } catch(const B& e) // 子类写在父类前面 { // ... ... } catch(const A& e) { // ... ... } catch(...) { // ... ... } return 0; }
============================
Unwinding:现代编译器基本都支持unwinding功能,这使得在一个异常发生时,会将堆栈中的所有对象都析构掉,防止资源泄漏。
关于C++异常更多细节请参看:C++异常机制的实现方式和开销分析