所谓异常处理,即让一个程序运行时遇到本身没法处理的错误时抛出一个异常,但愿调用者能够发现处理问题.ios
异常处理的基本思想是简化程序的错误代码,为程序键壮性提供一个标准检测机制.c++
也许咱们已经使用过异常,可是你习惯使用异常了吗?程序员
如今不少软件都是n*365*24小时运行,软件的健壮性相当重要. 编程
C++标准异常:windows
也许你很高兴看到错误以后的Heap/Stack中对象被释放,但是若是没有呢?数组
又或者试想一下一个能解决的错误,须要咱们把整个程序Kill掉吗?安全
在《C++标准异常》中我向你推荐这几章:数据结构
<使用异常规格编程> <构造和析构中的异常抛出> <使用析构函数防止资源泄漏>,以及深刻一点的<抛出一个异常的行为>.app
SEH异常:dom
我要问你你是一个WIN32程序员吗?若是不是,那么也许你真的不须要看.
SEH是Windows的结构化异常,每个WIN32程序员都应该要掌握它.
SEH功能强大,包括Termination handling和Exception handling两大部分.
强有力的维护了代码的健壮,虽然要以部分系统性能作牺牲(其实能够避免).
在SEH中有大量的代码,已经在Win平台上测试过了.
这里要提一下:在__finally处理中编译器参与了绝大多数的工做,而Exception则是OS接管了几乎全部的工做,也许我没有提到的是:
对__finally来讲当遇到ExitThread/ExitProcess/abort等函数时,finally块不会被执行.
另:<使用析构函数防止资源泄漏>这个节点引用了More effective C++的条款9.
用2个列子,讲述了咱们通常都会犯下的错误,每每这种错误是咱们没有意识到的但确实是会给咱们的软件带来致命的Leak/Crash,但这是有解决的方法的,那就是使用“灵巧指针”.
若是对照<More effective C++>的37条条款,关于异常的高级使用,有如下内容是没有完成的:
C++异常
C++引入异常的缘由:
例如使用未经处理的pointer变的很危险,Memory/Resource Leak变的更有可能了.
写出一个具备你但愿的行为的构造函数和析构函数也变的困难(不可预测),固然最危险的也许是咱们写出的东东狗屁了,或者是速度变慢了.
大多数的程序员知道Howto use exception 来处理咱们的代码,但是不少人并非很重视异常的处理(国外的不少Code却是处理的很好,Java的Exception机制很不错).
异常处理机制是解决某些问题的上佳办法,但同时它也引入了许多隐藏的控制流程;有时候,要正确无误的使用它并不容易.
在异常被throw后,没有一个方法可以作到使软件的行为具备可预测性和可靠性
对C程序来讲,使用Error Code就能够了,为何还要引入异常?由于异常不能被忽略.
若是一个函数经过设置一个状态变量或返回错误代码来表示一个异常状态,没有办法保证函数调用者将必定检测变量或测试错误代码.
结果程序会从它遇到的异常状态继续运行,异常没有被捕获,程序当即会终止执行.
在C程序中,咱们能够用int setjmp( jmp_buf env );和 void longjmp( jmp_buf env, int value );
这2个函数来完成和异常处理相识的功能,可是MSDN中介绍了在C++中使用longjmp来调整stack时不可以对局部的对象调用析构函数,
可是对C++程序来讲,析构函数是重要的(我就通常都把对象的Delete放在析构函数中).
因此咱们须要一个方法:
而C++的异常处理恰好就是来解决这些问题的.
有的地方只有用异常才能解决问题,好比说,在当前上下文环境中,没法捕捉或肯定的错误类型,咱们就得用一个异常抛出到更大的上下文环境当中去.
还有,异常处理的使用呢,可使出错处理程序与“一般”代码分离开来,使代码更简洁更灵活.
另外就是程序必不可少的健壮性了,异常处理每每在其中扮演着重要的角色.
C++使用throw关键字来产生异常,try关键字用来检测的程序块,catch关键字用来填写异常处理的代码.
异常能够由一个肯定类或派生类的对象产生。C++能释放堆栈,并可清除堆栈中全部的对象.
C++的异常和pascal不一样,是要程序员本身去实现的,编译器不会作过多的动做.
throw异常类编程,抛出异经常使用throw, 如:
throw ExceptionClass(“my throw“);
例句中,ExceptionClass是一个类,它的构造函数以一个字符串作为参数.
也就是说,在throw的时候,C++的编译器先构造一个ExceptionClass的对象,让它做为throw的值抛出去,同时,程序返回,调用析构.
看下面这个程序:
#include <iostream.h> class ExceptionClass { char* name; public: ExceptionClass(const char* name="default name") { cout<<"Construct "<<name<<endl; this->name=name; } ~ExceptionClass() { cout<<"Destruct "<<name<<endl; } void mythrow() { throw ExceptionClass("my throw"); } } void main() { ExceptionClass e("Test"); try { e.mythrow(); } catch(...) { cout<<”*********”<<endl; } }
这是输出信息:
Construct Test Construct my throw Destruct my throw **************** Destruct my throw (这里是异常处理空间中对异常类的拷贝的析构) Destruct Test ======================================
不过通常来讲咱们可能更习惯于把会产生异常的语句和要throw的异常类分红不一样的类来写,下面的代码能够是咱们更愿意书写的.
class ExceptionClass { public: ExceptionClass(const char* name="Exception Default Class") { cout<<"Exception Class Construct String"<<endl; } ~ExceptionClass() { cout<<"Exception Class Destruct String"<<endl; } void ReportError() { cout<<"Exception Class:: This is Report Error Message"<<endl; } }; class ArguClass { char* name; public: ArguClass(char* name="default name") { cout<<"Construct String::"<<name<<endl; this->name=name; } ~ArguClass() { cout<<"Destruct String::"<<name<<endl; } void mythrow() { throw ExceptionClass("my throw"); } }; _tmain() { ArguClass e("haha"); try { e.mythrow(); } catch(int) { cout<<"If This is Message display screen, This is a Error!!"<<endl; } catch(ExceptionClass pTest) { pTest.ReportError(); } catch(...) { cout<<"***************"<<endl; } }
输出Message:
Construct String::haha Exception Class Construct String Exception Class Destruct String Exception Class:: This is Report Error Message Exception Class Destruct String Destruct String::haha
使用异常规格编程
若是咱们调用别人的函数,里面有异常抛出,用去查看它的源代码去看看都有什么异常抛出吗?这样就会很烦琐.
比较好的解决办法,是编写带有异常抛出的函数时,采用异常规格说明,使咱们看到函数声明就知道有哪些异常出现。
异常规格说明大致上为如下格式:
void ExceptionFunction(argument…) throw(ExceptionClass1, ExceptionClass2, ….)
全部异常类都在函数末尾的throw()的括号中得以说明了,这样,对于函数调用者来讲,是一清二楚的。
注意下面一种形式:
void ExceptionFunction(argument…) throw()
代表没有任何异常抛出.
而正常的void ExceptionFunction(argument…)则表示:可能抛出任何一种异常,固然,也可能没有异常,意义是最普遍的.
异常捕获以后,能够再次抛出,就用一个不带任何参数的throw语句就能够了.
构造和析构中的异常抛出
这是异常处理中最要注意的地方了
先看个程序,假如我在构造函数的地方抛出异常,这个类的析构会被调用吗?可若是不调用,那类里的东西岂不是不能被释放了?
#include <iostream.h> #include <stdlib.h> class ExceptionClass1 { char* s; public: ExceptionClass1() { cout<<"ExceptionClass1()"<<endl; s=new char[4]; cout<<"throw a exception"<<endl; throw 18; } ~ExceptionClass1() { cout<<"~ExceptionClass1()"<<endl; delete[] s; } }; void main() { try { ExceptionClass1 e; } catch(...) {} }
结果为:
ExceptionClass1() throw a exception
在这两句输出之间,咱们已经给S分配了内存,但内存没有被释放(由于它是在析构函数中释放的).
应该说这符合实际现象,由于对象没有完整构造.
为了不这种状况,我想你也许会说:应避免对象经过自己的构造函数涉及到异常抛出.
即:既不在构造函数中出现异常抛出,也不该在构造函数调用的一切东西中出现异常抛出.
可是在C++中能够在构造函数中抛出异常,经典的解决方案是使用STL的标准类auto_ptr.
其实咱们也能够这样作来实现:
在类中增长一个 Init()以及 UnInit();成员函数用于进行容易产生错误的资源分配工做,而真正的构造函数中先将全部成员置为NULL,而后调用 Init();
并判断其返回值/或者捕捉 Init()抛出的异常,若是Init();失败了,则在构造函数中调用 UnInit(); 并设置一个标志位代表构造失败.
UnInit()中按照成员是否为NULL进行资源的释放工做.
那么,在析构函数中的状况呢?
咱们已经知道,异常抛出以后,就要调用自己的析构函数,若是这析构函数中还有异常抛出的话,则已存在的异常还没有被捕获,会致使异常捕捉不到.
标准C++异常类
C++有本身的标准的异常类.
一个基类
exception 是全部C++异常的基类.
class exception { public: exception() throw(); exception(const exception& rhs) throw(); exception& operator=(const exception& rhs) throw(); virtual ~exception() throw(); virtual const char *what() const throw(); };
派生了两个异常类
以上两个又分别有本身的派生类:
domain_error 报告违反了前置条件 invalid_argument 指出函数的一个无效参数 length_error 指出有一个产生超过NPOS长度的对象的企图(NPOS为size_t的最大可表现值 out_of_range 报告参数越界 bad_cast 在运行时类型识别中有一个无效的dynamic_cast表达式 bad_typeid 报告在表达式typeid(*p)中有一个空指针P
range_error 报告违反了后置条件
overflow_error 报告一个算术溢出
bad_alloc 报告一个存储分配错误
使用析构函数防止资源泄漏
这部分是一个经典和很日常就会遇到的实际状况,下面的内容大部分都是从More Effective C++条款中获得的.
假设,你正在为一个小动物收容所编写软件,小动物收容所是一个帮助小狗小猫寻找主人的组织.
天天收容所创建一个文件,包含当天它所管理的收容动物的资料信息,你的工做是写一个程序读出这些文件而后对每一个收容动物进行适当的处理(appropriate processing).
完成这个程序一个合理的方法是定义一个抽象类,ALA("Adorable Little Animal"),而后为小狗和小猫创建派生类.
一个虚拟函数processAdoption分别对各个种类的动物进行处理:
class ALA { public: virtual void processAdoption() = 0; ... }; class Puppy: public ALA { public: virtual void processAdoption(); ... }; class Kitten: public ALA { public: virtual void processAdoption(); ... };
你须要一个函数从文件中读信息,而后根据文件中的信息产生一个puppy(小狗)对象或者kitten(小猫)对象.
这个工做很是适合于虚拟构造器(virtual constructor),在条款25详细描述了这种函数.
为了完成咱们的目标,咱们这样声明函数:
// 从s中读动物信息, 而后返回一个指针 // 指向新创建的某种类型对象 ALA * readALA(istream& s);
你的程序的关键部分就是这个函数,以下所示:
void processAdoptions(istream& dataSource) { while(dataSource) { ALA *pa = readALA(dataSource); //获得下一个动物 pa->processAdoption(); //处理收容动物 delete pa; //删除readALA返回的对象 } }
这个函数循环遍历dataSource内的信息,处理它所遇到的每一个项目.
惟一要记住的一点是在每次循环结尾处删除ps.
这是必须的,由于每次调用readALA都创建一个堆对象.若是不删除对象,循环将产生资源泄漏。
如今考虑一下,若是pa->processAdoption抛出了一个异常,将会发生什么?
processAdoptions没有捕获异常,因此异常将传递给processAdoptions的调用者.
转递中,processAdoptions函数中的调用pa->processAdoption语句后的全部语句都被跳过,这就是说pa没有被删除.
结果,任什么时候候pa->processAdoption抛出一个异常都会致使processAdoptions内存泄漏,很容易堵塞泄漏.
void processAdoptions(istream& dataSource) { while(dataSource) { ALA *pa = readALA(dataSource); try { pa->processAdoption(); } catch(...) { // 捕获全部异常 delete pa; // 避免内存泄漏 // 当异常抛出时 throw; // 传送异常给调用者 } delete pa; // 避免资源泄漏 } // 当没有异常抛出时 }
可是你必须用try和catch对你的代码进行小改动.
更重要的是你必须写双份清除代码,一个为正常的运行准备,一个为异常发生时准备.
在这种状况下,必须写两个delete代码.
象其它重复代码同样,这种代码写起来使人心烦又难于维护,并且它看上去好像存在着问题.
不论咱们是让processAdoptions正常返回仍是抛出异常,咱们都须要删除pa,因此为何咱们必需要在多个地方编写删除代码呢?
咱们能够把总被执行的清除代码放入processAdoptions函数内的局部对象的析构函数里,这样能够避免重复书写清除代码.
由于当函数返回时局部对象老是被释放,不管函数是如何退出的.
(仅有一种例外就是当你调用longjmp时。Longjmp的这个缺点是C++率先支持异常处理的主要缘由)
具体方法是用一个对象代替指针pa,这个对象的行为与指针类似。当pointer-like(类指针)对象被释放时,咱们能让它的析构函数调用delete.
替代指针的对象被称为smart pointers(灵巧指针),下面有解释,你能使得pointer-like对象很是灵巧.
在这里,咱们用不着这么聪明的指针,咱们只须要一个pointer-lik对象,当它离开生存空间时知道删除它指向的对象.
写出这样一个类并不困难,可是咱们不须要本身去写。标准C++库函数包含一个类模板,叫作auto_ptr,这正是咱们想要的.
每个auto_ptr类的构造函数里,让一个指针指向一个堆对象(heap object),而且在它的析构函数里删除这个对象.
下面所示的是auto_ptr类的一些重要的部分:
template<class T> class auto_ptr { public: auto_ptr(T *p = 0): ptr(p) {} // 保存ptr,指向对象 ~auto_ptr() { delete ptr; } // 删除ptr指向的对象 private: T *ptr; // raw ptr to object };
auto_ptr类的完整代码是很是有趣的,上述简化的代码实现不能在实际中应用.
(咱们至少必须加上拷贝构造函数,赋值operator以及下面将要讲到的pointer-emulating函数)
可是它背后所蕴含的原理应该是清楚的:用auto_ptr对象代替raw指针,你将再也不为堆对象不能被删除而担忧,即便在抛出异常时,对象也能被及时删除.
(由于auto_ptr的析构函数使用的是单对象形式的delete,因此auto_ptr不能用于指向对象数组的指针.
若是想让auto_ptr相似于一个数组模板,你必须本身写一个。在这种状况下,用vector代替array可能更好)
auto_ptr template<class T> class auto_ptr { public: typedef T element_type; explicit auto_ptr(T *p = 0) throw(); auto_ptr(const auto_ptr<T>& rhs) throw(); auto_ptr<T>& operator=(auto_ptr<T>& rhs) throw(); ~auto_ptr(); T& operator*() const throw(); T *operator->() const throw(); T *get() const throw(); T *release() const throw(); };
使用auto_ptr对象代替raw指针,processAdoptions以下所示:
void processAdoptions(istream& dataSource) { while(dataSource) { auto_ptr<ALA> pa(readALA(dataSource)); pa->processAdoption(); } }
这个版本的processAdoptions在两个方面区别于原来的processAdoptions函数:
其他部分都同样,由于除了析构的方式,auto_ptr对象的行为就象一个普通的指针。是否是很容易.
隐藏在auto_ptr后的思想是:
用一个对象存储须要被自动释放的资源,而后依靠对象的析构函数来释放资源,这种思想不仅是能够运用在指针上,还能用在其它资源的分配和释放上.
想一下这样一个在GUI程序中的函数,它须要创建一个window来显式一些信息:
// 这个函数会发生资源泄漏,若是一个异常抛出 void displayInfo(const Information& info) { WINDOW_HANDLE w(createWindow());//在w对应的window中显式信息 destroyWindow(w); }
不少window系统有C-like接口,使用象like createWindow 和 destroyWindow函数来获取和释放window资源.
若是在w对应的window中显示信息时,一个异常被抛出,w所对应的window将被丢失,就象其它动态分配的资源同样.
解决方法与前面所述的同样,创建一个类,让它的构造函数与析构函数来获取和释放资源:
//一个类,获取和释放一个window 句柄 class WindowHandle { public: WindowHandle(WINDOW_HANDLE handle): w(handle) {} ~WindowHandle() { destroyWindow(w); } operator WINDOW_HANDLE() { return w; } // see below private: WINDOW_HANDLE w; // 下面的函数被声明为私有,防止创建多个WINDOW_HANDLE拷贝 //有关一个更灵活的方法的讨论请参见下面的灵巧指针 WindowHandle(const WindowHandle&); WindowHandle& operator=(const WindowHandle&); };
这看上去有些象auto_ptr,只是赋值操做与拷贝构造被显式地禁止(参见More effective C++条款27),有一个隐含的转换操做能把WindowHandle转换为WINDOW_HANDLE.
这个能力对于使用WindowHandle对象很是重要,由于这意味着你能在任何地方象使用raw WINDOW_HANDLE同样来使用WindowHandle.
(参见More effective C++条款5 ,了解为何你应该谨慎使用隐式类型转换操做)
经过给出的WindowHandle类,咱们可以重写displayInfo函数,以下所示:
// 若是一个异常被抛出,这个函数能避免资源泄漏 void displayInfo(const Information& info) { WindowHandle w(createWindow()); //在w对应的window中显式信息; }
即便一个异常在displayInfo内被抛出,被createWindow 创建的window也能被释放.
资源应该被封装在一个对象里,遵循这个规则,你一般就能避免在存在异常环境里发生资源泄漏.
可是若是你正在分配资源时一个异常被抛出,会发生什么状况呢?
例如当你正处于resource-acquiring类的构造函数中.
还有若是这样的资源正在被释放时,一个异常被抛出,又会发生什么状况呢?
构造函数和析构函数须要特殊的技术.
你能在More effective C++条款10和More effective C++条款11中获取有关的知识.
抛出一个异常的行为
我的认为接下来的这部分其实说的很经典,对咱们理解异常行为/异常拷贝是颇有帮助的.
条款12:理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差别
从语法上看,在函数里声明参数与在catch子句中声明参数几乎没有什么差异:
class Widget { ... }; //一个类,具体是什么类在这里并不重要 void f1(Widget w); // 一些函数,其参数分别为 void f2(Widget& w); // Widget, Widget&,或 void f3(const Widget& w); // Widget* 类型 void f4(Widget *pw); void f5(const Widget *pw); catch(Widget w) ... //一些catch 子句,用来 catch(Widget& w) ... //捕获异常,异常的类型为 catch(const Widget& w) ... // Widget, Widget&, 或 catch(Widget *pw) ... // Widget* catch(const Widget *pw) ...
你所以可能会认为用throw抛出一个异常到catch子句中与经过函数调用传递一个参数二者基本相同.
这里面确有一些相同点,可是他们也存在着巨大的差别.
让咱们先从相同点谈起.
你传递函数参数与异常的途径能够是传值、传递引用或传递指针,这是相同的.
可是当你传递参数和异常时,系统所要完成的操做过程则是彻底不一样的.
产生这个差别的缘由是:你调用函数时,程序的控制权最终还会返回到函数的调用处,可是当你抛出一个异常时,控制权永远不会回到抛出异常的地方。
有这样一个函数,参数类型是Widget,并抛出一个Widget类型的异常:
// 一个函数,从流中读值到Widget中 istream operator>>(istream& s, Widget& w); void passAndThrowWidget() { Widget localWidget; cin >> localWidget; //传递localWidget到 operator>> throw localWidget; // 抛出localWidget异常 }
当传递localWidget到函数operator>>里,不用进行拷贝操做,而是把operator>>内的引用类型变量w指向localWidget,任何对w的操做实际上都施加到localWidget上.
这与抛出localWidget异常有很大不一样.
不论经过传值捕获异常仍是经过引用捕获(不能经过指针捕获这个异常,由于类型不匹配)都将进行lcalWidget的拷贝操做,也就说传递到catch子句中的是localWidget的拷贝.
必须这么作,由于当localWidget离开了生存空间后,其析构函数将被调用.
若是把localWidget自己(而不是它的拷贝)传递给catch子句,这个子句接收到的只是一个被析构了的Widget,一个Widget的“尸体”.
这是没法使用的。所以C++规范要求被作为异常抛出的对象必须被复制.
即便被抛出的对象不会被释放,也会进行拷贝操做.
例如若是passAndThrowWidget函数声明localWidget为静态变量(static):
void passAndThrowWidget() { static Widget localWidget; // 如今是静态变量(static) 一直存在至程序结束 cin >> localWidget; // 象之前那样运行 throw localWidget; // 仍将对localWidget进行拷贝操做 }
当抛出异常时仍将复制出localWidget的一个拷贝.
这表示即便经过引用来捕获异常,也不能在catch块中修改localWidget;仅仅能修改localWidget的拷贝.
对异常对象进行强制复制拷贝,这个限制有助于咱们理解参数传递与抛出异常的第二个差别:抛出异常运行速度比参数传递要慢.
当异常对象被拷贝时,拷贝操做是由对象的拷贝构造函数完成的.
该拷贝构造函数是对象的静态类型(static type)所对应类的拷贝构造函数,而不是对象的动态类型(dynamic type)对应类的拷贝构造函数.
好比如下这通过少量修改的passAndThrowWidget:
class Widget { ... }; class SpecialWidget: public Widget { ... }; void passAndThrowWidget() { SpecialWidget localSpecialWidget; ... Widget& rw = localSpecialWidget; // rw 引用SpecialWidget throw rw; //它抛出一个类型为Widget的异常 }
这里抛出的异常对象是Widget,即便rw引用的是一个SpecialWidget.
由于rw的静态类型(static type)是Widget,而不是SpecialWidget.
你的编译器根本没有主要到rw引用的是一个SpecialWidget。编译器所注意的是rw的静态类型(static type).
这种行为可能与你所期待的不同,可是这与在其余状况下C++中拷贝构造函数的行为是一致的.
(不过有一种技术可让你根据对象的动态类型dynamic type进行拷贝,参见条款25)
异常是其它对象的拷贝,这个事实影响到你如何在catch块中再抛出一个异常.
好比下面这两个catch块,乍一看好像同样:
catch(Widget& w) // 捕获Widget异常 { ... // 处理异常 throw; // 从新抛出异常,让它 } // 继续传递 catch(Widget& w) // 捕获Widget异常 { ... // 处理异常 throw w; // 传递被捕获异常的 } // 拷贝
这两个catch块的差异在于第一个catch块中从新抛出的是当前捕获的异常,而第二个catch块中从新抛出的是当前捕获异常的一个新的拷贝.
若是忽略生成额外拷贝的系统开销,这两种方法还有差别么?
固然有。第一个块中从新抛出的是当前异常(current exception),不管它是什么类型.
特别是若是这个异常开始就是作为SpecialWidget类型抛出的,那么第一个块中传递出去的仍是SpecialWidget异常,即便w的静态类型(static type)是Widget.
这是由于从新抛出异常时没有进行拷贝操做.
第二个catch块从新抛出的是新异常,类型老是Widget,由于w的静态类型(static type)是Widget.
通常来讲,你应该用throw来从新抛出当前的异常,由于这样不会改变被传递出去的异常类型,并且更有效率,由于不用生成一个新拷贝.
(顺便说一句,异常生成的拷贝是一个临时对象.
正如条款19解释的,临时对象能让编译器优化它的生存期(optimize it out of existence),
不过我想你的编译器很难这么作,由于程序中不多发生异常,因此编译器厂商不会在这方面花大量的精力)
让咱们测试一下下面这三种用来捕获Widget异常的catch子句,异常是作为passAndThrowWidgetp抛出的:
catch (Widget w) ... // 经过传值捕获异常 catch (Widget& w) ... // 经过传递引用捕获异常 catch (const Widget& w) ... //经过传递指向const的引用捕获异常
咱们马上注意到了传递参数与传递异常的另外一个差别.
一个被异常抛出的对象(刚才解释过,老是一个临时对象)能够经过普通的引用捕获.
它不须要经过指向const对象的引用(reference-to-const)捕获.
在函数调用中不容许转递一个临时对象到一个非const引用类型的参数里(参见条款19),可是在异常中却被容许.
让咱们先无论这个差别,回到异常对象拷贝的测试上来.
咱们知道当用传值的方式传递函数的参数,咱们制造了被传递对象的一个拷贝(参见Effective C++ 条款22),并把这个拷贝存储到函数的参数里.
一样咱们经过传值的方式传递一个异常时,也是这么作的。当咱们这样声明一个catch子句时:
catch (Widget w) ... // 经过传值捕获
会创建两个被抛出对象的拷贝,一个是全部异常都必须创建的临时对象,第二个是把临时对象拷贝进w中.
一样,当咱们经过引用捕获异常时:
catch (Widget& w) ... // 经过引用捕获 catch (const Widget& w) ... file://也经过引用捕获
这仍旧会创建一个被抛出对象的拷贝:拷贝是一个临时对象.
相反当咱们经过引用传递函数参数时,没有进行对象拷贝.
当抛出一个异常时,系统构造的(之后会析构掉)被抛出对象的拷贝数比以相同对象作为参数传递给函数时构造的拷贝数要多一个.
咱们尚未讨论经过指针抛出异常的状况,不过经过指针抛出异常与经过指针传递参数是相同的.
不论哪一种方法都是一个指针的拷贝被传递.
你不能认为抛出的指针是一个指向局部对象的指针,由于当异常离开局部变量的生存空间时,该局部变量已经被释放.
Catch子句将得到一个指向已经不存在的对象的指针。这种行为在设计时应该予以免.
对象从函数的调用处传递到函数参数里与从异常抛出点传递到catch子句里所采用的方法不一样,
这只是参数传递与异常传递的区别的一个方面,第二个差别是在函数调用者或抛出异常者与被调用者或异常捕获者之间的类型匹配的过程不一样.
好比在标准数学库(the standard math library)中sqrt函数:
double sqrt(double); // from <cmath> or <math.h>
咱们能这样计算一个整数的平方根,以下所示:
int i; double sqrtOfi = sqrt(i);
毫无疑问,C++容许进行从int到double的隐式类型转换,因此在sqrt的调用中,i 被悄悄地转变为double类型,而且其返回值也是double.
(有关隐式类型转换的详细讨论参见条款5)通常来讲,catch子句匹配异常类型时不会进行这样的转换.
见下面的代码:
void f(int value) { try { if(someFunction()) // 若是 someFunction()返回 { throw value; //真,抛出一个整形值 ... } } catch(double d) // 只处理double类型的异常 { ... } ... }
在try块中抛出的int异常不会被处理double异常的catch子句捕获.
该子句只能捕获真真正正为double类型的异常;不进行类型转换.
所以若是要想捕获int异常,必须使用带有int或int&参数的catch子句.
不过在catch子句中进行异常匹配时能够进行两种类型转换.
第一种是继承类与基类间的转换.
一个用来捕获基类的catch子句也能够处理派生类类型的异常.
例如在标准C++库(STL)定义的异常类层次中的诊断部分(diagnostics portion )(参见Effective C++ 条款49).
捕获runtime_errors异常的Catch子句能够捕获range_error类型和overflow_error类型的异常,
能够接收根类exception异常的catch子句能捕获其任意派生类异常.
这种派生类与基类(inheritance_based)间的异常类型转换能够做用于数值、引用以及指针上:
catch (runtime_error) ... // can catch errors of type catch (runtime_error&) ... // runtime_error, catch (const runtime_error&) ... // range_error, or overflow_error catch (runtime_error*) ... // can catch errors of type catch (const runtime_error*) ... // runtime_error*,range_error*, oroverflow_error*
第二种是容许从一个类型化指针(typed pointer)转变成无类型指针(untyped pointer),
因此带有const void* 指针的catch子句能捕获任何类型的指针类型异常:
catch (const void*) ... file://捕获任何指针类型异常
传递参数和传递异常间最后一点差异是catch子句匹配顺序老是取决于它们在程序中出现的顺序.
所以一个派生类异常可能被处理其基类异常的catch子句捕获,即便同时存在有能处理该派生类异常的catch子句,与相同的try块相对应.
例如:
try { ... } catch(logic_error& ex) // 这个catch块 将捕获 { ... // 全部的logic_error } // 异常, 包括它的派生类 catch(invalid_argument& ex) // 这个块永远不会被执行 { ... //由于全部的invalid_argument异常 都被上面的catch子句捕获 }
与上面这种行为相反,当你调用一个虚拟函数时,被调用的函数位于与发出函数调用的对象的动态类型(dynamic type)最相近的类里.
你能够这样说虚拟函数采用最优适合法,而异常处理采用的是最早适合法.
若是一个处理派生类异常的catch子句位于处理基类异常的catch子句前面,编译器会发出警告.
(由于这样的代码在C++里一般是不合法的)
不过你最好作好预先防范:不要把处理基类异常的catch子句放在处理派生类异常的catch子句的前面.
上面那个例子,应该这样去写:
try { ... } catch(invalid_argument& ex) // 处理 invalid_argument { ... } catch(logic_error& ex) // 处理全部其它的 { ... // logic_errors异常 }
综上所述,把一个对象传递给函数或一个对象调用虚拟函数与把一个对象作为异常抛出,这之间有三个主要区别.
第1、异常对象在传递时总被进行拷贝;当经过传值方式捕获时,异常对象被拷贝了两次.
对象作为参数传递给函数时不须要被拷贝.
第2、对象作为异常被抛出与作为参数传递给函数相比,前者类型转换比后者要少(前者只有两种转换形式).
最后一点,catch子句进行异常类型匹配的顺序是它们在源代码中出现的顺序,第一个类型匹配成功的catch将被用来执行.
当一个对象调用一个虚拟函数时,被选择的函数位于与对象类型匹配最佳的类里,即便该类不是在源代码的最前头.
灵巧指针
第一次用到灵巧指针是在写ADO代码的时候,用到com_ptr_t灵巧指针;但一直印象不是很深;
其实灵巧指针的做用很大,对咱们来讲垃圾回收,ATL等都会使用到它.
在More effective 的条款后面特地增长这个节点,不只是想介绍它在异常处理方面的做用,还但愿对编写别的类型代码的时候能够有所帮助.
smart pointer(灵巧指针)其实并非一个指针,实际上是某种形式的类.
不过它的特长就是模仿C/C++中的指针,因此就叫pointer 了.
因此但愿你们必定要记住两点:smart pointer是一个类而非指针,但特长是模仿指针.
那怎么作到像指针的呢?
C++的模板技术和运算符重载给了很大的发挥空间.
首先smart pointer必须是高度类型化的(strongly typed ),模板给了这个功能.
其次须要模仿指针主要的两个运算符->和*,那就须要进行运算符重载.
详细的实现:
template<CLASS&NBSP; T> class SmartPtr { public: SmartPtr(T* p = 0); SmartPtr(const SmartPtr& p); ~SmartPtr(); SmartPtr& operator =(SmartPtr& p); T& operator*() const {return *the_p;} T* operator->() const {return the_p;} private: T *the_p; }
这只是一个大概的印象,不少东西是能够更改的.
好比能够去掉或加上一些const ,这都须要根据具体的应用环境而定.
注意重载运算符*和->,正是它们使smart pointer看起来跟普通的指针很相像.
而因为smart pointer是一个类,在构造函数、析构函数中均可以经过恰当的编程达到一些不错的效果.
举例:
好比C++标准库里的std::auto_ptr 就是应用很广的一个例子.
它的实如今不一样版本的STL 中虽有不一样,但原理都是同样,大概是下面这个样子:
template<CLASS&NBSP; X> class auto_ptr { public: typedef X element_type; explicit auto_ptr(X* p = 0) throw():the_p(p) {} auto_ptr(auto_ptr& a) throw():the_p(a.release()) {} auto_ptr& operator =(auto_ptr& rhs) throw() { reset(rhs.release()); return *this; } ~auto_ptr() throw() {delete the_p;} X& operator* () const throw() {return *the_p;} X* operator-> () const throw() {return the_p;} X* get() const throw() {return the_p;} X* release() throw() { X* tmp = the_p; the_p = 0; return tmp; } void reset(X* p = 0) throw() { if(the_p!=p) { delete the_p; the_p = p; } } private: X* the_p; };
关于auto_ptr 的使用能够找到不少的列子,这里不在举了.
它的主要优势是不用 delete ,能够自动回收已经被分配的空间,由此能够避免资源泄露的问题.
不少Java 的拥护者常常不分黑白的污蔑C++没有垃圾回收机制,其实不过是贻笑大方而已.
抛开在网上许许多多的商业化和非商业化的C++垃圾回收库不提, auto_ptr 就足以有效地解决这一问题.
而且即便在产生异常的状况下, auto_ptr 也能正确地回收资源.
这对于写出异常安全(exception-safe )的代码具备重要的意义.
在使用smart pointer 的过程当中,要注意的问题:
针对不一样的smart pointer ,有不一样的注意事项。好比auto_ptr ,就不能把它用在标准容器里,由于它只在内存中保留一份实例.
把握我前面说的两个原则:smart pointer 是类而不是指针,是模仿指针,那么一切问题都好办.
好比,smart pointer 做为一个类,那么如下的作法就可能有问题.
SmartPtr p; if(p==0) if(!p) if(p)
很显然, p 不是一个真正的指针,这么作可能出错.
而SmartPtr 的设计也是很重要的因素.
您能够加上一个bool SmartPtr::null() const 来进行判断.
若是坚持非要用上面的形式, 那也是能够的,咱们就加上operator void* ()试试:
template<CLASS&NBSP; T> class SmartPtr { public: ... operator void*() const {return the_p;} ... private: T* the_p; };
这种方法在basic_ios 中就使用过了。这里也能够更灵活地处理,好比类自己须要operator void*()这样地操做,
那么上面这种方法就不灵了。但咱们还有重载operator !()等等方法来实现.
总结smart pointer的实质:
smart pointer 的实质就是一个外壳,一层包装。正是多了这层包装,咱们能够作出许多普通指针没法完成的事,好比前面资源自动回收,或者自动进行引用记数,好比ATL 中CComPtr 和 CComQIPtr 这两个COM 接口指针类.
然而也会带来一些反作用,正因为多了这些功能,又会使 smart pointer 丧失一些功能.
WIN结构化异常
对使用WIN32平台的人来讲,对WIN的结构化异常应该要有所了解的。WINDOWS的结构化异常是操做系统的一部分,而C++异常只是C++的一部分,当咱们用C++编写代码的时候,咱们选择C++的标准异常(也能够用MS VC的异常),编译器会自动的把咱们的C++标准异常转化成SEH异常。
微软的Visual C++也支持C + +的异常处理,而且在内部实现上利用了已经引入到编译程序和Windows操做系统的结构化异常处理的功能。
SEH实际包含两个主要功能:结束处理(termination handling)和异常处理(exceptionhandling).
在MS VC的FAQ中有关于SEH的部分介绍,这里摘超其中的一句:
“在VC5中,增长了新的/EH编译选项用于控制C++异常处理。C++同步异常处理(/EH)使得编译器能生成更少的代码,/EH也是VC的缺省模型。”
必定要记得在背后的事情:在使用SEH的时候,编译程序和操做系统直接参与了程序代码的执行。
Win32异常事件的理解
我写的另外一篇文章:内存处理和DLL技术也涉及到了SEH中的异常处理。
Exception(异常处理) 分红软件和硬件exception2种.如:一个无效的参数或者被0除都会引发软件exception,而访问一个还没有commit的页会引发硬件exception.
发生异常的时候,执行流程终止,同时控制权转交给操做系统,OS会用上下文(CONTEXT)结构把当前的进程状态保存下来,而后就开始search 一个能处理exception的组件,search order以下:
结束处理程序
利用SEH,你能够彻底不用考虑代码里是否是有错误,这样就把主要的工做同错误处理分离开来.
这样的分离,可使你集中精力处理眼前的工做,而将可能发生的错误放在后面处理.
微软在Windows中引入SEH的主要动机是为了便于操做系统自己的开发.
操做系统的开发人员使用SEH,使得系统更增强壮.咱们也可使用SEH,使咱们的本身的程序更增强壮.
使用SEH所形成的负担主要由编译程序来承担,而不是由操做系统承担.
当异常块(exception block)出现时,编译程序要生成特殊的代码.
编译程序必须产生一些表(table)来支持处理SEH的数据结构.
编译程序还必须提供回调(callback)函数,操做系统能够调用这些函数,保证异常块被处理.
编译程序还要负责准备栈结构和其余内部信息,供操做系统使用和参考.
在编译程序中增长SEH支持不是一件容易的事.
不一样的编译程序厂商会以不一样的方式实现SEH,这一点并不让人感到奇怪.
幸好咱们能够没必要考虑编译程序的实现细节,而只使用编译程序的SEH功能.
结束处理程序代码初步
一个结束处理程序可以确保去调用和执行一个代码块(结束处理程序,termination handler),
而无论另一段代码(保护体, guarded body)是如何退出的。结束处理程序的语法结构以下: __try
{ file://保护块 } __finally { file://结束处理程序 }
在上面的代码段中,操做系统和编译程序共同来确保结束处理程序中的__finally代码块可以被执行,无论保护体(try块)是如何退出的。
不论你在保护体中使用return,仍是goto,或者是longjump,结束处理程序(finally块)都将被调用。
咱们来看一个实列:(返回值:10, 没有Leak,性能消耗:小)
DWORD Func_SEHTerminateHandle() { DWORD dwReturnData = 0; HANDLE hSem = NULL; const char* lpSemName = "TermSem"; hSem = CreateSemaphore(NULL, 1, 1, lpSemName); __try { WaitForSingleObject(hSem,INFINITE); dwReturnData = 5; } __finally { ReleaseSemaphore(hSem,1,NULL); CloseHandle(hSem); } dwReturnData += 5; return dwReturnData; }
这段代码应该只是作为一个基础函数,咱们将在后面修改它,来看看结束处理程序的做用.
在代码加一句:(返回值:5, 没有Leak,性能消耗:中下)
DWORD Func_SEHTerminateHandle() { DWORD dwReturnData = 0; HANDLE hSem = NULL; const char* lpSemName = "TermSem"; hSem = CreateSemaphore(NULL, 1, 1, lpSemName); __try { WaitForSingleObject(hSem,INFINITE); dwReturnData = 5; return dwReturnData; } __finally { ReleaseSemaphore(hSem,1,NULL); CloseHandle(hSem); } dwReturnData += 5; return dwReturnData; }
在try块的末尾增长了一个return语句.
这个return语句告诉编译程序在这里要退出这个函数并返回dwTemp变量的内容,如今这个变量的值是5.
可是,若是这个return语句被执行,该线程将不会释放信标,其余线程也就不能再得到对信标的控制.
能够想象,这样的执行次序会产生很大的问题,那些等待信标的线程可能永远不会恢复执行.
经过使用结束处理程序,能够避免return语句的过早执行.
当return语句试图退出try块时,编译程序要确保finally块中的代码首先被执行.
要保证finally块中的代码在try块中的return语句退出以前执行.
在程序中,将ReleaseSemaphore的调用放在结束处理程序块中,保证信标总会被释放.
这样就不会形成一个线程一直占有信标,不然将意味着全部其余等待信标的线程永远不会被分配CPU时间.
在finally块中的代码执行以后,函数实际上就返回.
任何出如今finally块之下的代码将再也不执行,由于函数已在try块中返回,因此这个函数的返回值是5,而不是10.
读者可能要问编译程序是如何保证在try块能够退出以前执行finally块的.
当编译程序检查源代码时,它看到在try块中有return语句.
这样,编译程序就生成代码将返回值(本例中是5)保存在一个编译程序创建的临时变量中.
编译程序而后再生成代码来执行finally块中包含的指令,这称为局部展开.
更特殊的状况是,因为try块中存在过早退出的代码,从而产生局部展开,致使系统执行finally块中的内容.
在finally块中的指令执行以后,编译程序临时变量的值被取出并从函数中返回.
能够看到,要完成这些事情,编译程序必须生成附加的代码,系统要执行额外的工做.
在不一样的CPU上,结束处理所须要的步骤也不一样.
例如,在Alpha处理器上,必须执行几百个甚至几千个CPU指令来捕捉try块中的过早返回并调用finally块.
在编写代码时,就应该避免引发结束处理程序的try块中的过早退出,由于程序的性能会受到影响.
后面,将讨论__leave关键字,它有助于避免编写引发局部展开的代码.
设计异常处理的目的是用来捕捉异常的—不常发生的语法规则的异常状况(在咱们的例子中,就是过早返回).
若是状况是正常的,明确地检查这些状况,比起依赖操做系统和编译程序的SEH功能来捕捉常见的事情要更有效.
注意当控制流天然地离开try块并进入finally块(就像在Funcenstein1中)时,进入finally块的系统开销是最小的.
在x86CPU上使用微软的编译程序,当执行离开try块进入finally块时,只有一个机器指令被执行,读者能够在本身的程序中注意到这种系统开销.
当编译程序要生成额外的代码,系统要执行额外的工做时系统开销就很值得注意了.
修改代码:(返回值:5,没有Leak,性能消耗:中)
DWORD Func_SEHTerminateHandle() { DWORD dwReturnData = 0; HANDLE hSem = NULL; const char* lpSemName = "TermSem"; hSem = CreateSemaphore(NULL, 1, 1, lpSemName); __try { WaitForSingleObject(hSem,INFINITE); dwReturnData = 5; if(dwReturnData == 5) goto ReturnValue; return dwReturnData; } __finally { ReleaseSemaphore(hSem,1,NULL); CloseHandle(hSem); } dwReturnData += 5; ReturnValue: return dwReturnData; }
代码中,当编译程序看到try块中的goto语句,它首先生成一个局部展开来执行finally块中的内容.
这一次,在finally块中的代码执行以后,在ReturnValue标号以后的代码将执行,由于在try块和finally块中都没有返回发生.
这里的代码使函数返回5,并且,因为中断了从try块到finally块的天然流程,可能要蒙受很大的性能损失(取决于运行程序的CPU)
写上面的代码是初步的,如今来看结束处理程序在咱们代码里面的真正的价值:
看代码:(信号灯被正常释放,reserve的一页内存没有被Free,安全性:安全)
DWORD TermHappenSomeError() { DWORD dwReturnValue = 9; DWORD dwMemorySize = 1024; char* lpAddress; lpAddress = (char*)VirtualAlloc(NULL, dwMemorySize, MEM_RESERVE, PAGE_READWRITE); }
finally块的总结性说明
咱们已经明确区分了强制执行finally块的两种状况:
从try块进入finally块的正常控制流.
•局部展开:从try块的过早退出(goto、longjump、continue、break、return等)强制控制转移到finally块.
第三种状况,全局展开(globalunwind),在发生的时候没有明显的标识,咱们在本章前面Func_SEHTerminate函数中已经见到.在Func_SEHTerminate的try块中,有一个对TermHappenSomeError函数的调用。TermHappenSomeError函数会引发一个内存访问违规(memory access violation),一个全局展开会使Func_SEHTerminate函数的finally块执行.
因为以上三种状况中某一种的结果而致使finally块中的代码开始执行。为了肯定是哪种状况引发finally块执行,能够调用内部函数AbnormalTermination:这个内部函数只在finally块中调用,返回一个Boolean值.指出与finally块相结合的try块是否过早退出。换句话说,若是控制流离开try块并天然进入finally块,AbnormalTermination将返回FALSE。若是控制流非正常退出try块—一般因为goto、return、break或continue语句引发的局部展开,或因为内存访问违规或其余异常引发的全局展开—对AbnormalTermination的调用将返回TRUE。没有办法区别finally块的执行是因为全局展开仍是因为局部展开.
但这一般不会成为问题,由于能够避免编写执行局部展开的代码.(注意内部函数是编译程序识别的一种特殊函数。编译程序为内部函数产生内联(inline)代码而不是生成调用函数的代码。例如,memcpy是一个内部函数(若是指定/Oi编译程序开关)。当编译程序看到一个对memcpy的调用,它直接将memcpy的代码插入调用memcpy的函数中,而不是生成一个对memcpy函数的调用。其做用是代码的长度增长了,但执行速度加快了。
在继续以前,回顾一下使用结束处理程序的理由:
•简化错误处理,因全部的清理工做都在一个位置而且保证被执行。
•提升程序的可读性。
•使代码更容易维护。
•若是使用得当,具备最小的系统开销。
异常处理程序
异常是咱们不但愿有的事件。在编写程序的时候,程序员不会想去存取一个无效的内存地址或用0来除一个数值。不过,这样的错误仍是经常会发生的。CPU负责捕捉无效内存访问和用0除一个数值这种错误,并相应引起一个异常做为对这些错误的反应。CPU引起的异常,就是所谓的硬件异常(hardwareexception)。在本章的后面,咱们还会看到操做系统和应用程序也能够引起相应的异常,称为软件异常(softwareexception)。
当出现一个硬件或软件异常时,操做系统向应用程序提供机会来考察是什么类型的异常被引起,并可以让应用程序本身来处理异常。下面就是异常处理程序的语法:
__try { //保护块 } __except(异常过虑器) { //异常处理程序 }
注意__except关键字。每当你创建一个try块,它必须跟随一个finally块或一个except块。一个try块以后不能既有finally块又有except块。但能够在try-except块中嵌套try-finally块,反过来也能够。
异常处理程序代码初步
与结束处理程序不一样,异常过滤器(exceptionfilter)和异常处理程序是经过操做系统直接执行的,编译程序在计算异常过滤器表达式和执行异常处理程序方面不作什么事。
下面几节的内容举例说明try-except块的正常执行,解释操做系统如何以及为何计算异常过滤器,并给出操做系统执行异常处理程序中代码的环境。
原本想把代码所有写出来的,可是实在是写这边文挡化的时间太长了,因此接下来就只是作说明,并且try和except块比较简单。
尽管在结束处理程序的try块中使用return、goto、continue和break语句是被强烈地反对,但在异常处理程序的try块中使用这些语句不会产生速度和代码规模方面的不良影响。
这样的语句出如今与except块相结合的try块中不会引发局部展开的系统开销.
当引起了异常时,系统将定位到except块的开头,并计算异常过滤器表达式的值,过滤器表达式的结果值只能是下面三个标识符之一,这些标识符定义在windows的Except.h文件中。标识符定义为:
EXCEPTION_CONTINUE_EXECUTION(–1) // Exception is dismissed. Continue execution at the point where the exception occurred. EXCEPTION_CONTINUE_SEARCH(0) // Exception is not recognized. Continue to search up the stack for a handler, first for containing try-except statements, then for handlers with the next highest precedence. EXCEPTION_EXECUTE_HANDLER(1) // Exception is recognized. Transfer control to the exception handler by executing the __except compound statement, then continue execution at the assembly instruction that was executing when the exception was raised
下面将讨论这些标识符如何改变线程的执行。
下面的流程归纳了系统如何处理一个异常的状况:(这里的流程假设是正向的)
*****开始 -> 执行一个CPU指令 -> {是否有异常被引起} -> 是 -> 系统肯定最里层的try 块 -> {这个try块是否有一个except块} -> 是 -> {过滤器表达式的值是什么} ->异常执行处理程序 -> 全局展开开始 -> 执行except块中的代码 -> 在except块以后执行继续*****
EXCEPTION_EXECUTE_HANDLER
在异常过滤器表达式的值若是是EXCEPTION_EXECUTE_HANDLER,这个值的意思是要告诉系统:“我认出了这个异常.
即,我感受这个异常可能在某个时候发生,我已编写了代码来处理这个问题,如今我想执行这个代码”
在这个时候,系统执行一个全局展开,而后执行向except块中代码(异常处理程序代码)的跳转.
在except块中代码执行完以后,系统考虑这个要被处理的异常并容许应用程序继续执行。这种机制使windows应用程序能够抓住错误并处理错误,再使程序继续运行,不须要用户知道错误的发生。可是,当except块执行后,代码将从何处恢复执行?稍加思索,咱们就能够想到几种可能性:
第一种可能性是从产生异常的CPU指令以后恢复执行。这看起来像是合理的作法,但实际上,不少程序的编写方式使得当前面的指令出错时,后续的指令不可以继续成功地执行。
代码应该尽量地结构化,这样,在产生异常的指令以后的CPU指令有望得到有效的返回值。例如,可能有一个指令分配内存,后面一系列指令要执行对该内存的操做。
若是内存不可以被分配,则全部后续的指令都将失败,上面这个程序重复地产生异常。
所幸的是,微软没有让系统从产生异常的指令以后恢复指令的执行。这种决策使咱们免于面对上面的问题。
第二种可能性是从产生异常的指令恢复执行。这是颇有意思的可能性。
若是在except块中有这样的语句会怎么样呢:在except块中有了这个赋值语句,能够从产生异常的指令恢复执行。
这一次,执行将继续,不会产生其余的异常。能够作些修改,让系统从新执行产生异常的指令。
你会发现这种方法将致使某些微妙的行为。咱们将在EXCEPTION_CONTINUE_EXECUTION一节中讨论这种技术。
第三种可能性是从except块以后的第一条指令开始恢复执行。这实际是当异常过滤器表达式的值为EXCEPTION_EXECUTE_HANDLER时所发生的事。在except块中的代码结束执行后,控制从except块以后的第一条指令恢复。