8. 异常

8.1 基本用法

对异常的处理:

  • 引发异常;
  • 使用异常处理程序(exception handler)捕捉异常;
  • 使用try块。

要在C++使用异常机制,可以在程序的try块内出现问题时使用throw语句抛出异常(一个变量/常量)。
try块中抛出的异常可以利用其后的catch块(异常处理程序)捕捉。
catch块通过识别抛出的异常类型捕捉异常(catch块类似于函数定义),并在执行完后返回到抛出异常的语句的下一个语句。

如果程序引发了异常而没有在try块内或没有匹配的处理程序(catch块)函数会调用abort()函数,不过这一行为可以修改。

8.2 将对象用作异常类型

通常,将对象作为异常抛出。这样做的重要优点之一是可以使用不同的异常类型来区分引发的问题。

8.3 栈解退

C++通常将调用函数的指令的地址(返回地址)放在栈中放在栈中。当被调用函数执行完毕(return)时,程序利用此地址确定从哪里继续执行。
此外,若在函数中创建了自动变量,后者也会被放进栈中,若调用了一个函数,被调用函数的信息也会被存入栈中。每个函数在执行完后都会按压栈的逆序释放栈中的信息,直到释放了第一个返回地址。

然而如果函数是因为异常而非return语句返回,则不会在释放了第一个返回地址后停止释放,而是会继续释放,直到释放了第一个在try块内的返回地址。
然后会将控制权交给紧跟其后的catch块,而非接着运行函数调用的下一条语句。

在栈解退过程中,自动变量会被自动释放,若自动变量是一个对象,那么对象的析构函数也会被自动调用。

8.4 异常规范(不提倡使用)

double harm(double a) throw(bad_thing);
double marm(double a) throw();

异常规范出现于函数原型或定义中,可以包含类型列表(指出可能发生的异常,可以有多个),也可不包含(不会发生异常)。
要指出不会引发异常,还可以用C++11中的关键字noexcept代替空类型列表的异常规范。
另外,还有运算符noexcept(),可以判断操作数是否会发生异常。

原则上,函数的异常规范应包含函数调用的函数的异常规范。

8.4 其他异常特征

8.4.1 catch 块使用引用

引发异常时编译器总是会创建一个异常的临时变量拷贝。这样,catch捕捉到的异常变量不是异常变量本身而只是一个副本,即使使用了引用。
但是使用引用还有其他好处:
使用引用可以避免创建另一个临时变量,减少不必要的开支。
如果使用基类的引用,那么这个catch块就还可以捕捉所有派生类类型的异常。不过如果要这么做要将处于继承层次上部的类型(如果想要特殊处理的话)放在靠后的位置。

8.4.2 默认异常处理程序

如果要捕捉未知类型的异常,使用类似以下这样的try块:
未确定

catch(...)
{
    cout << "undetermined error";
}

8.4.3 由于异常而引发的问题

  1. 异常在带有异常规范的函数中引发,如果引发的异常不能与异常规范中的任何一个类型匹配,则称这个异常为意外异常(unexpected exception)
  2. 异常不是在函数中引发的、函数没有异常规范或异常符合异常规范,这时异常如果没有被捕捉,则称这个异常为未捕捉异常(uncaught exception)

上述函数是指与catch块在同一层及更深层的函数调用。

通常情况下,意外异常和未捕捉异常都会导致异常终止,但是可以修改这一行为。
程序在发现以上两种异常时并不会直接结束程序,而是会分别调用unexpected()terminate()函数。默认情况下,terminate()函数都会调用abort()函数,而unexpected()函数会调用terminate()函数。
两个函数声明在头文件exception中。头文件exception中还声明了set_unexpected()set_terminate()函数,两个函数都接受一个返回值类型为void,参数列表为空的函数指针作为参数。
函数指针也在exception头文件中进行了定义:

typedef void (*terminate_handler) ();
typedef void (*unexpected_handler) ();

相比于terminate_handler函数,unexpected_handler函数的行为受到更严格的限制:

  • 通过调用abort()exit()terminate()终止程序。
  • 修改unexpected(),在unexpected()中引发异常。
    • 引发的异常与原来的异常规范匹配,则函数按普通的方式正常运行,即寻找与新异常匹配的catch块。
    • 如果不匹配,且原来的异常规范中没有包括bad_exception,异常程序会调用terminate()函数,而不是再次调用unexpected()函数。
    • 如果·不匹配,但原来的异常规范中包括bad_exception,那么不匹配的异常会自动被bad_exception代替。

8.5 exception 类

异常 描述
std::exception 该异常是所有标准 C++ 异常的父类。
std::bad_alloc 该异常可以通过 new 抛出。
std::bad_cast 该异常可以通过 dynamic_cast 抛出。
std::bad_exception 这在处理 C++ 程序中无法预期的异常时非常有用。
std::bad_typeid 该异常可以通过 typeid 抛出。
std::logic_error 理论上可以通过读取代码来检测到的异常。
std::domain_error 当使用了一个无效的数学域时,会抛出该异常。
std::invalid_argument 当使用了无效的参数时,会抛出该异常。
std::length_error 当创建了太长的 std::string 时,会抛出该异常。
std::out_of_range 该异常可以通过方法抛出,例如 std::vectorstd::bitset<>::operator[]()
std::runtime_error 理论上不可以通过读取代码来检测到的异常。
std::overflow_error 当发生数学上溢时,会抛出该异常。
std::range_error 当尝试存储超出范围的值时,会抛出该异常。
std::underflow_error 当发生数学下溢时,会抛出该异常。

异常具有这样的一种特性:类似于类,可以改变你的编程方式。

exception头文件中定义了exception类,类中有虚方法what()(除了构造函数和析构函数外,只有这个成员),what()返回一个字符串。
因此可以以其为基类派生出拥有用户定义what()函数的派生类。

这里部分未说明的异常类见[其他内容 1.3.1 RTTI 的原理](file:///F:/Documents/vnote_notebooks/C++/C++语言/其他内容.md#toc_3)。

8.5.1 stdexcept 异常类

头文件stdexcept定义了其他几个异常类。

首先以公有继承的方式从exception派生出了logic_errorruntime_error类。
这些类的构造函数接受一个string对象作为参数,其what函数接受一个C字符串。
ISO C++为新版本的C++定义了一个接受C字符串的构造函数。
另外的信息可以参考头文件。以下为内容节选:

explicit
    logic_error(const string& __arg) _GLIBCXX_TXN_SAFE;

#if __cplusplus >= 201103L
    explicit
    logic_error(const char*) _GLIBCXX_TXN_SAFE;
#endif

#if _GLIBCXX_USE_CXX11_ABI || _GLIBCXX_DEFINE_STDEXCEPT_COPY_OPS
    logic_error(const logic_error&) _GLIBCXX_USE_NOEXCEPT;
    logic_error& operator=(const logic_error&) _GLIBCXX_USE_NOEXCEPT;
#endif
// ...
virtual const char*
    what() const _GLIBCXX_TXN_SAFE_DYN _GLIBCXX_USE_NOEXCEPT;

logic_error又派生出了更具体的异常类:

  1. domain_error:建议用来处理数学的定义域问题。
  2. invalid_argument:建议用来处理函数参数问题(内容问题)。
  3. length_error:建议用来处理数组、动态分配的内存等长度不足的问题。
  4. out_of_range:建议用来处理索引超出范围的问题。

runtime_error派生出了以下异常类:

  1. range_error:计算结果可能不在函数允许的范围内但没有发生上溢下溢
  2. overflow_error:发生在浮点数计算中,一般来说,存在浮点类型可以表示的最大(绝对值)非零值,如果计算结果比这个值还大的话会发生下溢
  3. underflow_error:发生在浮点数计算中,一般来说,存在浮点类型可以表示的最小(绝对值)非零值,如果计算结果比这个值还小的话会发生下溢

8.5.2 bad_alloc 异常和 new

在以前,new请求内存出错(通常是内存不足)时,new返回一个空指针nullptr
但现在,new会引发bad_alloc异常。bad_alloc异常和nothrow都定义在头文件new中。
另外,在头文件new中还定义了bad_array_new_length异常类型。

为了尽量向下兼容,C++提供了返回空指针的new

int * pi = new (nothrow) int;
int * pi = new (nothrow) int[500];

8.6 有关异常的注意事项

  1. 应该在设计程序时就加入异常处理功能,而不是以后再添加。
  2. 异常规范不适用于模板,因为模板函数引发的异常可能随特定的具体化而异。
  3. 异常和动态内存分配并非总能协调工作。

对于第三点,一般有两种解决方案:

  1. catch块中添加相应地delete语句。
  2. 使用智能指针取代普通指针来管理动态分配的内存。

猜你喜欢

转载自www.cnblogs.com/alohana/p/12571695.html