异常处理
1.1 抛出异常
如果在代码中出现了异常的情况, 程序员可以创建一个包含错误信息的对象并能过关键字throw抛出当前语境, 将错误发送到一个更大范围的语境中.例如:
view plaincopy to clipboardprint?
try
{
if ( ! resourceAvail )
throw MyExceptionClass( "Resource Is Not Available" );
}
catch(MyExceptionClass& myException)
{
// resource was not available, do something cleanup
throw;
}
try
{
if ( ! resourceAvail )
throw MyExceptionClass( "Resource Is Not Available" );
}
catch(MyExceptionClass& myException)
{
// resource was not available, do something cleanup
throw;
}
在抛出一个异常时, 可以使用任意的类型, 包括内置类型.
在throw背后, 它创建了代码所抛出的对象的一个拷贝, 实际上, 包含throw表达式的函数返回这个对象, 即使该函数返回的类型不是该对象的类型. 可将这种情况视为交错返回机制(alnternate return mechanism).
1.2 捕获异常
try块
try块只是一个普通的程序块, 由关键字try引导:
view plaincopy to clipboardprint?
try
{
if ( ! resourceAvail )
throw MyExceptionClass( "Resource Is Not Available" );
}
try
{
if ( ! resourceAvail )
throw MyExceptionClass( "Resource Is Not Available" );
}
通常将可能出现异常的代码写在try块里面, 然后通过紧接着在try块后面的catch块来捕获异常.
异常处理器
异常处理器紧接着try块, 由关键字catch标识:
view plaincopy to clipboardprint?
catch(MyExceptionClass& myException)
{
// resource was not available, do something cleanup
throw;
}
catch(MyExceptionClass& myException)
{
// resource was not available, do something cleanup
throw;
}
异常处理器必须跟在try块之后, 一旦某个异常被抛出, 异常处理机制会依次寻找参数与异常类型匹配的catch块. 当找到第一个匹配的异常处理器后, 会执行这个catch里的代码. 于是系统便认为该异常已经处理.
不捕获异常
因为异常不可以忽略, 如果try后的异常处理器不能匹配所抛出的异常, 那么这个异常会一直被传递到更高的语境中去.
1. terminate()函数
如果没有一个层次的异常处理器能够捕获某种异常, 库函数terminate()(在头文件
另外, 在以下两种情况terminate()也会被调用:
局部对象的析构函数抛出异常时, 栈正在进行清理工作; 或者是静态对象或全局对象的构造函数或析构函数抛出异常.
2. set_terminate()函数
通过set_terminate()可以设置自己的terminate()函数, set_terminate()返回被替换的指向terminate()函数的指针. 自定义的terminate()函数不能有参数, 其类型必须void, 而且不能返回(return), 也不能抛出异常, 它必须执行某种方式的程序终止逻辑.
异常匹配
匹配一个异常并不要求异常与其处理器完全匹配. 一个对象或者指向派生类对象的引用都会与其基类处理器匹配. 最好是通过引用而不是通过值来捕获异常, 这里因为如果是通过值的话, 这个异常会被切割成基类对象, 并且还可以避免再次拷贝异常对象.
view plaincopy to clipboardprint?
#include
using namespace std;
class exam
{
public:
exam() // constructor
{
cout << "exam()" << endl;
}
~exam() // destructor
{
cout << "~exam()" << endl;
}
class my_exception // 内嵌类
{
public:
my_exception() // 默认构造函数
{
cout << "my_exception()" << endl;
}
my_exception(const char* msg)
{
cout << msg << endl;
}
~my_exception() // 析构函数
{
cout << "~my_exception()" << endl;
}
};
void f()
{
throw my_exception("throw my_exception"); // 抛出异常
}
};
int main()
{
try
{
exam ex;
ex.f();
}
catch(exam::my_exception& e) // 1
//catch(exam::my_exception e) // 2
{
cout << "caught some execption" << endl;
}
getchar();
return 0;
}
#include
using namespace std;
class exam
{
public:
exam() // constructor
{
cout << "exam()" << endl;
}
~exam() // destructor
{
cout << "~exam()" << endl;
}
class my_exception // 内嵌类
{
public:
my_exception() // 默认构造函数
{
cout << "my_exception()" << endl;
}
my_exception(const char* msg)
{
cout << msg << endl;
}
~my_exception() // 析构函数
{
cout << "~my_exception()" << endl;
}
};
void f()
{
throw my_exception("throw my_exception"); // 抛出异常
}
};
int main()
{
try
{
exam ex;
ex.f();
}
catch(exam::my_exception& e) // 1
//catch(exam::my_exception e) // 2
{
cout << "caught some execption" << endl;
}
getchar();
return 0;
}
上面程序经过gcc编译后执行的结果是:
yaowenjue@ubuntu:~/exception$ ./catch_exam
exam()
throw my_exception
~exam()
caught some execption
~my_exception()
而VC++2008的结果则为:
exam()
throw my_exception
~my_exception()
~exam()
caught some execption
~my_exception()
很明显, VC多调用了一次my_exception对象的析构函数. 为什么呢? 暂时不知道. 如果注释掉1, 把2的注释去掉, 两个编译器都在最后多调用一次my_exception对象的析构函数.
而对于派生类和基类对象异常的捕获, 通常都是将派生类对象先catch,最后才是基类对象.
捕获所有异常
用省略号代码异常处理器的参数列表就可以捕获所有的异常:
view plaincopy to clipboardprint?
catch()
{
// code
}
catch()
{
// code
}
通常这个异常处理器放在最后面.
重新抛出异常
在一个异常处理器的内部, 使用不带参数的throw语句:
view plaincopy to clipboardprint?
catch(MyExceptionClass& myException)
{
// resource was not available, do something cleanup
throw;
}
catch(MyExceptionClass& myException)
{
// resource was not available, do something cleanup
throw;
}
这个throw语句把该异常传递到更高一层语境中的异常处理器.
1.3 清理
如果异常抛出时, 程序不做任何恰当的清理工作, 那么异常处理本身没有用处. C++异常处理必须确保当程序的流程离开一个作用域时, 对于属于这个作用域的所有由构造函数创建的对象, 它们的析构函数都一定会被调用. 然而, 如果构造函数在执行过程中抛出异常, 那么该对象的析构函数不会被调用. 再如果, 在构造函数分配资源, 这时如果构造函数发生异常, 那么这些资源不会被释放, 因为析构函数根本没有机会被执行. 这会造成"悬挂"指针.
view plaincopy to clipboardprint?
#include
#include
using namespace std;
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
};
class B
{
public:
void* operator new(size_t sz)
{
cout << "allocating a B" << endl;
throw 21;
}
void operator delete(void* p)
{
cout << "deallocating a B" << endl;
::operator delete(p);
}
};
class use_res
{
A* pa;
B* pb;
public:
use_res(int count = 1)
{
cout << "use_res()" << endl;
pa = new A[count];
pb = new B;
}
~use_res()
{
cout << "~use_res" << endl;
delete []pa;
delete pb;
}
};
int main()
{
try
{
use_res ur(3);
}
catch(int)
{
cout << "inside handler" << endl;
}
getchar();
return 0;
}
#include
#include
using namespace std;
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
};
class B
{
public:
void* operator new(size_t sz)
{
cout << "allocating a B" << endl;
throw 21;
}
void operator delete(void* p)
{
cout << "deallocating a B" << endl;
::operator delete(p);
}
};
class use_res
{
A* pa;
B* pb;
public:
use_res(int count = 1)
{
cout << "use_res()" << endl;
pa = new A[count];
pb = new B;
}
~use_res()
{
cout << "~use_res" << endl;
delete []pa;
delete pb;
}
};
int main()
{
try
{
use_res ur(3);
}
catch(int)
{
cout << "inside handler" << endl;
}
getchar();
return 0;
}
程序执行结果如下:
use_res()
A()
A()
A()
allocating a B
inside handler
可以使用以下两种方式之一来防止"不成熟"的资源分配而造成的资源泄漏:
1.在构造函数中捕获异常, 用于资源释放.
2.在对象的构造函数中分配资源, 并在析构函数中释放资源.
对上面的例子可以用方法1进行改写:
view plaincopy to clipboardprint?
class use_res
{
A* pa;
B* pb;
public:
use_res(int count = 1)
{
try
{
cout << "use_res()" << endl;
pa = new A[count];
pb = new B;
}
catch(int)
{
delete []pa;
}
}
~use_res()
{
cout << "~use_res" << endl;
//delete []pa;
//delete pb;
}
};
class use_res
{
A* pa;
B* pb;
public:
use_res(int count = 1)
{
try
{
cout << "use_res()" << endl;
pa = new A[count];
pb = new B;
}
catch(int)
{
delete []pa;
}
}
~use_res()
{
cout << "~use_res" << endl;
//delete []pa;
//delete pb;
}
};
这样就可以释放资源了. 当然也还可以用模板.
auto_ptr
auto_ptr是C++标准中的一个RAII封装类, 用于封装指向分配的堆内存的指针, 可以使得程序能够自动释放内存. auto_ptr类模板是在
1.4 标准异常
所有的标准异常都是从exception类派生的, exception类在
下面几个表格描述了标准异常类:
--------------------------------------------------------------------------------
exception 由C++标准库为所有抛出异常的类提供的类库. 使用what()函数可以取得exception对象初始化时被设置的可选字符串.
--------------------------------------------------------------------------------
logic_error 从exception类派生. 报告程序逻辑错误, 通过检查代码, 能够发现这类错误.
--------------------------------------------------------------------------------
runtime_error 从exception类派生. 报告运行时错误, 只有在程序运行时, 这类错误才可能被检测到.
--------------------------------------------------------------------------------
ios::failure 从exception类派生, 没有子类
--------------------------------------------------------------------------------
从logic_error派生的异常类:
--------------------------------------------------------------------------------
domian_error 报告违反了前置条件
--------------------------------------------------------------------------------
invalid_argument 表明抛出这个异常的函数接收到了一个无效的参数
--------------------------------------------------------------------------------
length_error 表明程序试图产生一个长度大于npos的对象
--------------------------------------------------------------------------------
out_of_range 报告一个参数越界错误
--------------------------------------------------------------------------------
bad_cast 抛出这个异常的原因是在运行时类型识别(runtime type identification)中发现程序执行了 一个无效的动态类型转换(dynamic_cast)表达式
--------------------------------------------------------------------------------
bad_typeid 当表达式typeid(*p)中的参数p是一个空指针时抛出这个异常
--------------------------------------------------------------------------------
从runtime_error派生的异常
--------------------------------------------------------------------------------
range_error 报告违反了后置条件.
--------------------------------------------------------------------------------
overflow_error 报告一个算术溢出错误
--------------------------------------------------------------------------------
bad_alloc 报告一个失败的存储分配
--------------------------------------------------------------------------------
1.5 异常规格说明
C++提供一种语法来抛出所要抛出的异常, 这就是可的异常规格说明(exception specification), 它是函数声明修饰符, 写在参数列表后面. 函数可能抛出的所有异常都应该写在throw之后的异常规格说明参数列表里:
void f1() throw(too_big, too_small); // 表明该函数可能会抛出两个异常
void f2(); //可能抛出任何类型的异常
void f3() throw(); //不会抛出任何异常
1. unexpected()函数
如果函数所抛出的异常没有在异常规格说明的集合中, 那么unexpected()函数会被调用. 默认的unexpected()会调用terminate()函数.
2. set_unexpected()函数
使用set_unexpected()函数可以设置自己的unexpected()函数, 其参数是一个指向返回值为void函数的指针. 自定义的unexpected()函数不可以有参数, 其类型必须是void.
view plaincopy to clipboardprint?
#include
#include
using namespace std;
class my_except:public exception
{
// some code
};
void foo1()
{
throw 1.1;
}
void foo() throw(char, int, bool, my_except)
{
foo1();
}
void my_uhandler()
{
cout << "my_uhandler called" << endl;
exit(0);
}
int main()
{
set_unexpected(my_uhandler);
try
{
foo();
}
catch(double)
{
cout << "caught a double exception" << endl;
}
catch()
{
cout << "This will never print" << endl;
}
return 0;
}
#include
#include
using namespace std;
class my_except:public exception
{
// some code
};
void foo1()
{
throw 1.1;
}
void foo() throw(char, int, bool, my_except)
{
foo1();
}
void my_uhandler()
{
cout << "my_uhandler called" << endl;
exit(0);
}
int main()
{
set_unexpected(my_uhandler);
try
{
foo();
}
catch(double)
{
cout << "caught a double exception" << endl;
}
catch()
{
cout << "This will never print" << endl;
}
return 0;
}
异常规格说明和继承
由于异常规格说明在逻辑上也是函数声明的一部分, 所以在继承层次结构中也必须保持一致. 例如, 如果基类的一个函数声明只抛出异常A和异常B, 那么其派生类中覆盖这个函数的函数不能在异常规格说明参数列表中添加其他的异常, 但可以减少抛出的异常或不抛出异常, 也可以指定任何is-a A来代替A.
什么时候使用异常规格说明
异常规格说明就像是函数原型: 它提醒使用者来编写异常处理代码以及处理什么异常. 它提醒编译器这个函数可能抛出异常, 让编译器能够在运行时检测违反该异常规格说明的函数.
任何时候如果要使用异常规格说明, 或调用使用异常规格说明的函数 最好编写自己的unexpected()函数, 在这个unexpected()函数中将消息写进日志, 然后抛出异常或终止程序. 应该避免在模板类中使用异常规格说明, 因为无法预料模板参数类(template parameter classes)所抛出的异常的类型.