一. 回顾C++异常机制
(一)概述
1. 异常处理是C++的一项语言机制,用于在程序中处理异常事件(也被称为导常对象)。
2. 异常事件发生时,使用throw关键字抛出异常表达,抛出点称为异常出现点,由操作系统为程序设置当前异常对象。然后执行程序当前异常处理代码块。
3. 在包含异常出现点的最内层try块,依次匹配catch语句中的异常对象。若匹配成功,则执行catch块内的异常处理语句,然后接着执行try…catch…块之后的代码。
4.如果在当前try…catch…块内找不到匹配该异常对象的catch语句,则由更外层的try…catch…块来处理该异常。如果当前函数的所有try…catch…都不匹配该异常,则递归回退到调用栈的上一层去处理该异常。如果一直退出main()函数都不能处理该异常,则调用系统函数terminate()来终止程序。
(二)异常对象
1. 异常对象是一种特殊的对象。编译器依据异常抛出表达式构造异常对象(即异常对象总是被拷贝)。对象的类型是由表达式所表示对象的静态编译类型决定的。如Parent& rObj = Child; throw rObj;时会抛出Parent类型的异常对象。
2. 异常对象存放在内存特殊位置,该位置既不是栈也不是堆,在Windows中是放在线程信息TIB中。该对象由异常机制负责创建和释放!(g++和vc下存储区域处理略有差异)。
3. 异常对象不同于函数的局部对象,局部对象在函数调用结束后就被自动销毁,而异常对象将驻留在所有可能激活的catch语句都能访问到的内存空间中。当异常对象与catch语句成功匹配后,在该catch语句的结束处被自动析构。
4.在函数中返回局部变量的指针或引用几乎肯定会造成错误。同理,在throw语句中抛出局部变量的指针或引用也几乎是错误的。
【编程实验】捕获异常对象(按值、按引用和按指针)
#include <iostream> #include <string> using namespace std; class MyException { public: MyException() { cout << "MyException():" << this << endl; } MyException(const MyException&) { cout << "MyException(const MyException&):" << this << endl; } ~MyException() { cout << "~MyException():" << this << endl; } void what() { cout << "MyException: this = " << this << endl; } }; class MyChildExcept : public MyException { public: MyChildExcept() { cout << "MyChildExcept():" << this << endl; } MyChildExcept(const MyChildExcept&) { cout << "MyChildExcept(const MyChildExcept&):" << this << endl; } ~MyChildExcept() { cout << "~MyChildExcept():" << this << endl; } void what() { cout << "MyChildExcept: this = " << this << endl; } }; void func_local() { // throw 局部对象 MyException localEx; throw localEx; //尽管localEx是个局部对象,但这里会将其复制构造出一个异常对象,并存储在TIB中。而不是真正的将局部对象抛出去! } void func_temp() { //throw 临时对象 MyException(); //临时对象1 throw MyException(); //编译器会将这个临时对象直接存储在线程TIB中,成为异常对象(注意与临时对象1存储位置一般相距较远!) } void func_ptr() { //throw 指针 throw new MyException(); //注意:异常对象是复制的堆对象而来的指针(存在内存泄漏风险!!!) } void func_again() { MyChildExcept child; MyException& re = child; //注意抛出的是re的静态类型的异常对象,即MyException,而不是MyChildExcept; throw re; } int main() { cout << "----------------------------------catch by value------------------------------" << endl; //按值捕获 try { func_local(); //throw MyExecption() } catch (MyException e) { //复制异常对象,须额外进行一次拷贝! cout << "catch (MyException e)" << endl; e.what(); } cout << "--------------------------------catch by reference----------------------------" << endl; //按引用捕获 try { func_temp(); } catch (MyException& e) { //直接引用异常对象,无须拷贝 cout << "catch (MyException& e)" << endl; e.what(); } cout << "---------------------------------catch by pointer-----------------------------" << endl; //按指针捕获 try { func_ptr(); } catch (MyException* e) { //按指针捕获(可能造成内存泄漏) cout << "catch (MyException* e)" << endl; e->what(); delete e; //释放堆对象,防止内存泄漏 } cout << "------------------------------------throw again-------------------------------" << endl; //二次抛异常 try { try { func_again(); } catch (MyException& e) { e.what(); //注意以下两种方式不同 //1. 在throw后指定异常对象为e //throw e; //e会继续复制一份,并抛出复制的异常对象而e本身会被释放! //2.throw后不指定任何对象,只要是在catch中捕获的,一律抛出去。 throw; //此时,e本身再被抛出去。不会另外构造异常对象。 } } catch (MyException& e) { e.what(); } return 0; } /*输出结果(g++编译环境下的输出) ----------------------------------catch by value------------------------------ MyException():0x61fe7f //localEx对象 MyException(const MyException&):0x29978f8 //throw时将localEx复制给异常对象 ~MyException():0x61fe7f //释放localEx MyException(const MyException&):0x61feaf //将异常对象复制给catch中的e对象 catch (MyException e) MyException: this = 0x61feaf ~MyException():0x61feaf //释放catch中的e对象 ~MyException():0x29978f8 //释放异常对象 --------------------------------catch by reference---------------------------- MyException():0x61fe7f //创建临时对象1 ~MyException():0x61fe7f //释放临时对象1 MyException():0x29978f8 //throw MyException()时,会将这个临时对象直接创建在TIB中,与临时对象1不在同一地址段中 catch (MyException& e) //按引用传递,e直接引用异常对象,少了一次拷贝 MyException: this = 0x29978f8 ~MyException():0x29978f8 //释放异常对象 ---------------------------------catch by pointer----------------------------- MyException():0x299c638 //throw new Exception() ,先在堆中创建一个Exception对象,再将指针throw出去。 catch (MyException* e) MyException: this = 0x299c638 ~MyException():0x299c638 //手动调用delete后,释放堆中的Exception对象。 ------------------------------------throw again------------------------------- MyException():0x61fe7b MyChildExcept():0x61fe7b MyException(const MyException&):0x29978f8 //异常对象,这里是re的静态类型,即MyException,而不是MyChildExcept!!! ~MyChildExcept():0x61fe7b ~MyException():0x61fe7b MyException: this = 0x29978f8 //内层catch到的异常对象 MyException: this = 0x29978f8 //外层catch到的异常对象,注意与内层是同一对象 ~MyException():0x29978f8 //释放异常对象 */
二、异常规格
(一)C++0x与C++11异常规格声明方式的不同
1. void func() throw() { ... } // throw()声明该函数不会产生异常(C++0x)
2. void func() throw(int, double) { ... } //可能产生int或double类型异常(C++0x)
3. void func() noexcept { ... } // noexcept声明该函数不会产生异常(C++11)
4. void func() noexcept(常量表达式) { ... } //由表达式决定是否产生异常(C++11)
(二)noexcept和throw()异常规格声明的区别
1. 当函数后面加noexcept和throw()时均表示该函数不会产生异常。
2. 当使用throw()这种传统风格声明时,如果函数抛出了异常,异常处理机制会进行栈回溯,寻找(一个或多个)catch语句来匹配捕获。如果没有匹配的类型,会调用std::unexcepted函数,但是std::unexcepted本身也可能抛出异常,如果它抛出的异常对当前的异常规格是有效的,异常传递和栈回溯会像以前那样继续进行。这意味着如果使用throw()来声明,编译器几乎没有机会做优化,甚至会产生的代码会很臃肿、庞大。因为:
①栈必须保存在回退表中;
②所有对象的析构函数必须被正确调用(按照对象构建相反的顺序析构对象)。
③编译器可能引入新的传播栅栏(propagation barriers)、引入新的异常表入口,使得异常处理的代码变得庞大。
④内联函数的异常规格可能无效。
3. 当使用noexcept时,std::terminate()函数会被立即调用,而不是调用std::unexcepted()。因此栈不回溯,这为编译器的优化提供了空间。
4. 总之,如果知道函数绝对不会抛出任何异常,应该使用noexcept,而不是throw() 。
三、noexcept关键字
(一)noexcept异常规格语法
1. noexcept():声明函数不会抛出任何异常。(注意throw()声明不抛出动态异常时,会保证进行栈回溯,可能调用std::unexcepted)。
2. noexcept(true)、noexcept(false):前者与noexcept()等价,后者表示函数可能抛出异常。
3. noexcept(表达式):其中的表达式是可按语境转换为bool类型的常量表达式。若表达式求值为true,则声明函数为不抛出任何异常。若为false则表示函数可能抛出异常。
(二)noexcept在函数指针、虚函数中的使用规则
1. noexcept与函数指针:
(1)规则1:如果为函数指针显式声明了noexcept规格,则该指针只能指向带有noexcept的同种规格函数。
(2)规则2:如果未声明函数指针的异常规则(即隐式说明可能抛异常),则该指针可以指向任何函数(即带noexcept或不带noexcept函数)。
2. noexcept与虚函数:
(1)如果基类虚函数声明不抛异常,则子类也必须做出相同的承诺。即子类也须带上noexcept。
(2)如果基类虚函数声明可能抛异常,则子类可以抛异常,也可以不抛异常。
(三)注意事项
1. noexcept声明是函数接口的组成部分,这意味着调用方可能会对它有依赖。(但不能依靠noexcept来构成函数的重载)
2. 相对于不带noexcept声明的函数,带有noexcept声明的函数有更多机会得到优化。但大多数函数是异常中立的,即不具备noexcept性质。
3. 不抛异常的函数,允许调用潜在抛出异常的函数。异常如果没有被阻止传播,最终会调用std::terminate来终止程序。
4. noexcept对于移动操作、swap、内存释放函数和析构函数最有价值。默认地,operator delete、operator delete[]和析构函数都隐式具备了noexcept性质,但可以重新显式指定为可能抛出异常。
【编程实验】noexcept测试
#include <iostream> #include <type_traits> using namespace std; //以下两个函数不能构成重载 //void func(int) noexcept(true){} //void func(int) noexcept(false){} //1. 异常规格与函数指针 void func1(int) noexcept(true) {}; //不抛出异常 void func2(int) noexcept(false) {}; //可能抛出异常 void func3(int){} //2. noexcept与虚函数 class Parent { public: virtual void func1() noexcept {} //不抛异常 virtual void func2() noexcept(false) {} //可能抛异常 }; class Child : public Parent { public: //基类承诺不抛异常,则子类也必须承诺! //void func1() {}; //error, Parent::func1() 承诺不抛异常了 //基类声明为可能抛异常,则子类可以抛,也可以不抛异常 void func2() noexcept(true) {} //ok, 子类不抛异常 //void func2() {}; //ok,子类可以抛异常 }; //3. 有条件的noexcept //3.1 Swap:交互两个数组的元素。 //只要交互元素时不抛异常,但交换整个数组就不抛异常。因此Swap函数是否为noexcept取决于交互元素 template<typename T, size_t N> void Swap(T(&a)[N], T(&b)[N]) noexcept(noexcept(std::swap(*a, *b))) //*a、*b为首元素 { for (int i = 0; i < N; ++i) { std::swap(a[i], b[i]); } } template<typename T, size_t N> void printArray(T(&a)[N]) { for (int i = 0; i < N; ++i) { cout << a[i] << " "; } cout << endl; } //3.2 func //func_destructor函数抛不抛异常取决于T的析构函数。如果T析构会抛异常则func_destructor也会抛异常,反之不抛异常。 template<typename T> void func_destructor(const T&) noexcept(noexcept((std::declval<T>().~T()))) {} struct CA { ~CA(){ throw 1; } //注意析构函数默认为noexcept(true) }; struct CB { ~CB()noexcept(false){ throw 2; } }; struct CC { CB b; }; //3.3 pair template<class T1, class T2> class MyPair { T1 first; T2 second; public: //swap函数是否为noexcept,取决于交互first和second的过程是否为noexcept void swap(MyPair& p) noexcept(noexcept(std::swap(first, p.first)) && noexcept(std::swap(second, p.second))) { } }; //4. noexcept void Throw() { throw 1; } //可能抛异常:异常会被向外层传递出去 void NoBlockThrow() { Throw(); } //可能抛异常:异常会被向外层传递出去 void BlockThrow() noexcept { Throw(); } //不抛异常,但此函数实际会抛异常,异常会阻止传递,程序中止 int main() { //1. noexcept与函数指针 //1.1规则1: void(*pf1)(int) noexcept = func1; //正确 //void(*pf2)(int) noexcept = func2; //error;异常规格不同! //void(*pf3)(int) noexcept = func3; //error,带noexcept的指针,只能指向带同种noexcept规格的函数 //1.2 规则2: void(*pf4)(int) = func1; //or,pf3未带noexcept,可以指向任何函数 void(*pf5)(int) = func2; //ok void(*pf6)(int) = func3; //ok //2. noexcept与虚函数(见Child类) //3. 有条件的noexcept //3.1 交换数组 int a[] = { 1,2,3,4,5 }; int b[] = { 6,7,8,9,10 }; Swap(a, b); //Swap函数是否为noexcept,取决于std::swap(*a,*b)函数是否为noexcept printArray(a); printArray(b); //3.2 析构函数 try { CB temp; func_destructor(temp); //分别测试CA, CB, CC类。此处,由于CB析构函数为可能抛出异常,因此 //抛出会被继续抛出而被catch(...)语句捕获,程序不会被中止 } catch (...) { cout << "catch destructor exception" << endl; } //4. noexcept与异常传递测试 try { Throw(); //这里可增加测试NoBlockThrow、BlockThrow函数 }catch (...) { cout <<"catch exception..." << endl; } return 0; } /*输出结果: 6 7 8 9 10 1 2 3 4 5 catch destructor exception catch exception... */