C++ Primer 笔记——异常处理
1.栈展开过程沿着嵌套函数的调用链不断查找,直到找到了与异常匹配的catch句子为止,或者也可能一直没找到匹配的catch,则程序将调用terminate,退出主函数后查找过程终止。假设找到了一个catch,则执行其中的代码,执行完之后,找到与try块关联的最后一个catch子句之后的点,并从这里继续执行。
2.如果在栈展开过程中推出了某个块,编译器将负责确保在这个块中创建的对象能被正确销毁,如果异常发生在构造函数中,即使某个对象只构造了一部分,我们也要确保已构造的成员能被正确地销毁。
3.在栈展开的过程中,运行类类型的局部对象的析构函数。因为这些析构函数是自动执行的,所以它们不应该抛出异常,如果要抛出异常,则应该在析构函数内部得到处理。一旦在栈展开的过程中析构函数抛出了异常,并且析构函数自身没能捕获到该异常,则程序将被终止。
4.异常对象是一种特殊对象,编译器使用异常抛出表达式来对异常对象进行拷贝初始化。因此throw语句中的表达式必须拥有完全类型。而且如果该表达式是类类型的话,则相应的类必须含有一个可访问的析构函数和一个可访问的拷贝或移动构造函数。如果是数组类型或函数类型,则表达式将被转换成与之对应的指针类型。
5.异常对象位于编译器管理的空间中,编译器确保无论最终调用的是哪个catch子句都能访问该空间。当异常处理完毕后,异常对象被销毁。
6.当我们抛出一条表达式时,该表达式的静态编译时类型决定了异常对象的类型。如果throw解引用一个指向派生类的基类指针,则抛出的对象将被切掉派生类的部分。
7.声明的类型决定了处理代码所能捕获的异常类型,这个类型必须是完全类型,它可以是左值引用,但不能是右值引用。
8.通常情况下,如果catch接受的异常与某个继承体系有关,则最好将该catch的参数定义成引用类型。
9.在搜寻catch语句的过程中,挑选出来的应该是第一个与异常匹配的catch语句,因此,越是专门的catch越应该置于整个catch列表前端。
10.异常声明中绝大多数类型转换都不被允许,除了以下几点情况:
- 允许从非常量向常量的类型转换
- 允许派生类向基类的类型转换
- 数组或函数被转换成指针
11.有时候一个单独的catch语句不能完整地处理某个异常,可以通过重新抛出的操作将异常传递给另外一个catch语句,这里的重新抛出仍然是一条throw,只不过不包含任何表达式,空throw语句只能出现在catch语句或catch语句直接或间接调用的函数之内。如果在其他地方使用,编译器将调用terminate。很多时候,catch语句会改变其参数的内容,只有当异常声明是引用类型时,重新抛出的参数才会保留改变的内容继续传播。
struct test { int *id = nullptr; int *count = nullptr; }; struct testex : public test { }; testex t; void dotest() { try { if (!t.id) throw t; // 这里实际上对t做了拷贝 } catch (testex &e) { e.id = new int(1); if(!t.count) throw; } } int main() { try { dotest(); } catch (test e) { e.count = new int(1); // 这里的e的id已经指向1 } // 注意,到这里我们的全局变量t没有任何变化,如果想要t被改变,我们应该throw t的指针,即throw &t; return 0; }
12.为了一次性捕获所有的异常类型,我们使用省略号作为异常声明,如果catch(...)与其他几个catch语句一起出现,则catch(...)必须在最后的位置,出现在捕获所有异常语句后面的catch语句将永远不会被匹配。
13.构造函数体内的catch语句无法处理构造函数初始值列表抛出的异常。我们可以将构造函数写成函数try语句块的形式,这样既能处理构造函数体,也能处理构造函数的初始化过程。但是注意,在初始化构造函数的参数时也可能发生异常,这样的异常不属于函数try语句块的一部分,所以无法处理。
14.在C++11新标准中,我们可以通过提供noexcept说明指定某个函数不会抛出异常。紧跟在函数的参数列表后面,要跟在const及引用限定符之后,在final,override或虚函数的=0之前。
15.编译器并不会在编译时检查noexcept说明,如果一个函数在说明了noexcept的同时又含有throw语句或者调用了可能抛出异常的其他函数,编译器将顺利编译过。一旦一个noexcept函数抛出了异常,程序就会调用terminate以确保遵守不在运行时抛出异常的承诺,上述过程对是否执行栈展开未作约定,因此noexcept可以用在两种情况之下:
- 我们确认函数不会抛出异常
- 我们根本不知道该如何处理异常
16.noexcept说明符的实参常常与noexcept运算符混合使用,它是一个一元运算符,返回值是一个bool类型的右值常量表达式,用于表示给定的表达式是否会抛出异常。和sizeof类似,noexcept也不会求其运算对象的值。
void test() noexcept(true); // 不会抛出异常 void test1() { test(); } void test2() noexcept(test1); // test1调用的所有函数都做了不抛出说明并且test1本身不含有throw语句时,表达式为true
17.如果我们为某个指针做了不抛出异常的声明,则该指针将只能指向不抛出异常的函数。如果一个虚函数承诺了它不会抛出异常,则后续派生出来的虚函数也必须做出同意的承诺。
void test() noexcept(true); // 不会抛出异常 void test1() noexcept(false); // 可能会抛出异常 void(*pf)(void) noexcept = test; // 正确 void(*pf1)(void) noexcept = test1; // 错误 class base { public: virtual void add(int) noexcept {}; }; class sub : public base { public: void add(int) {} //错误 };
18.标准库异常类的继承体系如下,我们可以直接使用也可以继承它们定义自己的异常类型。