Skip to content

Latest commit

 

History

History
155 lines (113 loc) · 8.61 KB

16-异常处理.md

File metadata and controls

155 lines (113 loc) · 8.61 KB

异常处理

C++之父BjarneStroustrup在《TheC++ProgrammingLanguage》中讲到:一个库的作者可以检测出发生了运行时错误, 但一般不知道怎样去处理它们(因为和用户具体的应用有关);另一方面,库的用户知道怎样处理这些错误,但却无法检查它们何时发生(如果能检测,就可以再用户的代码里处理了,不用留给库去发现)。
提供异常的基本目的就是为了处理上面的问题。基本思想是:让一个函数在发现了自己无法处理的错误时抛出(throw)一个异常,然后它的(直接或者间接)调用者能够处理这个问题。

异常机制提供程序中错误检测与错误处理部分之间的通信。C++ 的异常处理中包括:

  • throw 表达式,错误检测部分使用这种表达式来说明遇到了不可处理的错误。可以说throw 引发了异常条件。
  • try 块,错误处理部分使用它来处理异常。try 语句块以 try 关键字开始,并以一个或多个 catch 子句结束。在 try 块中执行的代码所抛出(throw)的异常,通常会被其中一个 catch 子句处理。由于它们“处理”异常,catch 子句也称为处理代码。
  • 由标准库定义的一组异常类,用来在 throw 和相应的 catch 之间传递有关的错误信息。

1 标准异常

异常是程序在执行期间产生的问题。C++ 异常是指在程序运行时发生的特殊情况,比如尝试除以零的操作。 异常提供了一种转移程序控制权的方式。C++ 异常处理涉及到三个关键字:try、catch、throw

//抛出异常
throw runtime_error("a errro message");

//处理异常
try{
   // 保护代码
}catch( ExceptionName e1 ){
   // catch 块
}catch( ExceptionName e2 ){
   // catch 块
}catch( ExceptionName eN ){
   // catch 块
}

C++ 标准库定义了一组类,用于报告在标准库中的函数遇到的问题。程序员可在自己编写的程序中使用这些标准异常类。标准库异常类定义在四个头文件中:

  • exception头文件定义了最常见的异常类,它的类名是 exception。这个类只通知异常的产生,但不会提供更多的信息
  • stdexcept头文件定义了几种常见的异常类,这些类型在下表中列出
  • new头文件定义了 bad_alloc 异常类型,提供因无法分配内存而由 new 抛出的异常
  • type_info头文件定义了 bad_cast 异常类型。

常见异常如下所示:

  • 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、bad_alloc、bad_cast异常,不能为它们提供任何初始化值, 而其他异常在初始化时,需要提供string类型的异常信息。

标准库异常类

标准库异常类只提供很少的操作,包括创建、复制异常类型对象以及异常类型对象的赋值。 exception、bad_alloc 以及bad_cast类型只定义了默认构造函数, 无法在创建这些类型的对象时为它们提供初值。其他的异常类型则只定义了一个使用 string 初始化式的构造函数。当需要定义这些异常类型的对象时, 必须提供一个string 参数。string初始化式用于为所发生的错误提供更多的信息。

异常类型只定义了一个名为 what 的操作。这个函数不需要任何参数,并且返回 const char* 类型值。 它返回的指针指向一个 C 风格字符串。使用 C 风格字符串的目的是为所抛出的异常提出更详细的文字描述。


2 处理异常

栈解旋(unwinding)

异常被抛出后,从进入try块起,到异常被抛掷前,这期间在栈上构造的所有对象,都会被自动析构。 析构的顺序与构造的顺序相反,这一过程称为栈的解旋(unwinding)

异常接口声明

  • 为了加强程序的可读性,可以在函数声明中列出可能抛出异常的所有类型,例如:void func()throw(A,B,C);这个函数func能够且只能抛出类型A、B、C及其子类型的异常。
  • 如果在函数声明中没有包含异常接口声明,则此函数可以抛任何类型的异常,例如:void func()
  • 一个不抛任何类型异常的函数可声明为:void func()throw()
  • 如果一个函数抛出了它的异常接口声明锁不允许抛出的异常,那么会导致程序调用terminate函数,中断执行

异常类型和异常变量的生命周期

  • throw的异常是有类型的,可以是数字、字符串、类对象。
  • throw的异常是由类型的,catch需严格匹配异常类型。

catch类型:

  • 如果接受异常时,用异常变量去接,那么会拷贝匿名异常对象(创建了两个对象),异常处理完后对象会析构
  • 如果接受异常时,用引用去接,那么不需要拷贝(只创建了一个对象),异常处理完后对象会析构
  • 如果接受指针,那么要考虑指针是否是野指针的问题(抛出异常的函数,应该抛出堆上的异常),需要手动delete

如何编写自己的异常类

  • 建议自己的异常类要继承标准异常类。因为C++中可以抛出任何类型的异常,所以我们的异常类可以不继承自标准异常,但是这样可能会导致程序混乱,尤其是当我们多人协同开发时。
  • 当继承标准异常类时,应该重载父类的what函数和虚析构函数。
  • 因为栈展开的过程中,要复制异常类型,那么要根据你在类中添加的成员考虑是否提供自己的复制构造函数。

3 使用预处理器进行调试

NDEBUG 预处理变量

可使用 NDEBUG 预处理变量实现有条件的调试代码

int main() {
#ifndef NDEBUG
  cerr << "starting main" << endl;
#endif
  //

如果 NDEBUG 未定义,那么程序就会将信息写到 cerr 中。如果 NDEBUG 已经定义了,那么程序执行时将会跳过 #ifndef#endif 之间的代码。 默认情况下,NDEBUG 未定义(表示正在Debug!),这也就意味着必须执行 #ifndef#endif 之间的代码。在开发程序的过程中, 只要保持 NDEBUG 未定义就会执行其中的调试语句。开发完成后,要将程序交付给客户时,可通过定义NDEBUG 预处理变量,(有效地)删除这些调试语句。

大多数的编译器都提供定义 NDEBUG 命令行选项 :

相等于#define NDEBUG
gcc -DNDEBUG main.c

调试常量

  • __FILE__ 文件名
  • __LINE__ 当前行号
  • __TIME__ 文件被编译的时间
  • __DATE__ 文件被编译的日期

assert 宏

另一个常见的调试技术是使用 NDEBUG 预处理变量以及 assert 预处理宏。assert 宏是在 cassert 头文件中定义的。

预处理宏有点像函数调用。assert 宏需要一个表达式作为它的条件:

assert(expr)

与异常不同(异常用于处理程序执行时预期要发生的错误),程序员使用 assert 来 测试“不可能发生”的条件 。 例如,对于处理输入文本的程序,可以预测全部给出的单词都比指定的阈值长。那么程序可以包含这样一个语句:

assert(word.size() > threshold);

在测试过程中,assert 等效于检验数据是否总是具有预期的大小。一旦开发和测试工作完成,程序就已经建立好, 并且定义了 NDEBUG。 在成品代码中,assert 语句不做任何工作,因此也没有任何运行时代价 。当 然也不会引起任何运行时检查。assert 仅用于检查确实不可能的条件,这只对程序的调试有帮助,但不能用来代替运行时的逻辑检查,也不能代替对程序可能产生的错误的检测。