外观
18.拾遗:令人迷惑的写法、技巧:自定义内存管理、异常处理深度解析、函数的异常规格说明、动态内存申请的结果
约 2602 字大约 9 分钟
自定义内存管理C++编程语言个人
2022-06-17
六十八、拾遗:令人迷惑的写法
1、令人迷惑的写法
- 下面的程序想要表达什么意思?

历史上的原因。。。
- 早期的C++直接复用class关键字来定义模板
- 但是泛型编程针对的不只是类类型
- class关键字的复用使得代码出现二义性
typename诞生的直接诱因
- 自定义类类型内部的嵌套类型
- 不同类中的同一个标识符可能导致二义性
- 编译器无法辨识标识符究竟是什么
2、编程实验:模板中的二义性

3、令人迷惑的写法
- typename的作用:
- 在模板定义中声明泛指类型
- 明确告诉编译器其后的标识符为类型
4、下面的程序想要表达什么意思?

5、令人迷惑的写法
try ... catch 用于分隔正常功能代码与异常处理代码
try ... catch 可以直接将函数实现分隔为2部分
函数声明和定义时可以直接指定可能抛出的异常类型
异常声明成为函数的一部分可以提高代码可读性
函数异常声明的注意事项
- 函数异常声明是一种与编译器之间的契约
- 函数声明异常后就只能抛出声明的异常
- 抛出其它异常将导致程序运行终止
- 可以直接通过异常声明定义无异常函数
6、编程实验:新的异常写法

7、小结
- class可以用来在模板中定义泛指类型(不推荐)
- typename是可以消除模板中的二义性
- try...catch可以将函数体分成2部分
- 异常声明能够提供程序的可读性
六十九、技巧:自定义内存管理
1、笔试题
统计对象中某个成员变量的访问次数
2、遗失的关键字
mutable是为了突破const函数的限制而设计的
mutable成员变量将永远处于可改变的状态
mutable在实际的项目开发中被严禁滥用
mutable的深入分析
- mutable成员变量破坏了只读对象的内部状态
- const成员函数保证只读对象的状态不变性
- mutable成员变量的出现无法保证状态不变性
3、编程实验:成员变量的访问统计

4、面试题
new关键字创建出来的对象位于什么地方?
5、被忽略的事实
new / delete的本质是C++预定义的操作符
C++对这两个操作符做了严格的行为定义
new :
- 获取足够大的内存空间(默认为堆空间)
- 在获取的空间中调用构造函数创建对象
delete:
- 调用析构函数销毁对象
- 归还对象所占用的空间(默认为堆空间)
在C++中能够重载new / delete操作符
- 全局重载(不推荐)
- 局部重载(针对具体类进行重载)
重载new / delete的意义在于改变动态对象创建时的内存分配方式
new / delete的重载方式

6、编程实验:静态存储区中创建动态对象

7、面试题
如何在指定的地址上创建C++对象?
8、设计思路
- 解决方案
- 在类中重载new / delete操作符
- 在new的操作符重载函数中返回指定的地址
- 在delete操作符重载中标记对应的地址可用
9、编程实验:自定义动态对象的存储空间

10、被忽略的事实
- new[] / delete[]与new / delete完全不同
- 动态对象数组创建通过new[]完成
- 动态对象数组的销毁通过delete[]完成
- new[] / delete[]能够被重载,进而改变内存管理方式
- new[] / delete的重载方式

- 注意事项
- new[]实际需要返回的内存空间可能比期望的要多
- 对象数组占用的内存中需要保存数组信息(数组长度)
- 数组信息用于确定构造函数和析构函数的调用次数
11、编程实验:动态数组的内存管理

12、小结
- new / delete的本质为操作符
- 可以通过全局函数重载new / delete(不推荐)
- 可以针对具体的类重载new / delete
- new[] / delete[] new / delete 完全不同
- new[] / delete[]也是可以被重载的操作符
- new[]返回的内存空间可能比期望的要多
七十、异常处理深度解析
1、异常处理深度解析
问题
- 如果在main函数中抛出异常会发生什么?
如果异常不处理,最后会传到哪里?

- 下面的代码输出什么?

2、编程实验:异常的最终处理?

3、异常处理深度解析
如果异常无法被处理,terminate()结束函数会被自动调用
默认情况下,terminate()调用库函数abort()终止程序
abort()函数使得程序执行异常而立即退出
C++支持替换默认的terminate()函数实现
terminate()函数的替换
自定义一个无返回值无参数的函数
- 不能抛出任何异常
- 必须以某种方式结束当前程序
调用set_terminate()设置自定义的结束函数
- 参数类型为void (*)()
- 返回值为默认的terminate()函数入口地址
4、编程实验:自定义结束函数

5、面试题
如果析构函数中抛出异常会发生什么情况?
6、编程实验:析构函数抛出异常

7、小结
- 如果异常没有被处理,最后terminate()结束整个程序
- terminate()是整个程序释放系统资源的最后机会
- 结束函数可以自定义,但不能继续抛出异常
- 析构函数中不能抛出异常,可能导致terminate()多次调用
七十一、函数的异常规格说明
1、函数的异常规格说明
- 问题
如何判断一个函数是否会抛出异常,以及抛出哪些异常?
- C++提供语法用于声明函数所抛出的异常
- 异常声明作为函数声明的修饰符,写在参数列表后面

- 异常规格说明的意义
- 提示函数调用者必须做好异常处理的准备
- 提示函数的维护者不要抛出其它异常
- 异常规格说明是函数接口的一部分
2、问题
如果抛出的异常不在声明列表中,会发生什么?
下面的代码输出什么?

3、编程实验:异常规格之外的异常

4、函数的异常规格说明
函数抛出的异常不在规格说明中,全局unexpected()被调用
默认的unexpected()函数会调用全局的terminate()函数
可以自定义函数替换默认的unexpected()函数实现
注意:不是所有的C++编译器都支持这个标准行为
unexpected()函数的替换
自定义一个无返回值无参数的函数
- 能够再次抛出异常
- 当异常符合触发函数的异常规格说明时,恢复程序执行
- 否则,调用全局terminate()函数结束程序
调用set unexpected ()设置自定义的异常函数
- 参数类型为void (*)()
- 返回值为默认的unexpected()函数入口地址
5、编程实验:自定义unexpected()函数

6、小结
C++中的函数可以声明异常规格说明
异常规格说明可以看作接口的一部分
函数抛出的异常不在规格说明中,unexpected()被调用
unexpected()中能够再次抛出异常
- 异常能够匹配,恢复程序的执行
- 否则,调用terminate()结束程序
七十二、动态内存申请的结果
1、动态内存申请的结果
问题
- 动态内存申请一定成功吗?
常见的动态内存分配代码

- 必须知道的事实!
- malloc函数申请失败时返回NULL值
- new关键字申请失败时(根据编译器的不同)
- 返回NULL值
- 抛出std::bad_alloc异常
2、问题
new 语句中的异常是怎么抛出来的?
new 关键字在C++规范中的标准行为
- 在堆空间申请足够大的内存
成功:
- 在获取的空间中调用构造函数创建对象
- 返回对象的地址
失败:
- 抛出std::bad_alloc异常
- 在堆空间申请足够大的内存
new关键字在C++规范中的标准行为
new在分配内存时
- 如果空间不足,会调用全局的new_handler()函数
- new_handler()函数中抛出std::bad_alloc异常
可以自定义new_handler()函数
- 处理默认的 new内存分配失败的情况
new_handler()的定义和使用

3、问题
- 如何跨编译器统一new的行为,提高代码移植性?
- 解决方案
全局范围(不推荐)
- 重新定义new / delete 的实现,不抛出任何异常
- 自定义new_handler()函数,不抛出任何异常
类层次范围
- 重载new / delete,不抛出任何异常
单次动态内存分配
- 使用nothrow参数,指明new不抛出异常
4、编程实验:动态内存申请

5、动态内存申请的结果
- 实验结论
- 不是所有的编译器都遵循C++的标准规范
- 编译器可能重定义new的实现,并在实现中抛出bad_alloc异常
- 编译器的默认实现中,可能没有设置全局的new_handler()函数
- 对于移植性要求较高的代码,需要考虑new的具体细节
- vs2010 new实现方式
