From : http://www.uml.org.cn/c%2B%2B/201305272.asp
From : http://bbs.csdn.net/topics/390269815
From : http://developer.51cto.com/art/201512/502598.htm
From : http://www.cnblogs.com/ggjucheng/archive/2011/12/18/2292089.html
From : http://blog.csdn.net/dengzhouit/article/details/7012528
标准异常参考 http://www.cplusplus.com/reference/std/stdexcept/
什么是异常处理
一句话:异常处理就是处理程序中的错误。
为什么需要异常处理,以及异常处理的基本思想
C++之父Bjarne Stroustrup在《The C++ Programming Language》中讲到:一个库的作者可以检测出发生了运行时错误,但一般不知道怎样去处理它们(因为和用户具体的应用有关);另一方面,库的用户知道怎样处理这些错误,但却无法检查它们何时发生(如果能检测,就可以再用户的代码里处理了,不用留给库去发现)。
Bjarne Stroustrup说:提供异常的基本目的就是为了处理上面的问题。基本思想是:让一个函数在发现了自己无法处理的错误时抛出(throw)一个异常,然后它的(直接或者间接)调用者能够处理这个问题。
The fundamental idea is that a function that finds a problem it cannot cope with throws an exception, hoping that its (direct or indirect) caller can handle the problem.
也就是《C++ primer》中说的:将问题检测和问题处理相分离。
Exceptions let us separate problem detection from problem resolution
一种思想:在所有支持异常处理的编程语言中(例如java),要认识到的一个思想:在异常处理过程中,由问题检测代码可以抛出一个对象给问题处理代码,通过这个对象的类型和内容,实际上完成了两个部分的通信,通信的内容是“出现了什么错误”。当然,各种语言对异常的具体实现有着或多或少的区别,但是这个通信的思想是不变的。
异常出现之前处理错误的方式
在C语言的世界中,对错误的处理总是围绕着两种方法:一是使用整型的返回值标识错误;二是使用errno宏(可以简单的理解为一个全局整型变量)去记录错误。
当然C++中仍然是可以用这两种方法的。这两种方法最大的缺陷就是会出现不一致问题。例如有些函数返回1表示成功,返回0表示出错;而有些函数返回0表示成功,返回非0表示出错。还有一个缺点就是函数的返回值只有一个,你通过函数的返回值表示错误代码,那么函数就不能返回其他的值。当然,你也可以过指针或者C++的引用来返回另外的值,但是这样可能会令你的程序略微晦涩难懂。
异常为什么好
在如果使用异常处理的优点有以下几点:
1. 函数的返回值可以忽略,但异常不可忽略。如果程序出现异常,但是没有被捕获,程序就会终止,这多少会促使程序员开发出来的程序更健壮一点。而如果使用C语言的error宏或者函数返回值,调用者都有可能忘记检查,从而没有对错误进行处理,结果造成程序莫名其面的终止或出现错误的结果。
2. 整型返回值没有任何语义信息。而异常却包含语义信息,有时你从类名就能够体现出来。
3. 整型返回值缺乏相关的上下文信息。异常作为一个类,可以拥有自己的成员,这些成员就可以传递足够的信息。
4. 异常处理可以在调用跳级。这是一个代码编写时的问题:假设在有多个函数的调用栈中出现了某个错误,使用整型返回码要求你在每一级函数中都要进行处理。而使用异常处理的栈展开机制,只需要在一处进行处理就可以了,不需要每级函数都处理。
C++中使用异常时应注意的问题
任何事情都是两面性的,异常有好处就有坏处。如果你是C++程序员,并且希望在你的代码中使用异常,那么下面的问题是你要注意的。
1. 性能问题。这个一般不会成为瓶颈,但是如果你编写的是高性能或者实时性要求比较强的软件,就需要考虑了。
(如果你像我一样,曾经是java程序员,那么下面的事情可能会让你一时迷糊,但是没办法,谁叫你现在学的是C++呢。)
2. 指针和动态分配导致的内存回收问题:在C++中,不会自动回收动态分配的内存,如果遇到异常就需要考虑是否正确的回收了内存。在java中,就基本不需要考虑这个,有垃圾回收机制真好!
3. 函数的异常抛出列表:java中是如果一个函数没有在异常抛出列表中显式指定要抛出的异常,就不允许抛出;可是在C++中是如果你没有在函数的异常抛出列表指定要抛出的异常,意味着你可以抛出任何异常。
4. C++中编译时不会检查函数的异常抛出列表。这意味着你在编写C++程序时,如果在函数中抛出了没有在异常抛出列表中声明的异常,编译时是不会报错的。而在java中,eclipse的提示功能真的好强大啊!
5. 在java中,抛出的异常都要是一个异常类;但是在C++中,你可以抛出任何类型,你甚至可以抛出一个整型。(当然,在C++中如果你catch中接收时使用的是对象,而不是引用的话,那么你抛出的对象必须要是能够复制的。这是语言的要求,不是异常处理的要求)。
6. 在C++中是没有finally关键字的。而java和python中都是有finally关键字的。
异常的基本语法
1. 抛出和捕获异常
很简单,抛出异常用throw,捕获用try……catch。
C++ 异常处理涉及到三个关键字:try、catch、throw。
- throw: 使用 throw 关键字,当程序出现问题时会抛出一个异常。使用 throw 语句在代码块中的任何地方抛出异常。throw 语句的操作数可以是任意的表达式,表达式的结果的类型决定了抛出的异常的类型。
- catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
- try: try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。
-
函数和函数可能抛出的异常集合作为函数声明的一部分是有价值的,例如
void f(int a) throw (x2,x3);
表示f()只能抛出两个异常x2,x3,以及这些类型派生的异常,但不会抛出其他异常。如果f函数违反了这个规定,抛出了x2,x3之外的异常,例如x4,那么当函数f抛出x4异常时,
会转换为一个std::unexpected()调用,默认是调用std::terminate(),通常是调用abort()。如果函数不带异常描述,那么假定他可能抛出任何异常。例如:
int f(); //可能抛出任何异常
不带任何异常的函数可以用空表表示:
int g() throw (); // 不会抛出任何异常
-
捕获异常的代码一般如下:
try { throw E(); } catch (H h) { //何时我们可以能到这里呢 }
1.如果H和E是相同的类型
2.如果H是E的基类
3.如果H和E都是指针类型,而且1或者2对它们所引用的类型成立
4.如果H和E都是引用类型,而且1或者2对H所引用的类型成立
从原则上来说,异常在抛出时被复制,我们最后捕获的异常只是原始异常的一个副本,所以我们不应该抛出一个不允许抛出一个不允许复制的异常。
此外,我们可以在用于捕获异常的类型加上const,就像我们可以给函数加上const一样,限制我们,不能去修改捕捉到的那个异常。
还有,捕获异常时如果H和E不是引用类型或者指针类型,而且H是E的基类,那么h对象其实就是H h = E(),最后捕获的异常对象h会丢失E的附加携带信息。
try
{
// 保护代码
}catch( ExceptionName e1 )
{
// catch 块
}catch( ExceptionName e2 )
{
// catch 块
}catch( ExceptionName eN )
{
// catch 块
}
}catch(...) //三个点代表捕获所有异常
{
// catch 块
}
double division(int a, int b)
{
if( b == 0 )
{
throw "Division by zero condition!";
cout << "throw 后面的语句不再执行" <<endl;
}
return (a/b);
}
int main(void)
{
try
{
division(5, 0);
}
catch(const char* error)
{
cout << error <<endl;
}
return 0;
}
#include <iostream>
using namespace std;
int main(void)
{
try
{
throw 3.0;
}
catch (int) //如果处理块内不需要该变量,可以省略形参
{
cout << "int exception happened!" << endl;
}
catch (double d)
{
cout << d << " double exception happened!" << endl;
}
catch (…)
{
cout << "unknown exception happened!" << endl;
}
return 0;
}
说明:
1) throw 可以抛出一个任意类型的变量(或表达式)(包括内置类型如int的变量,或者自定义的类型的变量),catch 按照被抛出变量的编译时类型进行匹配,找到第一个匹配的类型即进入异常处理。
2) 异常处理是为了保证使程序能够不异常退出。
3) 建议:尽量不要使用throw抛出内置类型的变量。
4) 如果throw抛出的异常找不到匹配的类型,最终程序将调用C standard Library的terminate函数,程序将异常退出。
5) 当程序跳出try块时,try块内的局部变量被自动释放,对象的析构函数被调用。所以,为了保证程序不异常退出,应该保证析构函数不会抛出异常。
6) 调用函数时,程序的控制权最终还会返回到函数的调用处,但是当你抛出一个异常时,控制权永远不会回到抛出异常的地方。
7) 异常匹配时,只允许三种类型转换:const与非const;派生类与基类;数组与指针。(注意:不允许算术转换.)
8) 建议:catch子句的次序必须反映类型层次,派生类放到基类前面。
9) throw出的对象称为异常对象(exception object),由编译器管理,catch接受到的对象如果不是引用或指针的话,则进行对象拷贝。但是异常对象是程序结束才被释放。
10) 异常可以发生在构造函数中或者构造函数初始化式中。注意:如果异常发生在构造函数中,对象的析构函数将不会被调用!所以需要在构造函数中进行try-catch自己释放资源。另外,为了处理构造函数初始化式中可能发生的异常,语法应该修改为如下:
//normal constructor
UserClass(): m_nA(1), m_nB(2) { /*constructor body*/ }
//constructor using function try block
UserClass() try: m_nA(1), m_nB(2){ /*constructor body*/ }catch(…){ }
11) 标准库异常类定义在头文件中
重新抛出:
在catch块或被catch块调用的函数中,可以用”throw;”语句(throw空对象)将异常重新抛出。
异常规格说明(exception specification):
说明函数将会抛出什么类型的异常。
void func(int i) throw (runtime_error); //说明该函数func有可能抛出runtime_error异常
void func(int i) throw(); //说明函数func不会抛出任何异常
1) 如果函数运行时抛出了其他类型的异常,程序将会调用标准库的unexpected函数,该函数将调用terminate退出程序。
2) 派生类的虚函数的异常规格说明只能和基类一样或比基类更严格,不能增加新的异常类型。
3) 注意:一般只会使用throw()来说明一个函数是安全的,不会抛出任何异常,这样编译器就可以对调