一、异常处理

  异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理。异常使得我们能够将问题的检测与解决过程分离开来。程序的一半部分负责检测问题的出现,然后解决问题的任务传递给程序的另一部分。

 

1、抛出异常

  在C++语言中,我们通过抛出一条表达式来引发一个异常。被抛出的表达式的类型以及当前的调用链共同决定了哪段处理代码将被用来处理该异常。被选中的处理代码是在调用链中与抛出对象类型匹配的最近的处理代码。

  当执行一个throw时,跟在throw后面的语句将不再被执行。程序的控制权从thorw转移到与之匹配的catch模块。控制权从一处转移到另一处,还有两个重要的含义:

  • 沿着调用链的函数可能会提早退出。
  • 一旦程序开始执行异常处理代码,则沿着调用链创建的对象将被销毁。

1)栈展开

  当抛出一个异常后,程序暂停当前函数的执行过程并立即开始寻找与异常匹配的catch子句。如果对抛出异常的函数位于一个try语句块内,则检查与该try块关联的catch子句。如果找到了匹配的catch,就使用该catch处理异常。否则,如果该try语句块嵌套在其他try块中,则继续检查与外层try匹配的catch子句。如果仍然没有找到匹配的catch,则退出当前这个主调函数,继续在调用了刚刚退出的这个函数的其他函数中寻找,以此类推。

  上述过程被称为栈展开。一个异常如果没有被捕获,则它将终止当前的程序。

2)栈展开过程中对象被自动销毁

  在栈展开过程中,位于调用链上的语句块可能会提前退出。通常情况下,程序在这些块中创建了一些局部对象。如果在栈展开过程中退出了某个块,编译器将负责确保在这个块中创建的对象能被正确地销毁。如果某个局部对象的类型是类类型,则该对象的析构函数将被调用

  如果异常发生在构造函数中,则当前的对象可能只构造了一部分,我们也要确保已构造的程序能被正确地销毁

3)析构函数与异常

  析构函数总是会被执行的。因此,如果我们使用类来控制资源的分配,就能确保无论函数正常结束还是遭遇异常,资源都能被正确地释放。

  出于栈展开可能使用析构函数的考虑,析构函数不应该抛出不能被它自身处理的异常。如果析构函数需要执行某个可能抛出异常的操作,则该操作应该被放置在一个try语句块中,并且在析构函数内部得到处理。

  在栈展开的过程中,运行类类型的局部对象的析构函数。因为这些析构函数是自动执行的,所以它们不应该抛出异常。一旦在栈展开的过程中析构函数抛出了异常,并且析构函数自身没能捕获到该异常,则程序将被终止。

4)异常对象

  异常对象是一种特殊的对象,编译器使用异常抛出表达式来对异常对象进行拷贝初始化。

  异常对象位于由编译器管理的空间中,编译器确保无论最终调用的是哪个catch子句都能访问该空间。当异常处理完毕后,异常对象被销毁。

  抛出一个指向局部对象的指针是一种错误的行为。抛出指针要求在任何对应的处理代码存在的地方,指针所指的对象都必须存在。

  当我们抛出一条表达式时,该表达式的静态编译类型决定了异常对象的类型。如果一条throw表达式解引用一个基类指针,而该指针实际指向的是派生类对象,则抛出对象将被切掉一部分,只有基类部分被抛出。

 

2、捕获异常

  catch子句中的异常声明像在形参列表中一样,如果catch无须访问抛出的表达式的话,则我们可以忽略捕获形参的名字。

  声明的类型决定了处理代码所能捕获的异常类型。这个类型必须是完全类型,它可以是左值引用,但不能是右值引用

  当进入一个catch语句后,通过异常对象初始化异常声明中的参数。如果catch的参数类型是非引用类型,则该参数是异常对象的一个副本,在catch语句内改变该参数实际上改变的是局部副本而非异常对象本身;如果参数是引用类型,则和其他引用参数一样,该参数是异常对象的一个别名,此时改变参数也就是改变异常对象。

  如果catch的参数是基类类型,则我们可以使用其派生类类型的异常对象对其进行初始化。此时,如果catch的参数是非引用类型,则异常对象被切掉一部分,这与将派生类对象以值传递的方式传递给一个普通函数差不多。另一方面,如果catch的参数是基类的引用,则该参数将以常规方式绑定到异常对象上。

  异常声明的静态类型决定了catch语句所能执行的操作,如果catch的参数是基类类型,则catch无法使用派生类特有的任何成员。

1)查找匹配的代码

  因为catch语句总是按照其出现的顺序逐一进行匹配的,所以当程序使用具有继承关系的多个异常时必须对catch语句的顺序进行组织和管理,使得派生类异常的处理代码出现在基类异常的处理代码之前。

  异常和catch异常声明的匹配规则受到很多限制。绝大多数类型转换都不被允许,除了一些细小的差别之外,要求异常的类型和catch声明的类型是精确匹配的:

  • 允许从非常量向常量的类型转换,也就是说,一条非常量对象的throw语句可以匹配一个接受常量引用的catch语句。
  • 允许派生类向基类的类型转换。
  • 数组被转换成指向数组元素类型的指针,函数被转换成指向该函数类型的指针。

除此之外,包括标准算术类型转换和类类型转换在内,其他所有转换规则都不能在匹配catch的过程中使用。

2)重新抛出

  有时,一个单独的catch语句不能完整地处理某个异常。在执行了某些校正操作之后,当前的catch可能会决定由调用链更上一层的函数接着处理异常。一条catch语句通过重新抛出的操作来将异常传递给另外一个catch语句。重新抛出仍然是一条throw语句,只不过不包含表达式:

  throw;

空的throw语句只能出现在catch语句或catch语句直接或间接调用的函数之内。如果在处理代码之外的区域遇到了空的throw语句,编译器将调用terminate。

  一个重新抛出语句并不指定新的表达式,而是将当前的异常对象沿着调用链向上传递。只有当catch异常声明是引用类型时我们对参数所做的改变才会保留并继续传播。

 1 #include <iostream>
 2 #include <sstream>
 3 #include <memory>
 4 #include <string>
 5 #include <stdexcept>
 6 
 7 void func() {
 8     throw std::runtime_error("run time error");
 9     std::cout << __FUNCTION__ << std::endl;
10 }
11 
12 void g() {
13     try
14     {
15         func();
16     }
17     catch (std::runtime_error &e)
18     {
19         std::cout << "hello" << std::endl;
20         throw;
21     }
22     std::cout << __FUNCTION__ << std::endl;
23 }
24 int main()
25 {
26     try
27     {
28         g();
29     }
30     catch (std::runtime_error &e)
31     {
32         std::cout << e.what() << std::endl;
33     }
34     return 0;
35 }
View Code

 

3)捕获所有异常的代码

  为了一次性捕获所有代码,我们使用省略号作为异常声明,这样的处理代码称为捕获所有异常的处理代码,形如catch(...)。一条捕获所有异常的语句可以与任意类型的异常匹配。

 1 #include <iostream>
 2 #include <sstream>
 3 #include <memory>
 4 #include <string>
 5 #include <stdexcept>
 6 
 7 void func() {
 8     throw std::runtime_error("run time error");
 9     std::cout << __FUNCTION__ << std::endl;
10 }
11 
12 void g() {
13     try
14     {
15         func();
16     }
17     catch (...)
18     {
19         std::cout << "hello" << std::endl;
20         throw;
21     }
22     std::cout << __FUNCTION__ << std::endl;
23 }
24 int main()
25 {
26     try
27     {
28         g();
29     }
30     catch (std::runtime_error &e)
31     {
32         std::cout << e.what() << std::endl;
33     }
34     return 0;
35 }
View Code

  catch(...)既能单独出现,也能与其他几个catch语句一起出现。

 

3、函数try语句块与构造函数

  通常情况下,程序执行的任何时刻都可能发生异常,特别是异常可能发生在处理构造函数初始值的过程中。构造函数在进入其函数体之前首先执行初始值列表。因为在初始值列表抛出异常时构造函数体内的try语句块还未生效,所以构造函数体内的catch语句无法处理构造函数初始值列表抛出的异常。

  处理构造函数初始值异常的唯一方法是将构造函数写成函数语句块。函数try语句块使得一组catch语句既能处理构造函数体(或析构函数体),也能处理构造函数的初始化过程(或析构函数的析构过程)

 1 #include <iostream>
 2 #include <sstream>
 3 #include <memory>
 4 #include <string>
 5 #include <stdexcept>
 6 
 7 template<typename T>
 8 class Base {
 9 public:
10     Base()try
11         :x(0) {
12         throw std::runtime_error("this is exception.");
13         std::cout << __FUNCTION__ << std::endl;
14     }
15     catch (std::exception &e) {
16         std::cout << e.what() << std::endl;
17     }
18 private:
19     T x;
20 };
21 int main()
22 {
23     try
24     {
25         Base<int> b;
26     }
27     catch (std::exception &e)
28     {
29         std::cout << e.what() << std::endl;
30     }
31     return 0;
32 }
View Code

关键字try出现在表示构造函数初始值列表的冒号之前。与这个try关联的catch既能处理构造函数体抛出的异常,也能处理成员初始化列表抛出的异常。

  注意:如果在参数初始化的过程中发生了异常,则该异常属于调用表达式的一部分,并在调用者所在的上下文处理。

 

4、noexcept异常说明

  对于用户及编译器来说,预先知道某个函数不会抛出异常显然大有裨益。首先,知道函数不会抛出异常有助于简化调用该函数的代码;其次,如果编译器确认函数不会抛出异常,它就能执行某些特殊的优化操作,而这些优化操作并不适用于可能出错的代码。

  在C++11新标准中,我们可以通过提供noexcept说明指定某个函数不会抛出异常。其形式是关键字noexcept紧跟在函数的参数列表后面,用以标识该函数不会抛出异常。

  void recoup(int)noexcept;

  对于一个函数来说,noexcept要么出现在该函数的所有声明和定义语句中,要么一次也不出现。我们也可以在函数指针的声明和定义中指定noexcept。在typedef或类型别名中则不能出现noexcept。在成员函数中,noexcept说明符需要跟在const以及引用限定符之后,而在final、override或虚函数的=0之前。

1)违反异常说明

  一旦一个noexcept函数抛出了异常,程序就会调用terminate以确保遵守不在运行时抛出异常的承诺。上述过程对是否执行栈展开未做约定,因此noexcept可以用在两种情况下:一是我们确认函数不会抛出异常,二是我们根本不知道该如何处理异常。

  指明某个函数不会抛出异常可以令该函数的调用者不必再考虑如何处理异常。无论是函数确实不抛出异常,还是程序终止,调用者都无须为此负责。

2)异常说明的实参

  noexcept说明符接受一个可选的实参,该实参必须能够转换为bool类型:如果实参是true,则函数不会抛出异常;如果实参是false,则函数可能抛出异常。

  void recoup(int)noexcept(true);

  void recoup(int)noexcept(false);

3)noexcept运算符

  noexcept运算符时一个一元运算符,它的返回值是一个bool类型的右值常量表达式,用于表示给定的表达式是否会抛出异常。noexcept不会对其运算对象求值。

 1 #include <iostream>
 2 #include <sstream>
 3 #include <memory>
 4 #include <string>
 5 #include <stdexcept>
 6 
 7 void f(int)noexcept(true) {
 8     std::cout << __FUNCTION__ << std::endl;
 9 }
10 void g(int)noexcept(false) {
11     std::cout << __FUNCTION__ << std::endl;
12 }
13 int main()
14 {
15     std::cout << noexcept(f(1)) << std::endl;
16     std::cout << noexcept(g(2)) << std::endl;
17     return 0;
18 }
View Code

4)异常说明与指针、虚函数和拷贝控制

  尽管noexcept说明符不属于函数类型的一部分,但是函数的异常说明仍然会影响函数的使用。

  函数指针与所指的函数必须有一致的异常说明。如果我们为某个指针做了不抛出异常的说明,则该指针只能指向不抛出异常的函数。如果我们显示或隐式的说明了指针可能抛出异常,则该指针可以指向任何函数,即使是承诺了不抛出异常的函数也可以。

 1 #include <iostream>
 2 #include <sstream>
 3 #include <memory>
 4 #include <string>
 5 #include <stdexcept>
 6 
 7 void f(int)noexcept { // 不抛出异常
 8     std::cout << __FUNCTION__ << std::endl;
 9 }
10 void g(int){ // 可能抛出异常
11     std::cout << __FUNCTION__ << std::endl;
12 }
13 int main()
14 {
15     void(*p1)(int) noexcept = f;
16     void(*p2)(int) = g;
17     return 0;
18 }
View Code

  如果一个虚函数承诺了它不会抛出异常,则后续派生出来的虚函数也必须做出同样的承诺。与之相反,如果基类的虚函数允许抛出异常,则派生类的对应函数既可以允许抛出异常,也可以不允许抛出异常。

 

5、异常类层次

  what函数返回一个const char *,该指针指向一个以null结尾的字符数组,并且确保不会抛出异常。

 

posted on 2019-02-01 15:48  acgame  阅读(301)  评论(0编辑  收藏  举报