C语言中,出错信息通常通过函数的返回值来获得,或者通过设置全局的错误判断标志(标准C语言中的errno()和perror()),对每个函数的返回值进行错误检查会使程序变得繁琐,程序设计者可能简单的忽略这些出错信息。而C++的异常处理,
1) 出错处理程序的编写不再繁琐,也不需要将出错处理程序与正常代码紧密结合。在错误有可能出现的地方写一些代码,并在后面的语句中加入出错处理程序。如果程序中多次调用一个函数,在程序中加入一个函数出错处理程序即可。
2) 错误发生是不会被忽略的。如果被调用函数需要发送一条出错信息给调用函数,它会向调用函数发送一描述出错信息的对象。如果调用函数没有捕获和处理该对象,它会向更上一级调用者继续发送该出对象,直到该描述错信息的对象被捕获和处理。
抛出异常
如果程序发生异常情况,但在当前的上下文环境中获取不到异常处理的足够信息,我们可以创建一个包含出错信息的对象并将该对象抛出至当前上下文环境之外的更大的上下
文环境中。这称为异常抛出。如:
throw myerror ("something bad happened");
myerror是一个普通类,它以字符变量作为构造函数的参数。当进行异常抛出时我们可使用任意类型变量作为其参数(包括内部类型变量),但更为常用的办法是创建一个新的类对象用于异常抛出。
关键字throw的作用:首先,throw调用构造函数创建一个对象。其次,实际上这个对象正是throw函数的返回值(即使这个对象的类型不是函数设计的正常返回类型,我们可以当作替换的返回机制),throw通过抛出一个异常来退出作用域并返回一个值。
异常抛出同普通的函数调用的返回地点完全不同(异常处理器地点与异常抛出地点可能相差很远)。另外,只有在异常之前已经成功创建的对象才被销毁(普通函数则不同,作用域内的所有对象均被销毁)。当然,异常对象本身也在适当的地点销毁。
我们可以抛出许多不同类型的对象来满足需求。一般情况下,对于每种不同的错误可设定抛出不同类型的对象。采用这样的方法是为了存储对象中的信息和对象的类型,所以别人可以在更大的上下文环境中考虑如何处理我们的异常。
异常捕获
如果一个函数抛出一个异常,它必须假定该异常能被捕获和处理。如果在函数内抛出一个异常(或在函数调用时抛出一个异常),异常抛出时会退出函数。如果不想在异常抛出时退出函数,可在函数内创建一个特殊块try block。try块为普通作用域,由关键字try引导:
try {
// code that may generate exceptions
}
使用异常处理时可以不做差错检查,而将所有的工作放入try块中。
异常抛出后,一旦被异常处理器接收到就被销毁。异常处理器应具备接受任何一种类型的异常的能力。异常处理器紧随try块之后,处理的方法由关键字catch引导。
try {
// code that may generate exceptions
} catch(type1 id1) {
// handle exceptions of type1
} catch(type2 id2) {
// handle exceptions of type2
}
// etc...
每一个catch语句就相当于一个以特殊类型作为单一参数的小型函数。异常处理器中标识符(id1、id2 等)就如同函数中的一个参数。如果异常抛出给出的异常类型足以判断如何进行异常处理,那么异常处理器中的标识符可省略。异常处理部分必须直接放在try块之后。如果一个异常被抛出,异常处理器中第一个“参数与异常抛出对象相匹配的函数”将捕获该异常,然后进入相应的catch语句,执行异常处理程序。catch语句与switch语句不同,它不需要在每个case语句后加入break来中断后面程序的执行。
注意,在try块中不同的函数的调用可能会产生相同的异常情况,但这时只需要一个异常处理器。
异常规格说明
C++语言提供了异常规格说明语法,我们以可利用它清晰地告诉使用者函数抛出的异常的类型,这样使用者就可方便地进行异常处
理。它存在于函数说明中,位于参数列表之后。
异常规格说明再次使用了关键字throw,函数的所有潜在异常类型均随着关键字throw而插入函数说明中。所以函数说明可以带有异常说明如下:
void f ( ) throw ( toobig, toosmall, divzero);
而传统函数声明:
void f ( );
意味着函数可能抛出任何一种异常。
如果是:
void f ( ) throw ( );
这意味着函数不会有异常抛出。
如果函数实际抛出的异常类型与我们的异常规格说明不一致,将会产生什么样的结果呢?这时会调用特殊函数unexpected()。我们可以自动设置这个函数,通过set_unexpected(),将一个函数指针作为未知异常类型的处理函数。
// Basic exceptions
// Exception specifications & unexpected()
#include <exception>
#include <iostream>
#include <cstdlib>
#include <cstring>
using namespace std;
class Up {};
class Fit {};
void g();
void f(int i) throw (Up, Fit) {
switch(i) {
case 1: throw Up();
case 2: throw Fit();
}
g();
}
// void g() {} // Version 1
void g() { throw 47; } // Version 2
// (Can throw built-in types)
void my_unexpected() {
cout << "unexpected exception thrown";
exit(1);
}
int main() {
set_unexpected(my_unexpected);
// (ignores return value)
for(int i = 1; i <=3; i++)
try {
f(i);
} catch(Up) {
cout << "Up caught" << endl;
} catch(Fit) {
cout << "Fit caught" << endl;
}
}
不过这段代码在VS2008+WIN7上,并未能进入my_unexpected()函数。。。
捕获所有的异常
如果函数没有异常规格说明,任何类型的异常都有可能被函数抛出。为了解决这个问题,应创建一个能捕获任意类型的异常的处理器。
catch (. . . ) {
cout << "an exception was thrown" <<endl;
}
为了避免漏掉异常抛出,可将能捕获任意异常的处理器放在所有的处理器的最后面。
重新抛出异常
有时需要重新抛出刚接收到的异常,尤其是在我们无法得到有关异常的信息而用省略号捕获任意的异常时。这些工作通过加入不带参数的throw就可完成:
catch (. . .) {
cout << "an exception was thrown "<<endl;
throw ;
}
如果一个catch句子忽略了一个异常,那么这个异常将进入更高层的上下文环境。由于每个异常抛出的对象是被保留的,所以更高层上下文环境的处理器可得到这个对象的所有信息。
未被捕获的异常
如果异常未能被捕获,函数terminate()将自动被调用,我们也可以用set_terminate()来安装自己的terminate函数,如下例所示:
// Use of set_terminate()
// Also shows uncaught exceptions
#include <exception>
#include <iostream>
#include <cstdlib>
using namespace std;
void terminator() {
cout << "I'll be back!" << endl;
abort();
}
void (*old_terminate)()
= set_terminate(terminator);
class Botch {
public:
class Fruit {};
void f() {
cout << "Botch::f()" << endl;
throw Fruit();
}
~Botch() { throw 'c'; }
};
int main() {
try{
Botch b;
b.f();
} catch(...) {
cout << "inside catch(...)" << endl;
}
}
C++的异常处理器可以保证当我们离开一个作用域时,该作用域中所有结构完整的对象的析构函数都将被调用,以清除这些对象。但当对象的构造函数不完整时其析构函数将不被调用。因此,如果异常抛出发生在构造函数创建对象时,对象的析构函数将无法调用。
异常匹配
异常匹配并不要求在异常和处理器之间匹配得十分完美。一个对象或一个派生类对象的引用将与基类处理器匹配(然而假若处理器针对的是对象而非引用,异常对象在传递给处理器时会被“切割”,这样不会受到破坏但会丢失所有的派生类型信息)。假若抛出一个指针,标准指针转化处理会被用于匹配异常,但不会将某个异常类型在匹配过程中自动类型转化为另一个。下面是一个例子:
// No matching conversions
#include <iostream>
using namespace std;
class Except1 {};
class Except2 {
public:
Except2(Except1&) {}
};
void f() { throw Except1(); }
int main() {
try { f();
} catch (Except2) {
cout << "inside catch(Except2)" << endl;
} catch (Except1) {
cout << "inside catch(Except1)" << endl;
}
}
输出会是
"inside catch(Except1)"
下面是继承时的情况,
// Exception hierarchies
#include <iostream>
using namespace std;
class X {
public:
class Trouble {};
class Small : public Trouble {};
class Big : public Trouble {};
void f() { throw Big(); }
};
int main() {
X x;
try {
x.f();
} catch(X::Trouble) {
cout << "caught Trouble" << endl;
// Hidden by previous handler:
} catch(X::Small) {
cout << "caught Small Trouble" << endl;
} catch(X::Big) {
cout << "caught Big Trouble" << endl;
}
}
输出结果会是:
"caught Trouble"
标准异常
C++标准库的一些异常可以用于我们自己的程序中,
exception | 所有标准C++库异常的基类。可以调用what ( )以获得其特性的显示说明。 |
logic_error | 由exception派生的。它报告程序的逻辑错误,这些错误在程序执行前可以被检测到。 |
runtime_error | 由exception派生的。它报告程序运行时错误,这些错误仅在程序运行时可以被检测到。 |
继承自logic_error的异常:
domain_error | 违反先决条件 |
invalid_argument | 函数的参数无效 |
length_error | 试图创建大小超过size_t最大值的对象 |
out_of_range | 参数超出范围 |
bad_cast | 运行时无效的dynamic-cast |
bad_typeid | typeid(*p)中的p是NULL值 |
继承自runtime_error的异常:
range_error | 违反后置条件 |
overflow_error | 算术溢出 |
bad_alloc | 分配内存失败 |