目录1、在main() 函数中抛出异常会发生什么 由上节中的 异常抛出(throw exception)的逻辑分析 可知,异常抛出后,会顺着函数调用栈向上传播,在这期间,若异常被捕获,则程序正常运行;若异常在 main() 函数中依然没有被捕获,也就是说在 main() 函数中抛出异常会发生什么呢?(程序崩溃,但因编译器的不同,结果也会略有差异) 在 main() 函数中抛出异常
将上述代码在不同的编译器上运行,结果也会不同; 在 g++下运行,结果如下: main() begin... Test() terminate called after throwing an instance of 'int' Aborted (core dumped)
在 vs2013下运行,结果如下: main() begin... Test() 弹出异常调试对话框
从运行结果来看,在 main() 中抛出异常后会调用一个全局的 terminate() 结束函数,在 terminal() 函数中不同编译器处理的方式有所不同。 c++ 支持自定义结束函数,通过调用 set_terminate() 函数来设置自定义的结束函数,此时系统默认的 terminal() 函数就会失效; (1)自定义结束函数的特点:与默认的 terminal() 结束函数 原型一样,无参无返回值; 关于使用 自定义结束函数的注意事项: 1)不能在该函数中再次抛出异常,这是最后一次处理异常的机会了; 2)必须以某种方式结束当前程序,如 exit(1)、abort(); exit():结束当前的程序,并且可以确保所有的全局对象和静态局部对象全部都正常析构; abort():异常终止一个程序,并且异常终止的时候不会调用任何对象的析构函数; (2)set_terminate() 函数的特点:1)参数类型为函数指针 void(*)();2)返回值为自定义的 terminate() 函数入口地址; 自定义结束函数测试案例
2、在析构函数中抛出异常会发生什么 一般而言,在析构函数中销毁所使用的资源,若在资源销毁的过程中抛出异常,那么会导致所使用的资源无法完全销毁;若对这一解释深入挖掘,那么会发生什么呢? 试想程序在 main() 函数中抛出了异常,然而该异常并没有被捕获,那么该异常就会触发系统默认的结束函数 terminal();因为不同编译器对 terminal() 函数的内部实现有所差异, (1)若 terminal() 函数是以 exit(1) 这种方式结束程序的话,那么就会有可能调用到析构函数,而此时的析构函数中又抛出了一个异常,就会导致二次调用 terminal() 函数,后果不堪设想(类似堆空间的二次释放),但是,强大的 windows、Linux系统会帮我们解决这个问题,不过在一些嵌入式的操作系统中可能就会产生问题。 (2)若 terminal() 函数是以 abort() 这种方式结束程序的话,就不会发生(1)中的情况,这就是 g++ 编译器为什么会这么做的原因了。 注:terminal() 结束函数是最后处理异常的一个函数,所以该函数中不可以再次抛出异常,而(1)中就是违反了这条规则; 若在 terminal() 结束函数中抛出异常,就会导致二次调用 terminal() 结束函数。 结论:在析构函数中抛出异常时,若 terminate() 函数中以 exit() 这种方式结束程序的话会很危险,有可能二次调用 terminate() 函数,甚至死循环。 在析构函数中抛出异常案例测试
将上述代码在不同的编译器上运行,结果也会不同; 在 g++下运行,结果如下: main() begin... Test() void mterminate() // 在 main() 函数中第一次抛出异常,调用 自定义结束函数 mterminate() ~Test() // exit(1) 程序结束时,调用了析构函数,在析构函数中再次抛出了异常,会调用 abort() 函数 Aborted (core dumped) // 注:一些旧版本的编译器可能会调用 自定义结束函数 mterminate(),此行显示 void mterminate()
在 vs2013下运行,结果如下: main() begin... Test() void mterminate() ~Test() // exit(1) 程序结束时,调用了析构函数,在析构函数中再次抛出了异常,会 弹出异常调试对话框 弹出异常调试对话框 // 注:一些旧版本的编译器可能会调用 自定义结束函数 mterminate(),此行显示 void mterminate()
结论:新版本的编译器对 析构函数中抛出异常这种行为 做了优化,直接让程序异常终止。 3、函数的异常规格说明 如何判断某个函数是否会抛出异常,或许有很多办法,如查看函数的实现(可惜第三方库不提供函数实现)、查看技术文档(可能查看的文档与当前所使用的函数版本不一致),但刚才列举的这些方法都会存在缺陷。其实有一种更为简单高效的方法,就是直接通过异常声明来判断这个函数是否会抛出异常,简称为函数的异常规格说明。 异常声明作为函数声明的修饰符,写在参数列表的后面; [url=][/url]
1 /* 可能抛出任何异常 */2 void func1();3 4 /* 只能抛出的异常类型:char 和 int */5 void func2() throw(char, int);6 7 /* 不抛出任何异常 */8 void func3() throw();[url=][/url]
异常规格说明的意义: (1)提示函数调用者必须做好异常处理的准备;(如果想知道调用的函数会抛出哪些类型的异常时,只用打开头文件看看这个函数是怎么声明的就可以了;) (2)提示函数的维护者不要抛出其它异常; (3)异常规格说明是函数接口的一部分;(用于说明这个函数如何正确的使用;) 异常规格之外的异常测试案例
将上述代码在不同的编译器上运行,结果也会不同; 在 g++下运行,结果如下: func() terminate called after throwing an instance of 'char' Aborted (core dumped)
在 vs2013下运行,结果如下: func() catch(char) // 竟然捕获了该异常,说明不受异常规格说明限制
通过对上述代码结果的再次研究,我们发现在 g++中,当异常不在函数异常规格说明中,就会调用一个 全局函数 unexpected(),在该函数中再调用默认的全局结束函数 terminate(); 但在 vs2013中,异常并不会受限于函数异常规格说明的限制。 结论:g++ 编译器遵循了c++规范,然而 vs2013 编译器并不受限于这个约束。 提示:不同编译器对函数异常规格说明的处理方式有所不同,所以在进行项目开发时,有必要测试当前所使用的编译器。 c++ 中支持自定义异常函数;通过调用 set_unexpected() 函数来设置自定义异常函数,此时系统默认的 全局函数 unexpected() 就会失效; (1)自定义异常函数的特点:与默认的 全局函数 unexpected() 原型一样,无参无返回值; (2)关于使用 自定义异常函数的注意事项: 可以在函数中抛出异常(当异常符合触发函数的异常规格说明时,恢复程序执行;否则,调用全局 terminate() 函数结束程序); (3)set_unexpected() 函数的特点:1)参数类型为函数指针 void(*)();2)返回值为自定义的 unexpected() 函数入口地址; 自定义 unexpected() 函数的测试案例
将上述代码在不同的编译器上运行,结果也会不同; 在 g++下运行,结果如下: func() void m_unexpected() catch(int) // 由于自定义异常函数 m_unexpected() 中抛出的异常 throw 1 符合函数异常规格说明,所以该异常被捕获
在 vs2013下运行,结果如下: func() catch(char) // vs2013 没有遵循c++规范,不受异常规格说明的限制,直接捕获函数异常规格说明中 throw ‘c’这个异常
结论:(g++)unexpected() 函数是正确处理异常的最后机会,如果没有抓住,terminate() 函数会被调用,当前程序以异常告终; (vs2013)没有函数异常规格说明的限制,所有的函数都可以抛出任意异常。 4、动态内存申请结果的分析 在 c 语言中,使用 malloc 函数进行动态内存申请时,若成功,则返回对应的内存首地址;若失败,则返回 NULL 值。 在 c++规范中,通过重载 new、new[] 操作符去动态申请足够大的内存空间时, (1)若成功,则在获取的空间中调用构造函数创建对象,并返回对象地址; (2)若失败(内存空间不足),根据编译器的不同,结果也会不同; 1)返回 NULL 值;(早期编译器的行为,不属于 c++ 规范) 2)抛出 std::bad_alloc 异常;(后期的编译器会抛出异常,一些早期的编译器依然返回 NULL 值) 注:不同编译器 对如何抛出异常 也是不确定的,c++ 规范是在 new_handler() 函数中抛出 std::bad_alloc 异常,而 new_handler() 函数是在内存申请失败时自动调用的。 当内存空间不足时,会调用全局的 new_hander() 函数,调用该函数的意义就是让我们有机会整理出足够的内存空间;所以,我们可以自定义 new_hander() 函数,并通过全局函数 set_new_hander() 去设置自定义 new_hander() 函数。(通过实验证明, 有些编译器没有定义全局的 new_hander() 函数,比如 vs2013、g++ ,见案例1 ) 特别注意:set_new_hander() 的返回值是默认的全局 new_hander() 函数的入口地址。 而 set_terminate() 函数的返回值是自定义 terminate() 函数的入口地址; set_unexpected() 函数的返回值是自定义 unexpected() 函数的入口地址。 案例1:证明 编译器是否定义了全局 new_handler() 函数
将上述代码在不同的编译器上运行,结果也会不同; 在 vs2013 和 g++下运行,结果如下: func = 0 // => vs2013 and g++ 中没有定义 全局 new_handler() 函数
在 BCC下运行,结果如下: func = 00401468 catch(const bad_alloc&) // 在 BCC 中定义了全局 new_handler() 函数,并在该函数中抛出了 std::bad_alloc 异常
案例2:不同编译器在内存申请失败时的表现
将上述代码在不同的编译器上运行,结果也会不同; 在 g++下运行,结果如下: operator new: 4 Test() // 由于堆空间申请失败,返回 NULL 值,接着又在这片失败的空间上创建对象,当执行到 m_value = 0;时(相当于在 非法地址上赋值),编译器报 段错误 Segmentation fault (core dumped)
在 vs2013下运行,结果如下: operator new: 4 pt = 00000000 operator new[]: 24 pt = 00000000
在 BCC下运行,结果如下: operator new: 4 pt = 00000000 operator new[]: 24 pt = 00000000 operator delete[]: 00000000
总结:在 g++ 编译器中,内存空间申请失败,也会继续调用构造函数创建对象,这样会产生 段错误;在 vs2013、BCC 编译器中,内存空间申请失败,直接返回NULL。 为了让不同编译器在内存申请时的行为统一,所以必须要重载 new、delete 或者 new[]、delete[] 操作符,当内存申请失败时,直接返回 NULL 值,而不是抛出 std::bad_alloc 异常,这就必须通过 throw() 修饰 内存申请函数。 案例3:(优化)不同编译器在内存申请失败时的表现
通过测试,g++、vs2013、BCC 3款编译器的运行结果一样,输出结果如下: operator new: 4 pt = 00000000 operator new[]: 24 pt = 00000000 5、关于 new 关键字的新用法(1)nothrow 关键字 nothrow 关键字的使用
将上述代码在不同的编译器上运行,结果也会不同; 在 g++、BCC下运行,结果如下: 0 // 使用了 nothrow 关键字,在动态内存申请失败时,直接返回 NULL
--------------------
catch(const bad_alloc&) // 没有 nothrow 关键字,动态内存申请失败时,抛出 std::bad_alloc 异常
在 vs2013下编译失败: 原因是 内存申请太大,即数组的总大小不得超过 0x7fffffff 字节;
结论:nothrow 关键字的作用:无论动态内存申请结果是什么,都不要抛出异常,然而不同编译器之间也会有差异。 (2)通过 new 在指定的地址上创建对象 通过 new 在指定的地址上创制对象
在 g++、vs2013、BCC下运行,结果如下: 1::2 3::4
动态内存申请的结论: (1)不同的编译器在动态内存分配上的实现细节不同; (2)编译器可能重定义 new 的实现,并在实现中抛出 bad_alloc 异常;(vs2013、g++) (3)编译器的默认实现中,可能没有设置全局的 new_handler() 函数;(vs2013、g++) (4)对于移植性要求高的代码,需要考虑 new 的具体细节; 所以在 vs中,当动态内存申请失败时,会抛出 std::bad_alloc异常,而不会返回 NULL 值; 源码分析 new.cpp
源码分析 new2.cpp
|