异常处理、动态内存申请在不同编译器之间...

本文探讨了在main函数中和析构函数中抛出异常的后果,介绍了函数的异常规格说明及其实际测试案例。同时,分析了不同编译器在动态内存申请失败时的行为,并详细讲解了new关键字的使用,包括nothrow关键字和在指定地址创建对象。源码分析部分提供了深入理解的依据。
摘要由CSDN通过智能技术生成

在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 的具体细节;
  我们可以进一步验证上述结论,就以 vs2013 举例,在编译器的安装包找到 new.cpp、new2.cpp 这两个文件(文 件路径:C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\crt\src),分析其源码发现,在内存申请失败时,会调用 _callnewh(cb) 函数,该函数可以通过如下方式查看:https://docs.microsoft.com/zh-cn/cpp/c-runtime-library/reference/callnewh?view=vs-2015
 
 
  所以在 vs中,当动态内存申请失败时,会抛出 std::bad_alloc异常,而不会返回 NULL 值;

 源码分析 new.cpp
 源码分析 new2.cpp

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值