1 例子: throw, try, catch
当抛出(throw
)对象时,异常被引发(raise
)。
// throws exception if both objects do not refer to the same isbn
Sales_item operator+(const Sales_item& lhs, const Sales_item& rhs)
{
if (!lhs.same_isbn(rhs))
throw runtime_error("Data must refer to same ISBN");
//ok, if we're still here the ISBNs are the same so it's okay to do the addition
Sales_item ret(lhs); // copy lhs into a local object that we'll return
ret += rhs; // add in the contents of rhs
return ret; // return a copy of ret
}
// part of the application that interacts with the user
Sales_item item1, item2, sum;
while (cin >> item1 >> item2) { // read two transactions
try {
sum = item1 + item2; // calculate their sum
} catch (const runtime_error &e) {
cerr << e.what() << " Try again.\n" << endl;
}
}
函数可能抛出异常,需要检查的代码放在try
语句块中,在catch
语句块中处理捕捉到的异常。
2 重新抛出异常
如果有异常 catch
无法处理,那么它使用空的 throw
语句将异常沿函数调用链向上重新抛出:
throw;
例子:
catch (my_error &eObj) { // specifier is a reference type
eObj.status = severeErr; // modifies the exception object
throw; // the status member of the exception object is severeErr
} catch (other_error eObj) { // specifier is a nonreference type
eObj.status = badErr; // modifies local copy only
throw; //the status member of the exception rethrown is unchanged
}
3 捕捉所有异常
使用 catch(...)
捕捉所有异常,如果和其他catch
一起使用,catch(...)
要放在最后。
int manip() {
try {
// actions that cause an exception to be thrown
}
catch (const Exception& e) {
// actions
}
catch (...) {
}
}
4 C++ 中列举的异常
namespace std {
class bad_cast;
class bad_alloc;
class logic_error;
class domain_error;
class invalid_argument;
class length_error;
class out_of_range;
class runtime_error;
class range_error;
class overflow_error;
class underflow_error;
}
5 除零异常
问题:
如下的代码试图捕捉除0异常,看似合理,但实际上无法捕捉到。
int i = 0;
cin >> i; // what if someone enters zero?
try {
i = 5/i; // 除0不是C++异常!!!
}
catch (std::logic_error e) {
cerr << e.what();
}
原因:
整数除0不属于C++异常, 要自行检查代码。
浮点除0也不是,但至少还有处理这种异常的具体方式。
你可能会想,用 overflow_error
就可以捕捉了,但实际上:
C++11 第 5.6 章明确声明:
如果 / 或 % 的第二个操作数是0, 行为未定义。
即: 它有可能 会抛出异常。 它也有可能格式化你的硬盘并嘲笑你。
捕捉除0异常的示例code:
#include <iostream>
#include <stdexcept>
// Integer division, catching divide by zero.
inline int intDivEx (int numerator, int denominator) {
if (denominator == 0)
throw std::overflow_error("Divide by zero exception");
return numerator / denominator;
}
int main (void) {
int i = 42;
try {
i = intDivEx (10, 2);
} catch (std::overflow_error e) {
std::cout << e.what() << " -> ";
}
std::cout << i << std::endl;
try {
i = intDivEx (10, 0);
} catch (std::overflow_error e) {
std::cout << e.what() << " -> ";
}
std::cout << i << std::endl;
return 0;
}
//以上代码输出:
//5
//Divide by zero exception -> 5
运算符 %
也一样:
// Integer remainder, catching divide by zero.
inline int intModEx (int numerator, int denominator) {
if (denominator == 0)
throw std::overflow_error("Divide by zero exception");
return numerator % denominator;
}
6 栈展开(Stack Unwinding)
当异常被抛出后,当前函数的执行被挂起,随后开始查找匹配的catch
。查找过程首先检查 throw
是否位于try
语句块内,如果是这样,与该try
关联的catch
语句被逐一比对是否有一个与被抛出的对象匹配。如果找到匹配的catch
, 异常被处理,如果没有找到,当前函数退出,内存释放,局部对象被销毁,查找过程在上一级调用函数中继续进行。
如果对抛出异常函数的调用在一个 try
语句内部,则会检查与之关联的catch
语句,如果找到匹配的catch
,异常被处理。如果没找到,这个调用函数也将退出,查找在上一个调用此函数的函数中继续进行。
此过程称为栈展开,沿嵌套的函数调用链向上,直到找到匹配的catch
。一旦找到匹配的 catch
, 就会进入那一个catch
, 执行在此 handler中继续。当此catch
结束后, 执行从对应try
的最后一个catch
语句后开始执行。
异常必须处理,否则程序将调用库函数 terminate
终止执行。
析构函数中永远不要 throw
异常
原因:当局部类对象作用域结束,或者对象创建后,发生异常,函数提前退出,析构函数会执行资源释放等操作。如果析构函数中再次抛出异常,难以想象处理逻辑。
所有标准库类型的析构函数都承诺不会引发(raise
)异常。
7 std::auto_ptr
类
如下的例子,如果 x != 0
, 将产生内存泄露:
void func( int x )
{
char* pleak = new char[1024]; // might be lost => memory leak
std::string s( "hello world" ); // will be properly destructed
if ( x ) throw std::runtime_error( "boom" );
delete [] pleak; // will only get here if x == 0. if x!=0, throw exception
}
int main()
{
try {
func( 10 );
}
catch (const std::exception& e) {
return 1;
}
return 0;
}
解决方法之一,是将动态分配的变量设计成类的对象,即使出现异常,自动调用的析构函数也能释放内存。
另一个方法是使用标准库中的智能指针,例如使用类std::auto_ptr
:
对比:
void f()
{
int *ip = new int(42); // dynamically allocate a new object
// code that throws an exception that is not caught inside f
delete ip; // return the memory before exiting
}
以上代码,如果 delete ip
之前出现异常,将产生内存泄露。下面的代码,使用std::auto_ptr
则能避免此问题。
void f()
{
auto_ptr<int> ap(new int(42)); // allocate a new object
// code that throws an exception that is not caught inside f
}
// auto_ptr freed automatically when function ends
auto_ptr
是可以容纳任意类型指针的模板:
auto_ptr<string> ap1(new string("Brontosaurus"));
对 std::auto_ptr
的批评:
除了拥有”特殊”危险的能力–即能够被复制之外,它非常像一个域指针。允许被复制的能力也会意外地转移所有权。在最新的标准中已被弃用,你不应再使用它,而要使用 std::unique_ptr
。
(There is also std::auto_ptr. It is very much like a scoped pointer, except that it also has the “special” dangerous ability to be copied — which also unexpectedly transfers ownership! It is deprecated in the newest standards, so you shouldn’t use it. Use the std::unique_ptr instead.)
std::auto_ptr<MyObject> p1 (new MyObject());
std::auto_ptr<MyObject> p2 = p1; // Copy and transfer ownership.
// p1 gets set to empty!
p2->DoSomething(); // Works.
p1->DoSomething(); // Oh oh. Hopefully raises some NULL pointer exception.
C++11提供了丰富的智能指针类型:std::unique_ptr
,std::shared_ptr
以及std::weak_ptr
。
[1]. https://stackoverflow.com/questions/6121623/catching-exception-divide-by-zero
[2] C++ Primer 17.1 异常处理
[3] https://stackoverflow.com/questions/2331316/what-is-stack-unwinding
[4] https://stackoverflow.com/questions/106508/what-is-a-smart-pointer-and-when-should-i-use-one