异常结构
如果程序的某部分产生一个无法处理的问题时,就需要用到异常处理,检测出异常的部分(发出方)应该发出某种信号(异常)表示程序遇到故障无法继续下去,发出方发出异常就完成自己的工作,无需关注故障在何处解决,如何解决。
C++异常处理包括:
- throw表达式:引发(raise)异常
- try语句块:处理异常
- exception异常类:异常的具体信息。 ```cpp
// C++基本的异常处理模块。
void func(){ try { ……; // 程序代码 throw runtime_error(“runtime_error”); // 引发runtime_error异常 // 类似return,不会再执行下面部分代码 ……; // 程序代码 } catch (Exception1 e1) { // 异常声明 ……; // 处理异常Exception1代码 } catch (Exception2 e2) { // 异常声明 ……; // 处理异常Exception2代码 } ……; // 程序代码,如果被上面任一catch命中,则执行为catch函数体之后 // 立即执行此处代码。 }
<a name="ZBHYU"></a>
# 异常处理
假设throw引发了一个异常,异常处理过程如下:
```cpp
void func1(){
try{ // try1
func2();
......; // 代码code11
}
catch(Exception1 e){
......; // 代码code12
}
......; // 代码code13
}
void func2(){
try{ // try21
try { // try22
......; // 代码code21
throw ExceptionX("XXX"); // 引发异常,这里的ExceptionX仅作示范,可以是任何异常
......; // 代码code22
}
catch(Exception e){
......; // 代码code23
}
......; // 代码code24
}
catch(Exception2 e){
......; // 代码code25
}
......; // 代码code26
}
int main(){
try { // tryMain
func1(); //
......; // 代码code01
}
catch(Exception e){
......; // 代码code02
}
......; // 代码code03
}
// 假设以上代码中,只引发了func2中的那个异常。当引发该异常时:
//
// 若在try22中捕获,则执行代码为:(异常引发以后的代码)
// code23 -> code24 -> code26 -> code11 -> code13 -> code01 -> code03
//
// 若在try21中捕获,则执行代码为:
// code25 -> code26 -> ......
//
// 若在try1中捕获,则执行代码为:
// code12 -> code13 -> ......
//
// 若在tryMain中捕获,则执行代码为:
// code02 -> code03
//
// 若没有被任何try catch捕获,则执行代码为:
// terminate(); // 系统库函数,行为和平台相关,一般是让程序非正常退出。
//
// 上述过程叫做栈展开过程。
标准异常exception
异常对象如果是:
- 类类型时,必须有以下函数:
- public的析构函数。
- public的拷贝构造函数。
- public的移动构造函数。
- 数组、函数类型时:
- 转换成指针类型。
异常对象位于编译器管理的空间,因为要确保调用哪个catch都能访问到它,处理完毕之后销毁。
throw runtime_error("god damn it"); //抛出一个异常对象
BaseError* err = new DerivedError("god damn it");
throw *err; //抛出的是err的BaseError部分,DerivedError特有部分将被抛弃。
C++标准库定义了一组异常类,在4个头文件中:
- exception:所有异常父类,只报告异常发生,不会提供任何额外信息。
- bad_exception:处理无法预期的异常时非常有用。
- exception:最常见问题
- runtime_error:不能通过读取代码检测到的异常。
- range_error:生成结果超出有意义的值域范围
- overflow_error:计算上溢
- unerflow_error:计算下溢
- logic_error:逻辑错误,可以通过读取代码检测到的异常。
- domain_error:参数对应的结果值不存在
- invalid_argument:无效参数
- length_error:试图创建一个超出该类型最大长度的对象
- out_of_range:使用一个超出有效范围的值,如vector的下标访问。
- bad_alloc:new抛出。
- bad_cast:dynamic_cast抛出。
- bad_typeid:typeid抛出。
这些异常的继承结构: 常见几个异常类的结构:
class exception {
public:
exception() = default; // 默认构造函数,exception e; 默认初始化。
exception(const exception&); // 拷贝构造函数。
exception& operator=(const exception&); // 拷贝赋值运算符
virtual ~exception();
virtual const char* what() noexcept; // 返回用于初始化异常对象的信息。
}
class bad_cast : public exception {
public:
bad_cast() = default;
}
class bad_alloc : public exception {
public:
bad_alloc() = default;
}
class runtime_error : public exception {
public:
runtime_error() = delete; // !!注意没有默认构造函数,必须带参数构造。
runtime_error(const char*); //
runtime_error(string); //
}
class logic_error : public exception {
public:
logic_error() = delete; // !!注意没有默认构造函数,必须带参数构造。
logic_error(const char*); //
logic_error(string); //
}
捕获异常catch
catch(exception e){ // 异常声明,值传递(拷贝副本)
// 假设传来的异常类型是DerivedException,继承自exception
// e将只有传来对象的exception部分。发生截取情况
......
}
catch(exception &e){ // 异常声明,引用传递(同一个对象)
// 引用、指针都不会发生上面的截取情况。
// 最好是定义成引用或者指针类型。
......
}
catch(const exception &e){ // 这是最佳异常声明方式,const类型引用传递(同一个对象)
// 引用、指针都不会发生上面的截取情况。
......
throw; // 重新抛出异常,只能出现catch中,或者catch中调用的函数中。
// 否则terminate,如果是引用、指针类型,可以将修改后的异常抛出。
}
catch(...){ // 捕获所有异常
}
传入的异常对象与catch形参的转换规则:
- 允许非const像const转换
- 允许派生类向基类的转换
- 允许数组转换成数组指针
- 允许函数转换成函数指针
- 除此之外的都不行:
- 算术类型转换:int转double之类,都不允许,必须精确匹配。
根据这个转换规则,我们应该把派生类放前面,基类放后面,因为派生类可以匹配到基类。
在函数体内try catch无法捕获执行初始值列表产生的异常。下面就是专门针对构造函数的try catch,可以捕获初始值列表异常,但是无法捕获构造函数参数的异常,这应该在调用处来处理。
Class::Class(int a, int b) try // 捕获初始值列表异常
:m_a(a)
,m_b(b){
}
catch(const std::bad_alloc &e){
}
定义新异常
#include <iostream>
#include <exception>
class MyException : public Exception {
public:
const char* what() override {
return "this is my exception";
}
};
int main(){
try{
throw MyException();
}
catch(const Exception& e){ // 最好是用引用、形式,避免不必要的拷贝。
std::cout << e.what() << std::endl;
}
return 0;
}
不抛出说明noexcept
不抛出异常说明,有什么用?明确告诉外界,我这个函数对异常的态度有两个:
- 1、我绝对不会产生异常,是安全可以放心使用的,不用为处理调用我可能产生的异常而做额外的工作;
- 2、使用者不要考虑我产生的异常:在noexcept函数中抛出异常是必然会程序terminate。
总之可以让外界好配合,对程序有一定提升优化,如编译器。
void recoup(int) noexcept; // 不会抛出异常,做了不抛出声明nothrowing,如果真抛出,则直接terminate
void alloc(int); // 可能抛出异常
void fuck() noexcept; // 在fuck的声明和定义中都必须加上。
void fuck() noexcept{} // 在fuck的声明和定义中都必须加上。
void fuck() throw(); // 这是老版的不抛出说明
auto fuck() noexcept -> int{// 必须在尾置类型之前。
}
// 在typedef、using类型别名中不能出现
void (*pf1)(int) noexcept = fuck; // 可以在函数指针声明和定义中出现。
// fuck必须是noexcept否则报错
class a{
void fuck() const noexcept; // 必须在const引用限定符之后
void fuck() noexcept final; // 必须在final之前
void fuck() noexcept override; // 必须在override之前
virtual void fuck() noexcept = 0; // 必须在虚函数=0之前
virtual fuck() noexcept; // 父类的虚函数是noexcept,子类相应的必须显式noexcept
}
上面的noexcept是一个说明符号,它也可以是一个运算符。
noexcept(fuck(i)); // fuck函数是否不会抛出异常,是返回true,否返回false
noexcept(e); // 当e函数和内部所有用到的函数都做了不抛出说明
// 且e没有throw语句,返回true,否则false
void f() noexcept(noexcept(g())); // f和g的异常说明一致