Solmyr 的小品文系列之七异常_第1页
Solmyr 的小品文系列之七异常_第2页
Solmyr 的小品文系列之七异常_第3页
Solmyr 的小品文系列之七异常_第4页
Solmyr 的小品文系列之七异常_第5页
已阅读5页,还剩1页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、 Solmyr 的小品文系列之七:异常大雨。乌云象铅块一样低低的压了下来,豆大的雨滴打的玻璃窗啪啪作响,难得一见的异常天气正在竭力表现它令人讨厌的一面。不过这一切似乎并没有影响到 Solmyr,他仍然以他习惯的舒适姿势半躺在宽大的椅子里,手里还托着一杯热腾腾的果汁,在他背后,zero 在键盘上敲打着什么。“唉,Solmyr ,标准库中的 stack 怎么会是这个样子?设计糟透了。”zero 停止了工作,转过身来面对 Solmyr ,看起来有些困惑。“胡乱批评被纳入神圣标准的成员是会遭天遣的。”Solmyr 低着头,以一种算命先生似的语调答道。不知道上天是否打算加强 Solmyr 的说

2、服力,恰在此时天空划过一道闪电,蓝白色的电光挣扎着努力向地面扑来,紧接着就是“喀喇”一声巨响 这个雷很近。一秒钟前还在想“这未免也太扯了”的 zero 表情一下子变得很古怪,良久才恢复正常。他标出了两行代码接着说到:“好、好吧,Solmyr,那请你解释一下为什么 stack 的界面是这个样子。”std:stack<int> si;int i = si.top();si.pop();“只要让 pop() 返回栈顶元素就可以把上面两行合成一行,而且更加直观,为什么要搞成现在这样?”目睹了 zero 表情变化的 Solmyr 强忍住放声大笑的冲动 老天知道他忍的有多辛苦 缓缓的把杯子放到

3、桌上,转过身来开始讲解这个问题:“原因在于异常。”“异常?”“对,很多代码在没有异常的时候工作的挺好,但是一旦出现异常就变得不可收拾,就像一间茅草屋,平时看起来没什么问题,一遇到今天这种天气 ”,Solmyr 指了指窗外,“ 立刻就会垮掉。考虑一下如果 pop() 返回栈顶元素需要怎样实现,假设栈内部用数组实现,且不考虑栈是否为空的问题。”“很简单啊。”,zero 打开了编辑器,写下:template <typename T>T stack<T>:pop()    . .    return datatop-;

4、 / 假设数据存储于数组 data 中,top 代表栈顶位置Solmyr 摇摇头:“这就是茅草屋。要知道 stack 是个模板类,它存放的元素 T 可能是用户定义的类。我来问你,如果类型 T 的拷贝构造函数抛出异常,会出现什么情况?”“嗯 按值返回,返回值是个临时对象,该临时对象以 datatop 拷贝构造 嗯,这样一来函数返回时可能抛出异常,客户此时无法取得该元素。”“还有呢?”“还有?”“提示,你的 top 怎么了?”“ 哎呀!糟了!top 此时已经减一,栈顶元素就此丢失了!这样的话 必须实现一个函数允许客户修改 ”,zero 说不下去了。他想了一会,摇摇头承认失败:“不行,这里

5、拷贝构造发生在函数返回之后,无论如何无法避免这种情况。只能在文档里写明:要求 T 的拷贝构造函数不抛出异常。” zero 停了一停,小心翼翼的问 Solmyr :“这个不算过分的要求吧?”Solmyr 的回答异常简短:“new”“哦对,new 在分配内存失败时会抛出 std:bad_alloc 算我没说。Solmyr ,我明白了,为了处理异常的情况,调整栈顶位置必须在所有数据拷贝完成之后,所以按值返回是不可接受的。”“正确。所以对于一个设计目标是最大限度可复用性的标准库成员而言,这是不可接受的。” Solmyr 顿了顿,继续说到:“而且异常带来的影响远不止此。我刚才说假设栈内部用数组实现,但如

6、果你充分考虑抛出异常的各种可能性,你就会发现用数组实现是糟糕的主意。”“ 这是为什么?在没有传值返回的情况下,我们总可以捕捉到发生的异常并加以处理啊?”,zero 谨慎的发问。Solmyr 赞许的看着 zero 。“发问之前先自行思考,习惯不错。”,Solmyr 心想,但是脸上一点也没表现出来:“没错,但捕捉到异常不代表你总能正确的处理它。考虑一下 stack 的赋值运算符,如果我们用数组来实现,那么在拷贝数据的时候肯定会有类似这样的一个循环:”/ 各变量的意义与上面相同template <typename T>stack<T>& stack<T>:

7、operator=(const stack<T>& rhs)    . .    for(int i=0; i<rhs.top; i+)        datai = rhs.datai;    . .“现在考虑类型 T 的赋值运算符可能抛出异常,该怎样修改上面的代码。” Solmyr 停了下来,再度捧起了杯子。“用 try 把 哦 ”,zero 似乎发现了问题所在,沉默良久,才接着说到:“这个循环可能在运行到

8、一半的时候抛出异常,这样会导致一部分数据已经成功赋值,另一部分却还是老的。除非我们用 catch(.) 捕捉所有异常,忽略之并继续赋值。”“但是这样 ”,Solmyr 有意识的引导 zero 继续深入思考。“ 但是这样,赋值运算符抛出的异常就被我们吃掉了,异常总是代表着某些不该发生的事情发生了,所以应该让客户接收到这个异常才对。” zero 皱着眉头,一字一顿,显得相当辛苦。“正确。stack 作为一个通用的标准库成员,在面对异常时必须做到两点。一、异常安全,也就是说异常不会导致它本身处于一种错误的状态或是导致数据丢失或是造成资源泄漏;二、异常透明,也就是说客户代码 这里指它存放的类型 T 的

9、实现 抛出的任何异常,不应该被吃掉或者被改变,应该透明的传递给客户。一望即知,上面的代码无可能同时做到这两点。”“是这样,我懂了,这大概就是标准库中的 stack 不用数组实现的主要原因了吧”,zero 露出了很有把握的神情。“当然不是!有点常识好不好,用数组实现的话 stack 的大小固定,这怎么能够接受呢?!”又一次的,Solmyr 目睹了 zero 表情发生难以言喻的剧烈变化。这次他没能忍住放声大笑的冲动,连杯子里的果汁也洒了出来,一时间,笑声充满了整个办公室 不仅仅是他的,还包括了(众位看官应该猜的到吧?)围观同事们的笑声。驱散了围观者之后,zero 面带愠色的坐下:“有那么好笑吗?”

10、“抱歉抱歉,我 哈哈哈 我 哈哈 我只是一时忍不住 哈哈哈哈 ”,Solmyr 好容易平息了大笑,坐直了身子,放下了果汁,正色道:“关键在于上面引入的应该遵循的两条原则,也就是异常安全,和异常透明。现在你考虑一下如果 stack 内部的数据以指针存放,怎样在赋值运算符中保证上述两点?”“ 嗯 还是会有上面那样一个循环 呃 ”,zero 面有难色。“提示,不一定非得直接拷贝到 stack 保存数据的内存里。”“ 嗯 不直接拷贝,那么就是 就是拷贝到 啊!我明白了!”,zero 抓住了其中的关键,飞快的写下:/ pdata 代表指向存放数据内存的指针,top 代表栈顶元素的偏移量template

11、<typename T>stack<T>& stack<T>:operator=(const stack<T>& rhs)    . .    T* ptemp = new Trhs.top;    try            for(int i=0; i<rhs.top; i+)    

12、0;       *(ptemp+i) = *(rhs.pdata+i);        catch(.)  / 捕捉可能出现的异常            delete ptemp;        throw;  / 重新抛出    

13、60;   delete pdata;  / 释放当前的内存    pdata = ptemp;  / 让 pdata 指向赋值成功的内存块    . .“只要这样”,zero 边输入边说,“只要先把数据拷贝到一个临时分配的缓冲区,在此过程中处理异常,然后让 pdata 指向成功分配的内存就行了。这里的关键是让拷贝动作成为可以 呃 可以安全的取消的,剩下的赋值动作就是简单的指针赋值,肯定不会抛出异常了。”“非常好。值得指出的是,这是一种相当常见的手段,有个名字叫做 copy

14、& swap ,它不仅仅可以用来应付异常,也可以有效的实现一些其他特征。OK,这个问题大概就是这样了。”问题似乎可以告一段落了,Solmyr 开始打算就此结束这个话题。可 zero 疑惑的表情阻止了他。“还有什么问题吗?zero ?”“啊 没什么,我只是在想,异常导致了这么多麻烦,这一次,还有上一次的线程死锁问题(参见“小品文系列”的前一篇,“成对出现”)都是因为异常的存在才会变得如此复杂的,那为什么 C+ 还要支持它呢?有错误完全可以在返回值里报告嘛。”“嗯,这确实是个常见的疑惑,不过答案也很简单,异常的存在有它自己的价值。一、使用异常报告错误可以避免污染函数界面;二、如果你希望报告

15、比较丰富的错误信息,使用一个异常对象比简单的返回值要有效的多,而且避免了返回复杂对象造成的开销;三、也是我认为比较重要的,有些错误不合适用返回值来报告。举个例子,动态内存分配。我问你,C 语言中怎样报告动态内存分配错误?”,Solmyr 转过头来看着 zero 。“malloc 函数返回一个 NULL 值代表动态内存分配错误。”“但是你见过多少 C 程序员在每次使用 malloc 之后都检查返回值?”“ ”“没有是吗?这很正常,每次使用 malloc 之后检查返回值是件令人痛苦的事情,所以即使有 Steve Maguire(注:Writing Clean Code一书的作者)这样的老程序员谆谆

16、教导、耳提面命,还是有数以万计的 C 程序中存在这样的代码:”,Solmyr 顺手键入:/* 传统 C 程序 */int* p = malloc( sizeof(int) );*p = 10;“一旦 malloc 失败返回 NULL,这个程序就会崩溃。然而如果是 C+ 程序,使用 new 的话 ”,Solmyr 键入了对应的代码:/ C+ 程序int* p = new int;*p = 10;“就不存在这样的问题。我问你,这是为什么?”zero 很快找到了答案:“因为如果 new 失败,它会抛出 std:bad_alloc 异常,于是函数在此中断、退出,下面这一行也就不会被调用了。”“正确。而

17、且你不必在每一处处理这个异常,你只要保证你的程序对异常透明,就可以在 main 函数中写下 try . catch 对,捕获所有未捕获的异常。比如你可以在 main 函数中捕捉 std:bad_alloc,在输出内存不足错误信息,然后保存所有未保存的数据,完成所有的清理工作,最后结束程序。一言以蔽之,体面的退出。”zero 点着头,喃喃的重复着:“对,体面的退出。”见 zero 领会了他的意思,Solmyr 继续开始下一个议题:“异常的存在还有最后一个重要价值 也是当初设计它的初衷之一 提供一个通用的手段让构造函数可以方便的报告错误:因为构造函数没有返回值。”“还有析构函数也是。”没等 Sol

18、myr 说完,zero 就加上了这一句。Solmyr 对着自作聪明的 zero 摇了摇头:“不要想当然,关于异常有一个非常重要的原则:永远不要让你的析构函数抛出异常。知道为什么吗?”“ 不知道。” zero 这次决定老实承认。“因为抛出异常的析构函数会导致最简单的程序无法正确运行,比如下面两句:”这次出现在屏幕上的,是看来似乎毫无瑕疵的两行代码:evil p = new evil10;delete p;“看上去一点问题也没有是么?仔细分析一下 delete p 这一句,它会调用 10 次 evil 类的析构函数,假设其中第 5 次 evil 类的析构函数抛出异常,会出现什么情况?”zero 陷入了沉思,视线盯着屏幕一动不动,神情看起来就象是一段执行复杂运算的程序,而且是没有输出的那种。不过没多久,zero 就换了一种表情,这种表情通常被形容为胸有成竹:“我知道了 Solmyr ,在这种情况下,delete 面临两

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

最新文档

评论

0/150

提交评论