• 什么是异常? 异常是一种机制,利用这种异常机制,一段代码可以通知另一段代码发生了一种“异常”情况或错误条件,而不能再沿着正常代码的路径前进。 • C中的错误处理 C错误处理的标准就是使用整型的返回码和errno宏来通知程序出现了错误;许多C++程序也沿承了这种方法。但是,在不同的程序中,返回码 和errno或许并没有一致的规范,即同样的数据可能代表不同的意义,这往往会导致错误。 • C++中的异常 优点: 函数的返回码可以被忽略,但是异常不可忽略,否则程序会终止; 异常可以包含语义信息,返回码显然不可以; 异常可以返回相关的上下文信息,甚至用来传递消息(这一点有人认为是对异常的滥用),返回码不可以; 异常处理可以在调用栈中跳级。也就是说,在栈中,出错调用之上不一定刚好就是错误处理代码。但是,返回码要求各级调用栈都必须在前 一级清除之后显示清除。(有疑问???????????) 缺点: 性能问题,为支持异常而增加的语言特性会使所有程序变慢,即使根本没有使用异常的程序也会因此而变慢; C++中的异常支持并不是该语言中的一个集成部分,比如,一个没有指定异常列表的函数能抛出任意类型的异常,但在JAVA中是不可以的; 另外,C++并不能保证编译时异常列表,这说明函数的异常列表可能会在运行时改动。 异常机制会对动态分配的内存和资源清除带来问题,如,由于抛出了异常而被有执行后续的释放已分配资源的语句; • 使用介绍 #include <fstream> #include <iostream> #include <vector> #include <string> #include <stdexcept> using namespace std; void readIntegerFile( const string& fileName,vector<int>& dest ) { ifstream istr; int temp; istr.open( fileName.c_str( ) ); if( istr.fail( ) ) { throw invalid_argument( "" ); } while( istr>>temp ) dest.push_back( temp ); if( istr.eof( ) ) istr.close( ); else { istr.close( ); throw runtime_error( "" ); } } int main( ) { try { readIntegerFile( fileName,myInts ); } catch( const invalid_argument& e ) { cerr<<"Unable open file"<<endl; exit( 1 ); } catch( const runtime_error& e ) { cerr<<"Error reading file"<<endl; exit( 1 ); } return 0; } 注意: 可以抛出任何类型的异常。如throw 5;throw "you input rong name";C++定义了8个异常类,我们还可以编写自己的异常类。 程序catch块可以按值、按引用、const引用或指针来捕获异常: catch(const exception& e);catch(exception &e);catch(exception e)都可以 异常被捕获的顺序和catch块中的先后顺序有关,因为异常类之间支持多态性,所以 catch(const exception& e)可以捕获所有继承自exception的异常类。 要匹配任何异常的方式: try{//...} catch(...) //就是3个点号 {//...} 未捕获的异常: 如果存在一个未捕获的异常,可以改变程序的行为,而不是简单的退出。当程序遇到一个未捕获的异常时,它会调用内置的terminate()函数, 这个函数只是简单的调用<cstdlib>中的abort()来关闭程序。可以调用set_terminate(),并提供一个回调函数指针来设置自己的terminate_handler,这个回调函数无参且无返回值。注意,自己定义的回调函数也必须终止程序,否则程序还是会以某种方式调用abort(). 但是,我们可以使用自定义的回调函数在退出之前打印有帮助的消息。 void myTerminate( ) { cout<<"Uncaught exception!"<<endl; exit( 1 ); //必须退出 } int main( ) { vector<int> myInts; const string fileName="IntegerFile.txt"; terminate_handler old_handler=set_terminate( myTerminate ); //注意其返回原来的回调函数句柄 readIntegerFile( fileName,myInts ); for( size_t i=0;i<myInts.size( );++i ) cout<<myInts[ i ]" "; cout<<endl; set_terminate( old_handler ); //复原 return 0; } • 抛出列表 C++允许指定一个函数或方法想要抛出的异常,这个规范称之为抛出列表throw list或异常规范exception specification. void readIntegerFile(const string& fileName,vector<int>& dest) throw(invalid_argument,runtime_error) 抛出列表只是列出了可能由函数抛出的各种异常类型。需要注意,函数原型声明的时候也要写上抛出列表。 void readIntegerFile(const string& fileName,vector<int>& dest) throw(invalid_argument,runtime_error); 不同与const,异常规范并不是函数或方法签名的一部分。不能只根据抛出列表中不同的异常来重载一个函数或方法。 如果一个函数没有指定抛出列表,它就能抛出任何异常。 如果想要指定一个函数不抛出任何异常,就需要明确的写一个空的抛出列表: void readIntegerFile(const string& fileName,vector<int>& dest) throw(); 抛出异常列表以外的异常时,可以编译通过,但是程序运行以后会终止。抛出列表不能阻止函数抛出未列出的异常类型,但是能够阻止异常 到达处理函数。一个函数抛出了未在异常列表中的异常时,C++会调用unexpected().unexpected()只是内置实现调用terminate()。不过同样也可以像自定义terminate_handler函数一样,同样也可以自定义unexpected_handler。注意,在unexpected_handler中我们可以选择抛出一个新异常 或是终止程序。如果抛出了一个新异常,这个异常会取代原来的异常,就好像一开始就抛出这个新异常一样。如果新抛出的异常仍不在异常列表中,程序就会做:如果函数的异常列表包括bad_exception,则会抛出bad_exception;否则,函数终止。 在覆盖方法中修改抛出列表: 在子类中覆盖一个virtual方法时,可以修改抛出列表。只要所修改的抛出列表比超类中的抛出列表更为限定more restrictive更为具体就行。 一下修改都认为是更限定的修改: 从列表中删除异常,但是禁止全部删除; 增加超类抛出列表中所出现异常的子类,即用子类异常代替父类异常; 注意:在覆盖方法时,如果修改了抛出列表,则不能增加异常,因为由于多态性子类抛出了父类没有列出的异常,则会引起unexpected()的调用。 C++中的异常类是有层次结构和继承关系的,所以满足类的继承性和多态性。在多态地捕获异常的时候,要确保按引用捕获异常;如果按值捕获异常,可能会遇到切割问题,这样就会丢失对象信息。 • 栈展开和清除 当一段代码抛出一个异常时,控制会立即跳至与该异常匹配的异常处理程序。这个异常处理程序可能位于栈中一个或几个函数调用之上。 当控制在栈中上跳时,这个过程称为栈展开stack unwinding.当前执行点之后各函数中余下的所有代码都会跳过。不过,每个“展开”函数中 的局部对象和变量会撤销,就好像代码正常完成这个函数一样。但是,在栈展开过程中,指针变量不会释放(也就是堆中分配),而且不会完成其他 清除工作。 #include <stdexcept> void funcOne( ) throw( exception ); void funcTwo( ) throw( exception ); int main( ) { try { funcOne( ); } catch( const exception& e ) { cerr<<"Exception caught /n"; exit( 1 ); } return 0; } void funcOne( ) throw( exception ) { string str1; //str1在栈上,最后会自己消除 string* str2=new string( ); //产生内存泄漏 funcTwo( ); delete str2; //不会被执行 } void funcTwo( ) throw( exception ) { ifstream istr; //istr定义在栈上,所以最后程序退出时会自动调用istr的析构函数,来关闭文件 istr.open( "filename" ); throw exception( ); //异常抛出后会直接跳到main中的cerr语句执行 istr.close( ); //异常点以后的语句都不会执行 } 通过上面的例子,我们应该意识到,在栈上分配的内存在程序结束后都会自动撤销,而堆上却不会。 解决上面内存泄漏的两种方法: 1、捕获、清除和重新抛出 void funcOne( ) throw( exception ) { string str1; string* str2=new string( ); try { funcTwo( ); } catch( ... ) //捕获所有的异常 { delete str2; //清除指针数据 throw; //把捕获的异常重新抛给上层函数 } delete str2; } 2、使用智能指针 #include <memory> void funcOne( )throw ( exception ) { string str1; auto_ptr<string> str2( new string( "hello" ) ); //智能指针会在程序结束后自动删除指针数据 funcTwo( ); } • 常见的错误处理 1、内存分配错误 使用new/new[]分配内存失败时,会抛出bad_alloc的异常,这个类型在<new>中定义。 首先,我们可以使用try-catch来捕获异常: try { ptr=new int[ numbers ]; } catch( const bad_alloc& e ) { cerr<<"Unable to allocate memory"<<endl; return ; } //allocate succeed 然后,可以禁止new返回异常,也就是调用new(nothrow),这样分配失败时会返回NULL: ptr=new( nothrow ) int [ numberes ]; //nothrow是new的一个参数 if( ptr==NULL ) { cerr<<"Unable to allocate memory"<<endl; return ; } 最后,可以“定制内存分配失败行为” (要考虑到,记录一个错误也是需要分配内存的,如果new失败,甚至可能没有留下足够的内存来记录错误信息) C++允许指定一个new处理程序newhandler回调函数。默认并没有new处理程序,因此new/new[]只会抛出bad_alloc异常。 但是,如果存在一个new处理函数的话,一旦分配内存失败,就会执行new处理函数,然后内存分配进程就会再次尝试分配内存,如果 不成功,还会再调用new处理函数,一直循环下去。 (C++中有3个设置回调的函数:set_terminate(),set_unexpected(),set_new_handler() .) #include <new> #include <cstdlib> void myNewHandler( ) { cerr<<"unable to allocate memory"<<endl; abort( ); } int main( ) { //... new_handler oldHandler=set_new_handler( myNewHandler ); //... set_new_handler( oldHandler ); //... return 0; } 2、构造函数中的错误 由于构造函数没有返回值,所以无法采用C中常用的方法来进行判断。 一个可行的方法是,在类中定义一个标志符和一个检查标识符的函数,在定义一个对象以后调用该函数进行检测。 使用异常: class Test { public: Test( ) { int i,j; m_arry=new int* [ mWidth ]; //失败也没有影响 try { for(i=0;i<mWidth;++i) m_Cell[ i ]=new int[ mHeight ]; } catch( ... ) { for( j=0;j<i;++j ) delete [ ] m_Cell[j]; delete [ ] m_Cell; throw bad_alloc( ); } } } 注意:如果由异常退出构造函数,该对象的析构函数是不会被调用的。因此,要小心处理已分配的内存。 如果增加了继承,超类的构造函数会在子类构造函数调用以前调用。如果子类构造函数抛出了一个异常,那么超类的析构函数会调用吗? C++将确保那些构造函数完成的对象的析构函数一定会被调用。 3、析构函数中出现错误 应该在析构函数自身中处理其自己出现的错误。 很新颖地,C++提供了一种能力:可以确定是因为一个正常的退出或撤销函数调用(delete)来执行析构函数,还是由于栈展开而执行析构函数。 <exception>头文件中,有一个uncaught_exception(),如果有一个未捕获的异常,而且正处在栈展开的过程中,它就会返回true,否则发挥false。不过这种方法很混乱,应当避免。