当前课程知识点:基于Linux的C++ > 第十一讲 泛型编程 > 11.4 异常处理机制(二) > LinuxCPP1104
刚才那个例子
因为我们定义的是两个空异常类
它所能够提供的信息是有限的
我们可以为这样的两个异常类
提供更丰富的信息
在实际的编程中 所有的这些异常
我们都应该精心的设计对应的异常类
能够提供足够的完整的信息
这样在异常处理的时候
它的处理的方案和处理的方法
才能够更加灵活
对于我们的 EStackFull 这个类来说
我们提供一个值这样的一个私有字段
然后在引发这个异常的时候
我们能够提供这个值的信息
就是说你当时
在把哪一个数据压栈的时候出了异常
那么我们这个 _value 就保存那个数据
它可以提供更详尽的信息
那么 在 Push() 这个成员函数里边
当栈是满的时候
它就会 throw EStackFull( value )
它就用压栈的这个值
构造这个异常类的一个对象
然后把这个对象抛出
这一点在异常编程的时候是特别重要的
主程序当然也需要做一些修改
try 块还一样 我们尝试着压栈
catch 想捕获这个异常
我们捕获的是哪一个异常呢
当然是 EStackFull 类的一个引用
OK 我们就写 const EStackFull & e
我们就捕获这个异常
那个异常对象在 Push() 的过程中
不是被抛出来了吗
那么我们的 catch 就要捕获那个异常
Push() 所构造的那个异常对象
在 catch 这个子句里
将通过 e 这个引用来访问
所以我们就可以在 catch 这个子句里边
通过 e 来获取那个异常对象的特定的属性信息
通过 e 来获取那个异常对象的特定的属性信息
它到底是在压入哪一个数据的时候引发这个异常的
它到底是在压入哪一个数据的时候引发这个异常的
那么 e.GetValue() 一下子就能够获得那个值
这一点显然比刚才空类的设计要好得多
有一点是需要特别说明的
异常类本身可以派生和继承
你在 C++ 的类库架构下边写一个异常类
再写一个异常类
互相之间完全没关系
那种可能性其实是不多的
我们可以为所有的这些异常类本身
也构造一个完整的类的继承层次
正常情况下 我们的程序逻辑应该是
有一个顶层的抽象的异常类
然后后续的所有的异常类
都应该从这个异常类继承下来
事实上 C++ 标准库已经给我们
提供了这样的一个异常类
而你写的异常类应该都从 C++ 标准库的
那个异常类里边继承下来
可捕获的异常对象的型式
有一点是需要说明的
在 catch 那个子句里边
我们可以捕获任何一个普通型式 包括类
整型可以 浮点型可以
字符串可以 类也可以
但是你要注意
当你以普通型式的形式来捕获的时候
那么这些异常对象就需要拷贝
你也可以捕获对某型式对象的一个引用
这个时候是没有额外拷贝动作的
因为你传递的时候
它那个参数将是以引用的方式进行传递
是没有额外的拷贝动作的 这是第二个
第三个 你也可以捕获指向某型式对象的一个指针 这也可以
第三个 你也可以捕获指向某型式对象的一个指针 这也可以
但是你要记住 在这种情况下
它会要求对象动态构造
这样的话 那个指针在 catch 里才可以访问到
不管怎么样 你必须保证那个对象
在我们的 catch 子句里可以访问
所以如果是指向某型式对象的指针
那么这样一个对象在大部分情况下
都应该是动态构造的
在 catch 子句里
可以封装对这个特定异常的一些必要的处理代码
可以封装对这个特定异常的一些必要的处理代码
实际编写程序的时候
你的 try ... catch 块
那个 catch 块我们称它为 catch 子句
实际上可以有很多个
每一个 catch 子句都负责捕获一种、一类
或者全部异常
如果你按照这样的模式写
catch( int )、catch( const char * )
那么它将捕获这一种异常
catch( int ) 只能捕获整数异常
catch( const char * ) 只能捕获 const char *
这种型式的异常
它不能捕获其它的异常
如果你是按照这样的方式写
catch( const EStackFull & )
有没有“&”都一样
它就只能捕获一类异常
为什么是一类呢
是因为所有的 EStackFull 这一类的对象
它都可以捕获
另外 EStackFull 这个类的所有派生类
它们的异常也可以被捕获
也就是说 你可以通过基类
捕获派生类的异常
正是在这个角度来讲
我们说 catch( const EStackFull & )
像这样的异常捕获
它捕获的是一类异常而不是一种
第三个 你如果写 catch(...) 中间省略了
就表示所有类型的异常它都可以捕获
在执行的时候 所有的 catch 子句
是按照你定义的顺序去做的
所以 如果你的异常类是有继承层次的
那么你必须把派生类的那个异常
对应的 catch 子句写在前边
把基类的那个 catch 子句写在后边
否则的话 你的派生类那个异常
是没有机会得到执行的
就像我们刚才讲的
因为你通过 catch(后面是一个类)
或者是一个类的一个引用
或者是一个指向这个类的指针的时候
你如果按照这样模式进行定义
不管它是那个类本身
还是那个类的引用
还是指向那个类的指针
这个参数的型式它都能够捕获
那个类和它所有的派生类异常
所以在这种情况下 千万要记得
如果有基类和派生类
应该把派生类写在前边 然后才能写基类的
应该把派生类写在前边 然后才能写基类的
可以在基本任务完成后
重新引发所处理的异常
你在处理异常的过程中
完成一个基本的任务
然后为了保证后续的代码
依然还能够处理这样的异常
或者你认为在这个程序处理完之后
这个异常有必要重新地被再次处理
我们说再引发
异常的再引发主要用于
在程序终止的时候写入我们的日志
在程序终止之前
我要完成这个日志的编写工作
把这个情况、这个信息记录在案
那么这个时候
我们就可以把它放在我们 catch 子句里
处理完了以后
那我们的程序流程就应该停止
然后可能还要做后续的异常
比如说完成我们的数据对象清除工作
那么在这种情况下面
这个异常就可以重新地再引发
去实施特定的清除任务
也有可能我们需要这一点
所以异常的再引发主要用于
在程序终止前写入我们的日志
和进行特殊的清除工作
异常再引发的方式特别简单
就是 throw 这个关键字
后边什么东西都不用带了
catch(...) 全部异常都处理
然后我们 throw
处理完以后我重新引发这个异常
那么更顶层的 catch 逻辑
就可以紧接着去处理它对应的异常
根据它的异常类具体的信息
来去做特定的处理
而我们这里不管三七二十一
先把它比如说
完成我们日志写入工作再说
写完了我再重新引发那个异常
这个处理方式是合乎逻辑的
在异常处理的过程中
我们的编译器会完成一个栈的展开
这一点在编程的时候
实际上就已经写在了我们的可执行代码里
异常引发的代码和异常处理代码
可能是位于不同函数的
这一点同学们一定要清楚
所以当异常发生的时候
它会沿着这个异常处理块的
嵌套顺序逆向地查找
去找它对应的 catch 子句
找着了那个 catch 子句
它就会执行 catch 子句里边的那段代码
来处理我们这个异常
异常如果处理完了
那么程序就会保持在这个 catch 子句
所在的那个函数的栈框架里边
不会返回引发异常的那个函数栈框架
特别注意这一点
为什么我们说在异常处理的时候
要进行栈展开呢
就是因为这个道理
因为你异常的引发代码和异常处理代码
不在一个函数里
当它向后回溯的时候
来找你对应的 catch 子句
来处理这个异常的时候
它不得不把引发那个异常的栈框架给去掉
如果它没找到的话
向上回溯一级函数调用
看主调函数里有没有对应的 catch 子句
如果没有 它还要继续向上回溯
再取消一次函数栈框架
一直到找到这个 catch 子句为止
然后完成这个异常处理的流程
这个流程一做完
它还能回到引发这个异常的
那个函数调用栈框架里边吗
它恢复不过去了
所以整个程序流程
这个时候就会停留在这个 catch 子句
栈框架里边
并且继续往下去做 它回不到原点了
这是非常重要的一个地方
函数栈框架消失了
编译器本身能够替我们
自动地将局部对象析构 它会清除
但是如果你动态分配了一段数据对象
你动态分配了一段内存
那么在catch子句里边
如果你没有调用 delete 操作符去析构它
那么动态分配的内存 它就会保留原样
也就是说 你动态分配的
这些目标数据对象就不会被析构
这一点在编写异常处理代码的时候
是需要特别注意的
这不就导致内存泄露了吗
所以一定要非常小心
所有的未处理异常
都由预定义的 std::terminate() 这个函数来处理
你可以使用 std::set_terminate() 这个函数
来设置 terminate() 函数的处理例程
我们看这段代码 这个示例里边
我们可以定义一个 term_func()
这是一个终止函数
那么你可以在 try 块里边
调用 set_terminate() 这个函数
将我们这个 term_func 传递给它
也就是说 terminate() 这个函数
在异常没有处理情况下面
调用我们的 term_func() 这个函数
去做特定的处理 处理完了
你也可以继续引发一个异常 这没关系
set_terminate() 将会将我们的异常处理例程
设置为我们的 term_func
然后你引发这个异常
引发这个异常之后
因为后面的 catch 子句只处理整数异常
它并不处理我们的字符串异常
所以这个异常将没有被获得处理
因为是未处理的
terminate() 这个函数就会处理这个异常
它怎么处理呢 当然是调用我们
term_func() 这个函数对它进行处理
这就是未处理异常最后一次处理的时机
就在这里 如果你没设
那么所有未处理的异常
都将交给操作系统来处理了
如果要描述函数是否引发异常
一个是 C++11 之前的逻辑
就是使用 throw 这个关键字
throw() 表示这样的一个函数是不引发异常
如果说这样一个函数
可能引发任意型式的异常
那么你就写 throw( ... )
如果这样的函数
只引发某种特定类型的异常
那么你就把那个特定类型
写在 throw 的小括号对里边
注意 有些编译器 当你写 throw( T ) 的时候
它自动地把它理解为throw( ... )
也就是说 它不关心小括号对里面的那个T
到底是什么型
在 C++11 规范里边
如果你要描述一个函数
不会引发任何异常
那么你就应该写新的关键字 noexcept
它等价于 noexcept( true )
如果这样一个函数确实引发一个异常
那么你就写 noexcept( false )
如果它可能引发异常
也就说可能引发 可能不引发
那么你就应该写 noexcept( noexcept( expr ) )
看上去很怪 expr 是什么呢
expr 就是可转换为 true 和 false 的
一个常数表达式
这两个 noexcept 不能省略了
第一个 noexcept 其实是关键字
来描述这个异常处理策略
也就是异常描述规范的
第二个 noexcept 其实是一个操作符
它是计算那个 expr
把它的结果变成 true 和 false
所以这两个 noexcept 都不能省略
在 C++11 下边
建议你使用 noexcept 来取代 throw
它认为 throw 已经过时了
你不应该用了 就是这个意思
不管你是用 throw 还是用 noexcept
来描述一个函数是否引发异常
这个东西 就叫异常描述规范
对于我们这个栈类
因为 Pop() 和 Push() 都有可能导致异常
所以我们在编写成员函数原型的时候
就应该描述清楚
这两个函数是否引发异常
引发的是什么类型的异常
应该写在这个函数的声明处
这个就叫异常描述规范
你看我是这些写的
int Pop() throw( EStackEmpty )
void Push() throw( EStackFull )
就表示这两个函数
将会引发这两种类型的异常
如果你是在 C++11 下边写了
你当然你应该写 noexcept( false )
不是不引发异常 对吧
那就是会引发异常的
-1.1 提纲
-1.2 程序设计的基本概念
-1.3 简单C/C++程序介绍
-1.4 程序设计的基本流程
-1.5 基本语法元素
-1.6 程序设计风格
-1.7 编程实践
-第一讲 C/C++基本语法元素--编程实践提交入口
-2.1 提纲
-2.2 结构化程序设计基础
-2.3 布尔数据
-2.4 分支结构
-2.5 break语句
-2.6 循环结构
-2.7 编程实践
-第二讲 程序控制结构--编程实践提交入口
-3.1 提纲
-3.2 函数声明、调用与定义
-3.3 函数调用栈框架
-3.4 编程实践
-第三讲 函数--编程实践提交入口
-4.1 提纲
-4.2 算法概念与特征
-4.3 算法描述
-4.4 算法设计与实现
-4.5 递归算法(一)
-4.6 递归算法(二)
-4.7 容错与计算复杂度
-4.8 编程实践
-第四讲 算法--编程实践提交入口
-5.1 提纲
-5.2 库与接口
-5.3 随机数库(一)
-5.4 随机数库(二)
-5.5 作用域与生存期
-5.6 典型软件开发流程(一)
-5.7 典型软件开发流程(二)
-5.8 编程实践
-第五讲 程序组织与开发方法--编程实践提交入口
-6.1 提纲
-6.2 字符
-6.3 数组(一)
-6.4 数组(二)
-6.5 结构体
-6.6 编程实践
-第六讲 复合数据类型--编程实践提交入口
-7.1 提纲
-7.2 指针基本概念
-7.3 指针与函数
-7.4 指针与复合数据类型(一)
-7.5 指针与复合数据类型(二)
-7.6 字符串
-7.7 动态存储管理(一)
-7.8 动态存储管理(二)
-7.9 引用
-7.10 编程实践
-第七讲 指针与引用--编程实践提交入口
-8.1 提纲
-8.2 数据抽象(一)
-8.3 数据抽象(二)
-8.4 链表(一)
-8.5 链表(二)
-8.6 链表(三)
-8.7 链表(四)
-8.8 函数指针(一)
-8.9 函数指针(二)
-8.10 抽象链表(一)
-8.11 抽象链表(二)
-8.12 编程实践
-第八讲 链表与程序抽象--编程实践提交入口
-9.1 提纲
-9.2 程序抽象与面向对象
-9.3 类类型
-9.4 对象(一)
-9.5 对象(二)
-9.6 类与对象的成员(一)
-9.7 类与对象的成员(二)
-9.8 类与对象的成员(三)
-9.9 继承(一)
-9.10 继承(二)
-9.11 继承(三)
-9.12 多态(一)
-9.13 多态(二)
-9.14 编程实践
-第九讲 类与对象--编程实践提交入口
-10.1 提纲
-10.2 四则运算符重载(一)
-10.3 四则运算符重载(二)
-10.4 关系与下标操作符重载
-10.5 赋值操作符重载(一)
-10.6 赋值操作符重载(二)
-10.7 赋值操作符重载(三)
-10.8 赋值操作符重载(四)
-10.9 赋值操作符重载(五)
-10.10 流操作符重载(一)
-10.11 流操作符重载(二)
-10.12 流操作符重载(三)
-10.13 操作符重载总结
-10.14 编程实践
-第十讲 操作符重载--编程实践提交入口
-11.1 提纲
-11.2 泛型编程概览
-11.3 异常处理机制(一)
-11.4 异常处理机制(二)
-11.5 运行期型式信息(一)
-11.6 运行期型式信息(二)
-11.7 模板与型式参数化
-11.8 题外话:术语翻译
-11.9 泛型编程实践(一)
-11.10 泛型编程实践(二)
-11.11 泛型编程实践(三)
-11.12 泛型编程实践(四)
-11.13 泛型编程实践(五)
-11.14 泛型编程实践(六)
-11.15 泛型编程实践(七)
-11.16 泛型编程实践(八)
-11.17 泛型编程实践(九)
-11.18 泛型编程实践(十)
-11.19 编程实践
-第十一讲 泛型编程--编程实践提交入口
-12.1 提纲
-12.2 程序执行环境(一)
-12.3 程序执行环境(二)
-12.4 程序执行环境(三)
-12.5 程序执行环境(四)
-12.6 输入输出(一)
-12.7 输入输出(二)
-12.8 文件系统
-12.9 设备
-12.10 库(一)
-12.11 库(二)
-12.12 makefile文件(一)
-12.13 makefile文件(二)
-12.14 makefile文件(三)
-12.15 编程实践
-第十二讲 Linux系统编程基础--编程实践提交入口
-13.01 提纲
-13.02 进程基本概念
-13.03 信号
-13.04 进程管理(一)
-13.05 进程管理(二)
-13.06 进程管理(三)
-13.07 进程间通信(一)
-13.08 进程间通信(二)
-13.09 进程间通信(三)
-13.10 进程间通信(四)
-13.11 进程池
-13.12 编程实践
-第十三讲 进程编程--编程实践提交入口
-14.1 提纲
-14.2 线程基本概念
-14.3 线程管理(一)
-14.4 线程管理(二)
-14.5 线程管理(三)
-14.6 线程管理(四)
-14.7 线程同步机制(一)
-14.8 线程同步机制(二)
-14.9 C++11线程库(一)
-14.10 C++11线程库(二)
-14.11 C++11线程库(三)
-14.12 C++11线程库(四)
-14.13 C++11线程库(五)
-14.14 编程实践
-第十四讲 线程编程--编程实践提交入口
-15.1 提纲
-15.2 Internet网络协议
-15.3 套接字(一)
-15.4 套接字(二)
-15.5 编程实践
-第十五讲 网络编程--编程实践提交入口