异常处理、动态内存申请在不同编译器之间的表现差异
续上节内容 c++中的异常处理 ...
目录
1、在main() 函数中抛出异常会发生什么
由上节中的 异常抛出(throw exception)的逻辑分析 可知,异常抛出后,会顺着函数调用栈向上传播,在这期间,若异常被捕获,则程序正常运行;若异常在 main() 函数中依然没有被捕获,也就是说在 main() 函数中抛出异常会发生什么呢?(程序崩溃,但因编译器的不同,结果也会略有差异)
1 #include <iostream> 2 #include <cstdlib> 3 4 using namespace std; 5 6 class Test 7 { 8 public: 9 Test() 10 { 11 cout << "Test()" << endl; 12 } 13 14 ~Test() 15 { 16 cout << "~Test()" << endl; 17 } 18 }; 19 20 int main() 21 { 22 cout << "main() begin..." << endl; 23 24 static Test t; 25 26 throw 1; 27 28 cout << "main() end..." << endl; 29 30 return 0; 31 }
将上述代码在不同的编译器上运行,结果也会不同;
在 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() 函数入口地址;
1 #include <iostream> 2 #include <cstdlib> 3 4 using namespace std; 5 6 class Test 7 { 8 public: 9 Test() 10 { 11 cout << "Test()" << endl; 12 } 13 14 ~Test() 15 { 16 cout << "~Test()" << endl; 17 } 18 }; 19 20 void mterminate() 21 { 22 cout << "void mterminate()" << endl; 23 abort(); // 异常终止一个程序,不会析构任何对象 24 //exit(1); // 结束当前程序,但会析构所有的全局和静态局部对象 25 } 26 27 int main() 28 { 29 terminate_handler f = set_terminate(mterminate); 30 31 cout << "terminate() 函数的入口地址 = " << f << "::" << mterminate << endl; 32 33 cout << "main() begin..." << endl; 34 35 static Test t; 36 37 throw 1; 38 39 cout << "main() end..." << endl; 40 41 return 0; 42 } 43 /** 44 * 以 exit(1) 结束程序时的运行结果: 45 * terminate() 函数的入口地址 = 1::1 46 * main() begin... 47 * Test() 48 * void mterminate() 49 * ~Test() 50 */ 51 52 /** 53 * 以 abort() 结束程序时的运行结果: 54 * terminate() 函数的入口地址 = 1::1,《为什么全局函数的地址都是 1 ?》 55 * main() begin... 56 * Test() 57 * void mterminate() 58 * Aborted (core dumped) 59 */
2、在析构函数中抛出异常会发生什么
一般而言,在析构函数中销毁所使用的资源,若在资源销毁的过程中抛出异常,那么会导致所使用的资源无法完全销毁;若对这一解释深入挖掘,那么会发生什么呢?
试想程序在 main() 函数中抛出了异常,然而该异常并没有被捕获,那么该异常就会触发系统默认的结束函数 terminal();因为不同编译器对 terminal() 函数的内部实现有所差异,
(1)若 terminal() 函数是以 exit(1) 这种方式结束程序的话,那么就会有可能调用到析构函数,而此时的析构函数中又抛出了一个异常,就会导致二次调用 terminal() 函数,后果不堪设想(类似堆空间的二次释放),但是,强大的 windows、Linux系统会帮我们解决这个问题,不过在一些嵌入式的操作系统中可能就会产生问题。
(2)若 terminal() 函数是以 abort() 这种方式结束程序的话,就不会发生(1)中的情况,这就是 g++ 编译器为什么会这么做的原因了。
注:terminal() 结束函数是最后处理异常的一个函数,所以该函数中不可以再次抛出异常,而(1)中就是违反了这条规则;
若在 terminal() 结束函数中抛出异常,就会导致二次调用 terminal() 结束函数。
结论:在析构函数中抛出异常时,若 terminate() 函数中以 exit() 这种方式结束程序的话会很危险,有可能二次调用 terminate() 函数,甚至死循环。
1 #include <iostream> 2 #include <cstdlib> 3 4 using namespace std; 5 6 class Test 7 { 8 public: 9 Test() 10 { 11 cout << "Test()" << endl; 12 } 13 14 ~Test() 15 { 16 cout << "~Test()" << endl; 17 18 throw 1; // 代码分析:会二次调用 mterminate() 19 } 20 }; 21 22 void mterminate() 23 { 24 cout << "void mterminate()" << endl; 25 exit(1); // 结束当前程序,但会析构所有的全局和静态局部对象 26 } 27 28 int main() 29 { 30 set_terminate(mterminate); 31 32 cout << "main() begin..." << endl; 33 34 static Test t; 35 36 throw 1; 37 38 cout << "main() end..." << endl; 39 40 return 0; 41 }
将上述代码在不同的编译器上运行,结果也会不同;
在 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、函数的异常规格说明
如何判断某个函数是否会抛出异常,或许有很多办法,如查看函数的实现(可惜第三方库不提供函数实现)、查看技术文档(可能查看的文档与当前所使用的函数版本不一致),但刚才列举的这些方法都会存在缺陷。其实有一种更为简单高效的方法,就是直接通过异常声明来判断这个函数是否会抛出异常,简称为函数的异常规格说明。
异常声明作为函数声明的修饰符,写在参数列表的后面;
1 /* 可能抛出任何异常 */ 2 void func1(); 3 4 /* 只能抛出的异常类型:char 和 int */ 5 void func2() throw(char, int); 6 7 /* 不抛出任何异常 */ 8 void func3() throw();
异常规格说明的意义:
(1)提示函数调用者必须做好异常处理的准备;(如果想知道调用的函数会抛出哪些类型的异常时,只用打开头文件看看这个函数是怎么声明的就可以了;)
(2)提示函数的维护者不要抛出其它异常;
(3)异常规格说明是函数接口的一部分;(用于说明这个函数如何正确的使用;)
1 #include <iostream> 2 3 using namespace std; 4 5 void func() throw(int) 6 { 7 cout << "func()" << endl; 8 9 throw 'c'; 10 } 11 12 int main() 13 { 14 try 15 { 16 func(); 17 } 18 catch(int) 19 { 20 cout << "catch(int)" << endl; 21 } 22 catch(char) 23 { 24 cout << "catch(char)" << endl; 25 } 26 27 return 0; 28 }
将上述代码在不同的编译器上运行,结果也会不同;
在 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() 函数入口地址;
1 #include <iostream> 2 #include <cstdlib> 3 4 using namespace std; 5 6 void m_unexpected() 7 { 8 cout << "void m_unexpected()" << endl; 9 10 throw 1; // 2 这个异常符合异常规格说明,所以可以被捕获 11 // terminate(); // 若这么写,与上个程序的运行结果相同 12 } 13 14 void func() throw(int) 15 { 16 cout << "func()" << endl; 17 18 throw 'c'; // 1 由于不符合异常规格说明,此时会调用 m_unexpected() 函数 19 } 20 21 int main() 22 { 23 set_unexpected(m_unexpected); 24 25 try 26 { 27 func(); 28 } 29 catch(int) 30 { 31 cout << "catch(int)" << endl; 32 } 33 catch(char) 34 { 35 cout << "catch(char)" << endl; 36 } 37 38 return 0; 39 }
将上述代码在不同的编译器上运行,结果也会不同;
在 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 #include <iostream> 2 3 using namespace std; 4 5 void my_new_handler() 6 { 7 cout << "void my_new_handler()" << endl; 8 } 9 10 int main(int argc, char *argv[]) 11 { 12 // 若编译器有全局 new_handler() 函数,则 func != NULL,否则,func == NULL; 13 new_handler func = set_new_handler(my_new_handler); 14 15 try 16 { 17 cout << "func = " << func << endl; 18 19 if( func ) 20 { 21 func(); 22 } 23 } 24 catch(const bad_alloc&) 25 { 26 cout << "catch(const bad_alloc&)" << endl; 27 } 28 29 return 0; 30 }
将上述代码在不同的编译器上运行,结果也会不同;
在 vs2013 和 g++下运行,结果如下:
func = 0 // => vs2013 and g++ 中没有定义 全局 new_handler() 函数
在 BCC下运行,结果如下:
func = 00401468
catch(const bad_alloc&) // 在 BCC 中定义了全局 new_handler() 函数,并在该函数中抛出了 std::bad_alloc 异常
1 #include <iostream> 2 #include <new> 3 #include <cstdlib> 4 #include <exception> 5 6 using namespace std; 7 8 class Test 9 { 10 int m_value; 11 public: 12 Test() 13 { 14 cout << "Test()" << endl; 15 16 m_value = 0; 17 } 18 19 ~Test() 20 { 21 cout << "~Test()" << endl; 22 } 23 24 void* operator new (size_t size) 25 { 26 cout << "operator new: " << size << endl; 27 28 return NULL; 29 } 30 31 void operator delete (void* p) 32 { 33 cout << "operator delete: " << p << endl; 34 35 free(p); 36 } 37 38 void* operator new[] (size_t size) 39 { 40 cout << "operator new[]: " << size << endl; 41 42 return NULL; 43 } 44 45 void operator delete[] (void* p) 46 { 47 cout << "operator delete[]: " << p << endl; 48 49 free(p); 50 } 51 }; 52 53 int main(int argc, char *argv[]) 54 { 55 Test* pt = new Test(); 56 57 cout << "pt = " << pt << endl; 58 59 delete pt; 60 61 pt = new Test[5]; 62 63 cout << "pt = " << pt << endl; 64 65 delete[] pt; 66 67 return 0; 68 }
将上述代码在不同的编译器上运行,结果也会不同;
在 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() 修饰 内存申请函数。
1 #include <iostream> 2 #include <new> 3 #include <cstdlib> 4 #include <exception> 5 6 using namespace std; 7 8 class Test 9 { 10 int m_value; 11 public: 12 Test() 13 { 14 cout << "Test()" << endl; 15 16 m_value = 0; 17 } 18 19 ~Test() 20 { 21 cout << "~Test()" << endl; 22 } 23 24 void* operator new (size_t size) throw() 25 { 26 cout << "operator new: " << size << endl; 27 28 return NULL; 29 } 30 31 void operator delete (void* p) 32 { 33 cout << "operator delete: " << p << endl; 34 35 free(p); 36 } 37 38 void* operator new[] (size_t size) throw() 39 { 40 cout << "operator new[]: " << size << endl; 41 42 return NULL; 43 } 44 45 void operator delete[] (void* p) 46 { 47 cout << "operator delete[]: " << p << endl; 48 49 free(p); 50 } 51 }; 52 53 int main(int argc, char *argv[]) 54 { 55 Test* pt = new Test(); 56 57 cout << "pt = " << pt << endl; 58 59 delete pt; 60 61 pt = new Test[5]; 62 63 cout << "pt = " << pt << endl; 64 65 delete[] pt; 66 67 return 0; 68 }
通过测试,g++、vs2013、BCC 3款编译器的运行结果一样,输出结果如下:
operator new: 4
pt = 00000000
operator new[]: 24
pt = 00000000
5、关于 new 关键字的新用法
(1)nothrow 关键字
1 #include <iostream> 2 #include <exception> 3 4 using namespace std; 5 6 void func1() 7 { 8 try 9 { 10 int* p = new(nothrow) int[-1]; 11 12 cout << p << endl; 13 14 delete[] p; 15 } 16 catch(const bad_alloc&) 17 { 18 cout << "catch(const bad_alloc&)" << endl; 19 } 20 21 cout << "--------------------" << endl; 22 23 try 24 { 25 int* p = new int[-1]; 26 27 cout << p << endl; 28 29 delete[] p; 30 } 31 catch(const bad_alloc&) 32 { 33 cout << "catch(const bad_alloc&)" << endl; 34 } 35 } 36 37 int main(int argc, char *argv[]) 38 { 39 func1(); 40 41 return 0; 42 }
将上述代码在不同的编译器上运行,结果也会不同;
在 g++、BCC下运行,结果如下:
0 // 使用了 nothrow 关键字,在动态内存申请失败时,直接返回 NULL
--------------------
catch(const bad_alloc&) // 没有 nothrow 关键字,动态内存申请失败时,抛出 std::bad_alloc 异常
在 vs2013下编译失败:
原因是 内存申请太大,即数组的总大小不得超过 0x7fffffff 字节;
结论:nothrow 关键字的作用:无论动态内存申请结果是什么,都不会抛出异常,并且这种现象在不同编译器之间也会有差异。
(2)通过 new 在指定的地址上创建对象
1 #include <iostream> 2 3 using namespace std; 4 5 void func2() 6 { 7 int bb[2] = {0}; 8 9 struct ST 10 { 11 int x; 12 int y; 13 }; 14 15 // 通过 new 在指定的地址上创制对象 16 // 将动态内存ST 创建到栈空间上(int bb[2] = {0}),但要保证二者的内存模型相同,此处是 8 bytes 17 ST* pt = new(bb) ST(); 18 19 pt->x = 1; 20 pt->y = 2; 21 22 cout << bb[0] << "::" << bb[1] << endl; 23 24 bb[0] = 3; 25 bb[1] = 4; 26 27 cout << pt->x << "::" << pt->y << endl; 28 29 pt->~ST(); // 由于指定了创建对象的空间,必选显示的调用析构函数 30 } 31 32 int main(int argc, char *argv[]) 33 { 34 func2(); 35 36 return 0; 37 }
在 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 值;
1 #ifdef _SYSCRT 2 #include <cruntime.h> 3 #include <crtdbg.h> 4 #include <malloc.h> 5 #include <new.h> 6 #include <stdlib.h> 7 #include <winheap.h> 8 #include <rtcsup.h> 9 #include <internal.h> 10 11 // 两个版本的 new 实现方式,失败时都会抛出 bad_alloc 异常 12 void * operator new( size_t cb ) 13 { 14 void *res; 15 16 for (;;) { 17 18 // allocate memory block 19 res = _heap_alloc(cb); 20 21 // if successful allocation, return pointer to memory 22 23 if (res) 24 break; 25 26 // call installed new handler 27 if (!_callnewh(cb)) // 申请失败,则抛出 bad_alloc 异常 28 break; 29 30 // new handler was successful -- try to allocate again 31 } 32 33 RTCCALLBACK(_RTC_Allocate_hook, (res, cb, 0)); 34 35 return res; 36 } 37 #else /* _SYSCRT */ 38 39 #include <cstdlib> 40 #include <new> 41 42 _C_LIB_DECL 43 int __cdecl _callnewh(size_t size) _THROW1(_STD bad_alloc); 44 _END_C_LIB_DECL 45 46 void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc) 47 { // try to allocate size bytes 48 void *p; 49 while ((p = malloc(size)) == 0) 50 if (_callnewh(size) == 0) 51 { // report no memory 52 _THROW_NCEE(_XSTD bad_alloc, ); 53 } 54 55 return (p); 56 }
1 #include <cruntime.h> 2 #include <malloc.h> 3 #include <new.h> 4 #include <stdlib.h> 5 #include <winheap.h> 6 #include <rtcsup.h> 7 8 void *__CRTDECL operator new(size_t) /*_THROW1(std::bad_alloc)*/; 9 10 void * operator new[]( size_t cb ) 11 { 12 void *res = operator new(cb); 13 14 RTCCALLBACK(_RTC_Allocate_hook, (res, cb, 0)); 15 16 return res; 17 }