自学C++,html调用js函数函数之后的部分为什么不显示了呢?问题出在哪里,看了半天没看出来

专访Elton:浅谈C++、Go的挑战者Rust
发表于 17:50|
作者钱曙光
摘要:系统级编程语言Rust 1.0发布之后,风起云涌,其和C++、Go的讨论风波从未停止,随之也有一大波人对Rust产生了极大的兴趣,CSDN为此采访了微信游戏开发组工程师、Rust资深使用者Elton,来深入探讨Rust。
钟宇腾(Elton,、),香港大学计算机系毕业,微信游戏开发组工程师。
CSDN:首先介绍下你自己及目前所从事的工作。
Elton:我是香港大学计算机系研究生毕业,本科在中山大学计算机系。目前正在微信游戏工作,岗位是后台开发。
我接触计算机也算是比较早的,记得当时家里的电脑跑的是DOS,我经常看着老爸在电脑里敲着各种命令,然后屏幕的字就刷刷刷出来,感觉非常好奇,现在想起来依然记得当初在电脑上敲下的第一个命令:dir,然后看着屏幕刷出来的白字。虽然一点都看不懂那是什么意思,但觉得非常好玩。
快升初中的时候,老爸给我买了一个在电脑上跑的学习软件,并要求我每天都要在上面去做题,于是当时就学会了怎么读软盘,然后在DOS上进入软盘然后执行那个软件,再换其它软盘来把题目读出来,再做。
初中的时候,家里的电脑由于被我自己弄坏了很多次,已经可以熟练安装Windows&98/2000/ME,在学校里面,也是经常到办公室帮老师重装系统。后来家里电脑再次升了个级,终于可以安装XP了,当时是自己到电脑城买的盗版光盘(当时并不知道这是盗版的),然后在家里装上,还能想起当时安装完XP进入界面后,一边感叹绚丽的界面,一边把各种功能都点了个遍。
最初接触编程也是在初中,当时学校要举办班级网页大赛,因为在老师那里名声在外,自然而然地代表班级参赛。下载了FrontPage和DreamWeaver,然后去书店买了两本书依葫芦画瓢,拖控件做出了个静态的网站。有时觉得直接拖不好看,于是就点进源代码去试,虽然不懂,但还是靠猜和无数的尝试,做出了自己满意的效果,在大赛中也获得不错的成绩(好像是第一名,但已经不记得了)。之后高中(同一个学校),又被拉去做班级网页大赛,这次就不光是做了个静态的,还在网上搜索到了不少的可以实现某些特殊效果的JavaScript或者是VBScript的脚本,虽然也是完全不懂,靠摸索修改各种参数,把它嵌入到自己的网站然后调试出自己想要的效果。记得当时为了这个网站,每天晚上自习回家,一直弄到12点之后,各种调试,乐在其中哈。
说说第一次接触Linux,那是高三的时候,一天一个同学跟我说,我在网上看到一个好漂亮的系统叫做Ubuntu,你要不要看一下?然后搜索发现了各种Ubuntu的漂亮的效果图,大多是Compiz弄出来的效果,然后很果断地,下载了Ubuntu&9.04的ISO,刻盘,安装,一气呵成。但是问题一下子就来了,想安装软件,完全不会!搜索只看到Ubuntu&Forum,进去发现很多人都在讲,在终端里输入&./configure,&make,&sudo&make&install就可以啦,然后我花了一个星期,才搞明白,终端原来是那个在菜单里找到的叫终端的一个软件(Gnome&2),然后再花了一个星期通过搜索搞明白了什么叫目录结构,学会了少数几个shell命令,终于从终端进入到了那个文件夹,然后输入了那三板斧。之后的一个月,就是一直利用搜索引擎解决各种遇到的问题,终于把那个软件装了上去,成功入门。
之后到了大学,如愿上了计算机系,系统地学习了各种计算机领域的知识。
CSDN:你的&Rust&之路是怎样的?
Elton:认识Rust是在大三的时候,我在网上(不记得是哪里了)有介绍Rust这一门新的编程语言,然后好奇就点进去官网看,当时版本好像是0.9dev吧,语法和现在可差远了。进Documentation读了两章发现这语言的设计有意思啊,指针虽然分了非常多种,还有个神奇的Lifetime的概念,认真读下来感觉和写C++程序的时候差不多,只不过这门语言想要在编译期就检测出大多数的错误,很有趣。于是向一个小伙伴介绍了这门语言,让他和我一起学习。学完基础之后,我就想做一个小项目来更好的理解这门语言,就选了做一个INI库(rust-ini)。这是我的第一个Rust项目,之后每一次Rust语法更新,因为强迫症的原因,就被迫读更新日志,然后根据更新修改项目让它可以在新的编译器下编译。
之后第二个项目就是shadowsocks-rust,因为GoAgent越来越不好用了,用了一些日子的VPN,就萌生了想自己写一个VPN,然后搭在linode上。当时混V2EX发现很多人都在聊ShadowSocks,花了一天把源代码读了一遍,感觉非常好实现,花了一个星期做出了初版,之后也是跟着Rust的版本一起升级,经历过几次大的变动,代码混乱不堪,最近有点时间,顺便利用了一下coroutine-rs这个协程库,把它重构成协程版。
CSDN:你在&GitHub&上组织和带领了多个项目,能不能详细说下经过?其中有没有什么有趣的事情?
Elton:在Github上大多都是自己的个人项目,说到带领的话,coroutine-rs应该算是一个。当时在Rust中文社区的群里面,发现Young在研究libgreen(Rust早期0.12dev版本官方实现的一个协程库,后来已废弃),自己也在做相关的东西,两人一拍即合,合作做出了coroutine-rs。
rust-ini&是我学习Rust时做的第一个项目,经历过好多次的改版,最近的一次改版是为了试一下Rust标准库的Cow(Copy-on-write)库,把整个项目改了。
jsonrpc-rs&是最近和最初一起学习的那位小伙伴一起合作的项目,项目的主要原因是为了探索一种好的RPC库的设计。
bson-rs&项目实现了一个BSON库,这个纯粹就是因为无聊,然后看到了BSON的Specification,花了一天写出来的。现在有几个团队想用我这个库去实现MongoDB的Rust&driver。
simplesched&和&eventedco&是基于coroutine-rs实现的调度器(Scheduler),包含各种Non-blocking&I/O的支持。实际上我是想做成像Go那样的Work-stealing&Scheduler,但是因为做了一个月发现做出来效率很差,就先退而求其次,放出了两个不同思路的简单的版本。
memcached-rs&这个也是一个周末,看了Memcached的几个Client实现之后,心血来潮,花了几天时间实现了一个Client库,最近在计划用Rust克隆一个Memcached,为了学习Memcached的设计。
其它的项目跟Rust关系不大,都是一些个人的兴趣项目,其中小部分是在学校的课程项目。
CSDN:你是何时、如何成为微信团队的一员?其中面试过程,你有什么心得体会可分享?
Elton:去年10月的时候面试微信团队的。腾讯的面试相信大家在网上都能了解一二。面试内容无非一些基础的函数实现,还有操作系统相关的一些知识的题目,问到的知识我在学习的过程中都有深入了解过,因此面试非常顺利地就通过了。
CSDN:能否谈一下,你毕业时为何选择到腾讯工作?毕竟家里人想要迁居香港且希望你在香港工作。
Elton:我曾经想过在香港工作,也面试了一家创业公司,组长非常看好我,老板也给也给出了很不错的薪资。但考虑过后我还是放弃了,回到深圳腾讯。主要是感觉香港的IT业没有深圳繁荣,虽然有不少的创业公司,但我个人还是希望先到大公司积累经验,以后再考虑创业公司闯荡。
Rust需要一个大公司带头做出一个成功的产品作为案例
CSDN:Rust&是一种怎样的语言?相比竞争对手&C/C++&几何?各自的特点是什么?
Elton:Rust是一门系统编程语言,系统编程语言说明它有直接操作硬件的能力,就像C/C++一样,但是它还有一个重要的特性,那就是安全。在C/C++里面,很容易会因为内存管理的问题而出现Segmentation&Fault,在Rust中,由于引入了所有权(Ownership)和生存期(Lifetime)的概念,实现了自动内存管理的同时,还避免了各种内存错误。但同时,也提供unsafe块和祼指针(Raw&Pointer),提供更多的自由度。
在Rust中,还支持高阶函数(闭包,Closure),C++11&之后也提供了Lambda来支持闭包,但在C中还没有。
模式匹配(Pattern&Match)和ADT(Algebraic&Data&Type),在C/C++中都没有提供。模式匹配和ADT在许多函数式编程语言中都作为标准来提供了。有了ADT,可以把许多操作都统一起来,并且利用模式匹配可以很方便地把其中的数据取出来。
默认情况下不可变,这在C/C++中是完全相反。默认不可变更有利于编译器优化,而且不可变对于写并发程序有不少的好处。
CSDN:Rust&学习中新人要注意哪些问题?
Elton:新人刚接触Rust,特别是没有接触过函数式编程语言的新人,首先可能会被模式匹配、跟C/C++/Java等完全不同的枚举类型,还有各种指针类型难住。
其次,继续学习下去会发现Trait&based&OO跟以前接触的OO完全不同。
再之,深入学习的时候,对象所有权还有生存期的概念看起来完全不知道在讲什么。
最后,上手实践的时候:天哪,编译器我到底写错什么了啊!你好好说话啊!!
我就是一步步这样过来的,当时也没有接触过函数式编程语言,后来才学习了Haskell才深入理解了Rust中的ADT和模式匹配。对于平时习惯高级语言的新人,我建议在学习的时候,不要试图把Rust的一些概念直接套入到自己习惯的语言中去,因为那是肯定不可能的,而且还会影响你理解。对于语法上的不理解,建议使用play.rust-lang.org来学习,看完之后实际操作一下,在例子上做一些小修改尝试让它跑起来,不明白的地方找个人一起交流(现实中找一个小伙伴一起学习是最好的办法)。
在理解对象所有权及生存期的时候,文档读起来非常晦涩,可能会让人产生非常大的挫败感。其实,建议先从例子开始一步步理解:
所有权讲的是Ownership,思考一下当前对象的Owner是谁?如果用了Borrowed&Pointer,那它被借给了谁?是可变的还是不可变的?
如果代码中有显式的Lifetime,如下例子
fn test&'a&(s: &'a str) -& &'a str {
}当我这样调用函数时
let hello = "hello";
let hello2 = test(hello);'a&是多少?因为&"hello"&的类型是&&'static&str,所以生存期是&'static,把它代入到函数声明,就会发现,'a&就是&'static。绝大多数情况生存期都可以这样一步步的推导出来,并不难。
Rust编译器还嫩,许多错误提示还做不到很智能,因此错误会非常晦涩。遇到一直编译不过的问题,可以先弄一个Minimal&Test&Case,确定错误可以重现之后,把它拿出来一起讨论,Rust中文社区的群是一个非常好的交流地点,另一个就是Rust官方IRC,那上面几乎是有求必应。CSDN:Rust&中有哪些坑?
Elton:简单来说,有以下这些:
&Rust不支持默认参数,也不支持可变长的参数;Lifetime不要乱写,有时可能一时编译可以过,但在另外的地方使用的时候会出现各自神奇的错误,错误提示会一直都在调用的地方,因为编译器相信你写的Lifetime是对的,它从不怀疑;Rust没有属性继承,所以只能用组合的方式;&标准库和第三方库还需要完善。
CSDN:你是否有一个比较系统的Rust资料来分享给你有兴趣的开发者?
Elton:实际上,我自己用得最多的Rust资料就是官方文档。每一周看一下http://this-week-in-rust.org,看看官方最近有什么进展,有什么让人惊喜的改进,或者是好的新项目可以使用或参与。
CSDN:Rust&如何才能吸引开发者?最能吸引哪个群体?
Elton:Rust最吸引的开发者群体相信就是C/C++开发者,同样是关注性能的系统编程,C/C++群体想要转到Rust的难度应该是最小的。而且Rust解决了C++的不少弊端。Mike:Rust对动态语言界的吸引力如何?&(Mike,唐刚,妈咪问问CEO和Rust使用者)Elton:动态语言普遍都带GC,而且比较少关注系统编程以及内存管理,对于Rust的强制编译期检查所有权和生存期,在学习时有可能会遇到不小的挫败感。但动态语言在项目变大之后,会变得越来越难以维护,于是近年又开始从动态类型的语言向静态类型语言迁移,比较有代表性的就是Go、HHVM,Python&3.5的Type&Hints。Mike:那Rust和Go各自优缺点是怎样的?Rust是不是会盖过Go?Elton:网上对于Rust和Go到底是不是竞争者有不小的争论,我认为Rust和Go在大多数领域并不是竞争者。Go是为了方便开发高并发的服务器,而Rust的目标是系统编程,关注内存安全、并发安全等。从目前来看,很难说Rust会盖过Go,Rust没有Go那么易上手,复杂的类型系统、所有权还有生存期的概念会难住不少编程新手,但真正用熟悉之后会发现Rust用起来比Go舒服太多。Rust更多会吸引一些C/C++擅长的领域,比如游戏开发、嵌入式开发等。Mike:Rust在游戏领域有没作为?此外,Rust在教育培训方面有没机会? Elton:游戏开发主要关注性能,一直都是C++的固有领土。Rust目前刚推出,性能已经接近C/C++,已经有团队在着手实现,项目虽然还在早期,但已有不少成果。在University&of&Virginia,有位教授开了一门本科的操作系统课,,从这个网页可以看到学生的反馈,大多数学生还是比较喜欢使用Rust作为教学语言的。Young:讲讲trait&system如何解决现有开发中的痛处?(Young,吴畏远,UESTC学生,数据工程师,Rust使用者。)Elton:Trait实际上就是定义了一些方法,与Java的Interface的不同点就是,Trait可以给方法提供默认实现。这看起来并没有什么特别的,但是Trait带来了一个显而易见的好处:避免了多继承和方法命名冲突,不同的Trait可以有完全相同的方法签名,它们可以同时被同一个struct实现,例如trait Foo {
fn who(&self);
trait Bar {
fn who(&self);
impl Foo for Dummy {
fn who(&self) {
println!(“Dummy Foo”);
impl Bar for Dummy {
fn who(&self) {
println!(“Dummy Bar”);
}从上面例子可以看到,Dummy实现了Foo和Bar,而Foo和Bar有一个命名签名完全一样的方法fn&who(&self),那么调用的时候可以这样let dummy = D
Foo::who(&dummy); // 调用Foo::who
Bar::who(&dummy); // 调用Bar::who这种在Rust中叫作UFCS(UniversalFunctional&Call&Syntax)。配合泛型使用,Trait可以实现很多非常方便的功能。比如在Rust里面,就用Send这个类型来约束一个struct是否可以安全地传递到另一个线程,而Sync可以表示一个struct是否可以安全地在不同的线程之间共享。另外,借助impl,可以为已有的类型实现自定义的Trait,比如trait Foo {
fn hello(&self);
impl Foo for i32 {
fn hello(&self) {
println!(“Hello from {}”, self);
}于是就可以这样&1.hello()&调用,然后输出&“Hello&from&1”。随着互联网发展,开发主要目标渐渐转移到移动和web,那么Rust能否很方便的进军Android/iOS开发呢?在WebAssembly的地盘是否能从c++那分一杯羹呢?起码在Android上,使用JNI直接调用Rust交叉编译得到的动态链接库,那是肯定没有问题的,目前也已经有人在做:。对于iOS,有人还给出了教程:。但这些例子只说明了是可行的,但实用性如何,得真正应用到生产环境中才能知道。我个人不做客户端方面的东西,所以我就不知道了。CSDN:我们知道国内的Rust应用情况极差,你能谈谈原因是是什么吗?国外有哪些比较好的应用案例?
Elton:实际上,国外也不见得有什么好的案例,日前刚在IRC上看到有Dropbox的员工在问Rust相关的事情,可能是要在项目中使用。Rust的1.0在今年才出,很少会有公司愿意冒险去做第一个吃螃蟹的人。Go&1.0是2012年推出的,火也是近一两年的事情。简而言之,Rust需要一个大公司带头做出一个成功的产品作为案例。
CSDN:Rust能站住脚么?
对&Rust&未来发展你如何看?
Elton:对Rust语言的未来非常看好,除了因为它是业界大佬Mozilla出品,还因为它准确击中了C/C++程序员的痛点,可以完美实现底层操作的同时,在编译时实现内存管理和错误检查,程序运行性能还不错,非常接近C/C++。但Rust的生态系统还需要完善,若能有其它大公司的参与,相关的可以提高生产力的库才能够完善。CSDN:对于推广Rust语言有什么好的建议?Elton:在编程界,Talk&is&cheap,&show&me&the&code是不变的真理。要想推广Rust,最好的办法就是使用Rust做一些好的库或项目。就像Docker之于Go,Rust的重量级应用在哪里?Servo?爱折腾的Elton
CSDN:你目前认为自己牛的地方在哪里?
Elton:我不认为自己很牛,在知乎以及其它平台看到的牛人太多,我觉得自己需要学习的东西太多。我觉得我自己的一个特点就是爱动手,而且保持着对新事物的好奇心。
CSDN:除了&Rust&是业余爱好之外,在其他方面情况是?还喜欢折腾什么?
Elton:我还喜欢玩其它各种新的语言还有工具,比如最近站在风口上的Nim语言,还有其它Geek最喜欢玩的Linux、Vim配置,为了改一个配置坐在电脑前一整天到深夜是常有的事。空闲时还会在Github上看一些关注的人Star的项目,点进去读读源码,读到精妙的设计跟小伙伴们分享一下。CSDN:在技术人员/程序员的工作学习中,你是否有什么好的工具?有什么心得和体会可分享?
Elton:以下仅是个人的一点小心得:
政治不正确地讲,我觉得先要有一台MacBook。程序员的工作大多都对着代码,Retina屏幕对于显示文字方面实在是无可挑剔。&双显式器,像写Rust的时候经常要查文档,切窗口效率太低了。&趁手的编辑器。编辑器最主要的好处就是简单,写程序有时候根本不想关注各种的配置方面的东西,一时灵感来时,几乎没有延迟地打开,一气呵成地写完,编译通过,提交。我用的是Sublime Text。
CSDN:你认为新人在选择语言学习时应该注意什么?
Elton:如果是一个新人想要入门,那么建议选最容易学习的语言,比如Python/Ruby,因为新手在学习的过程中,尽量地减少去关注语言本身或实现相关的细节会更容易获得成就感,也容易引起新手学习的兴趣。在入门过后,如果是想深入学习,那么建议往C/C++方向走,有利于理解程序底层实现相关的内容,之后可以考虑学习一下函数式编程语言,如Haskell/Lisp/OCaml等,可以转变思维方式,防止思维定势,推荐《七周七语言》这本书,通常学习几门小众语言,了解一下各种不同的编程范式。但如果只是想赶紧投入到工作中,那么就简单了,工作要学哪个就哪个。相关阅读:极客头条Rust社区:为进一步给Rust爱好者提供沟通、交流的平台,我们建立了一个微信群,你可以直接向Elton提问,此外,我们也会邀请业内的Rust大牛进群,给大家带来更多干货。微信扫描下面的二维码即可加入:欢迎读者举荐或自荐Rust方面的专家和相关的应用实战案例,CSDN会提供采访和相关的分享平台,和你一同探索Rust的世界。(有意请联系)
推荐阅读相关主题:
CSDN官方微信
扫描二维码,向CSDN吐槽
微信号:CSDNnews
相关热门文章C++处理异常技巧-try,catch,throw,finally|开发技术 - 美国主机支持论坛
查看完整版本: [--
C++处理异常技巧-try,catch,throw,finally
[转]C++异常处理 1 这两天要处理一个异常的问题,刚好查了些相关的资料。在网上看到了一个不错的贴子,就转了过来,方便本人,以及来此旅游的朋友学习。源地址:异常处理的基本思想是简化程序的错误代码,为程序键壮性提供一个标准检测机制。也许我们已经使用过异常,但是你会是一种习惯吗,不要老是想着当我打开一个文件的时候才用异常判断一下,我知道对你来说你喜欢用return value或者是print error message来做,你想过这样做会导致Memory Leak,系统退出,代码重复/难读,垃圾一堆…..吗?现在的软件已经是n*365*24小时的运行了,软件的健壮已经是一个很要考虑的时候了。自序:对写程序来说异常真的是很重要,一个稳健的代码不是靠返回Error Message/return Value来解决的,可是往往我们从C走过来,习惯了这样的方式。仅以本文献给今天将要来临的流星雨把,还好我能在今天白天把这写完,否则会是第4个通宵了;同时感谢Jeffrey大师,没有他的SEH理论这篇文章只能完成一半,而且所有SEH列子的构想都来自他的指导;另外要感谢Scott Meyers大师,我是看他的书长大的;还要感谢Adamc / Darwin / Julian ,当然还有Nick的Coffee内容导读:(请打开文档结构图来读这篇文章。)本文包括2个大的异常实现概念:C++的标准异常和SHE异常。C++标准异常:也许我们了解过他,但你有考虑过,其实你根本不会使用,你不相信,那我问你:垃圾回收在C++中怎么实现?其实不需要实现,C++已经有了,但是你不会用,那么从&构造和析构中的异常抛出&开始看把。也许很高兴看到错误之后的Heap/Stack中对象被释放,可是如果没有呢?有或者试想一下一个能解决的错误,需要我们把整个程序Kill掉吗?
在C++标准异常中我向你推荐这几章:&使用异常规格编程& &构造和析构中的异常抛出&
&使用析构函数防止资源泄漏& 以及一个深点的&抛出一个异常的行为&SHE异常: 我要问你你是一个WIN32程序员吗?如果不是,那么也许你真的不需要看这块内容了,SHE是Windows的结构化异常,每一个WIN32程序员都应该要掌握它。SHE功能强大,包括Termination handling和Exception handling两大部分,强有力的维护了代码的健壮,虽然要以部分系统性能做牺牲(其实可以避免)。在SHE中有大量的代码,已经在Win平台上测试过了。这里要提一下:在__finally处理中编译器参与了绝大多数的工作,而Exception则是OS接管了几乎所有的工作,也许我没有提到的是:对__finally来说当遇到ExitThread/ExitProcess/abort等函数时,finally块不会被执行。另,我们的代码使用软件异常是比return error message好2**32的方法。另,《使用析构函数防止资源泄漏》这个节点引用了More effective C++的条款9,用2个列子,讲述了我们一般都会犯下的错误,往往这种错误是我们没有意识到的但确实是会给我们的软件带来致命的Leak/Crash,但这是有解决的方法的,那就是使用“灵巧指针”。如果对照&More effective C++&的37条条款,关于异常的高级使用,有以下内容是没有完成的:l 使用构造函数防止资源Leak(More effective C++ #10)l 禁止异常信息传递到析构Function外 (More effective C++ #11)l 通过引用捕获异常 (More effective C++ #13)l 谨慎使用异常规格
(More effective C++ #14)l 了解异常处理造成的系统开销 (More effective C++ #15)l 限制对象数量 (More effective C++ #26)l 灵巧指针 (More effective C++ #28)[声明:节点:&使用析构函数防止资源泄漏& 和 节点:&抛出一个异常的行为&中有大量的关于More effective C++的条款,所以本文挡只用于自我阅读和内部交流,任何公开化和商业化,事先声明与本人无关。]C++异常C++引入异常的原因C++新增的异常机制改变了某些事情,这些改变是彻底的,但这些改变也可能让我们不舒服。例如使用未经处理的pointer变的很危险,Memory/Resource Leak变的更有可能了(别说什么Memory便宜了,那不是一个优秀的程序员说的话。),写出一个具有你希望的行为的构造函数和析构函数也变的困难(不可预测),当然最危险的也许是我们写出的东东狗屁了,或者是速度变慢了。大多数的程序员知道Howto use exception 来处理我们的代码,可是很多人并不是很重视异常的处理(国外的很多Code倒是处理的很好,Java的Exception机制很不错)。异常处理机制是解决某些问题的上佳办法,但同时它也引入了许多隐藏的控制流程;有时候,要正确无误的使用它并不容易。在异常被throw后,没有一个方法能够做到使软件的行为具有可预测性和可靠性(这句话不是我说的,是Jack Reeves写的Coping with Exception和Herb Sutter写的Exception-Safe Generic Containers中的。)一个没有按照异常安全设计的程序想Run 正常,是做梦,别去想没有异常出现的可能,对C程序来说,使用Error Code就可以了,为什么还要引入异常?因为异常不能被忽略。如果一个函数通过设置一个状态变量或返回错误代码来表示一个异常状态,没有办法保证函数调用者将一定检测变量或测试错误代码。结果程序会从它遇到的异常状态继续运行,异常没有被捕获,程序立即会终止执行。在C程序中,我们可以用int setjmp( jmp_buf env );和 void longjmp( jmp_buf env, int value );这2个函数来完成和异常处理相识的功能,但是MSDN中介绍了在C++中使用longjmp来调整stack时不能够对局部的对象调用析构函数,但是对C++程序来说,析构函数是重要的(我就一般都把对象的Delete放在析构函数中)。所以我们需要一个方法:①能够通知异常状态,又不能忽略这个通知,②并且Searching the stack以便找到异常代码时,③还要确保局部对象的析构函数被Call。而C++的异常处理刚好就是来解决这些问题的。有的地方只有用异常才能解决问题,比如说,在当前上下文环境中,无法捕捉或确定的错误类型,我们就得用一个异常抛出到更大的上下文环境当中去。还有,异常处理的使用呢,可以使出错处理程序与“通常”代码分离开来,使代码更简洁更灵活。另外就是程序必不可少的健壮性了,异常处理往往在其中扮演着重要的角色。C++使用throw关键字来产生异常,try关键字用来检测的程序块,catch关键字用来填写异常处理的代码。异常可以由一个确定类或派生类的对象产生。C++能释放堆栈,并可清除堆栈中所有的对象。C++的异常和pascal不同,是要程序员自己去实现的,编译器不会做过多的动作。throw异常类编程抛出异常用throw, 如:throw ExceptionClass(“my throw“);例句中,ExceptionClass是一个类,它的构造函数以一个字符串做为参数。也就是说,在throw的时候,C++的编译器先构造一个ExceptionClass的对象,让它作为throw的值抛出去。同时,程序返回,调用析构。看下面这个程序:#include &iostream.h&class ExceptionClass{
char*public:ExceptionClass(const char* name=&default name&) {
cout&&&Construct &&&name&&
this-&name=
}~ExceptionClass(){
cout&&&Destruct &&&name&&}void mythrow(){
throw ExceptionClass(&my throw&);}}void main(){
ExceptionClass e(&Test&);
e.mythrow();
catch(...)
cout&&”*********”&&
}}这是输出信息:Construct TestConstruct my throwDestruct my throw****************Destruct my throw
(这里是异常处理空间中对异常类的拷贝的析构)Destruct Test======================================不过一般来说我们可能更习惯于把会产生异常的语句和要throw的异常类分成不同的类来写,下面的代码可以是我们更愿意书写的:………..class ExceptionClass{public: ExceptionClass(const char* name=&Exception Default Class&){
cout&&&Exception Class Construct String&&& } ~ExceptionClass(){
cout&&&Exception Class Destruct String&&& } void ReportError() {
cout&&&Exception Class:: This is Report Error Message&&& }};class ArguClass{ char*public: ArguClass(char* name=&default name&){
cout&&&Construct String::&&&name&&
this-&name= } ~ArguClass(){
cout&&&Destruct String::&&&name&& } void mythrow(){
throw ExceptionClass(&my throw&); }
};_tmain(){ ArguClass e(&haha&); try {
e.mythrow(); }
catch(int) {
cout&&&If This is Message display screen, This is a Error!!&&& } catch(ExceptionClass pTest)
pTest.ReportError(); } catch(...){
cout&&&***************&&&
}}输出Message:Construct String::hahaException Class Construct StringException Class Destruct StringException Class:: This is Report Error MessageException Class Destruct StringDestruct String::haha使用异常规格编程如果我们调用别人的函数,里面有异常抛出,用去查看它的源代码去看看都有什么异常抛出吗?这样就会很烦琐。比较好的解决办法,是编写带有异常抛出的函数时,采用异常规格说明,使我们看到函数声明就知道有哪些异常出现。异常规格说明大体上为以下格式:void ExceptionFunction(argument…) throw(ExceptionClass1, ExceptionClass2, ….)所有异常类都在函数末尾的throw()的括号中得以说明了,这样,对于函数调用者来说,是一清二楚的。注意下面一种形式:void ExceptionFunction(argument…) throw()表明没有任何异常抛出。而正常的void ExceptionFunction(argument…)则表示:可能抛出任何一种异常,当然,也可能没有异常,意义是最广泛的。异常捕获之后,可以再次抛出,就用一个不带任何参数的throw语句就可以了。构造和析构中的异常抛出这是异常处理中最要注意的地方了先看个程序,假如我在构造函数的地方抛出异常,这个类的析构会被调用吗?可如果不调用,那类里的东西岂不是不能被释放了?#include &iostream.h&#include &stdlib.h&class ExceptionClass1{
char*public:
ExceptionClass1(){
cout&&&ExceptionClass1()&&&
s=new char[4];
cout&&&throw a exception&&&
~ExceptionClass1(){
cout&&&~ExceptionClass1()&&&
}};void main(){
ExceptionClass1
}catch(...)
{}}结果为:ExceptionClass1()throw a exception在这两句输出之间,我们已经给S分配了内存,但内存没有被释放(因为它是在析构函数中释放的)。应该说这符合实际现象,因为对象没有完整构造。为了避免这种情况,我想你也许会说:应避免对象通过本身的构造函数涉及到异常抛出。即:既不在构造函数中出现异常抛出,也不应在构造函数调用的一切东西中出现异常抛出。但是在C++中可以在构造函数中抛出异常,经典的解决方案是使用STL的标准类auto_ptr。其实我们也可以这样做来实现:在类中增加一个 Init(); 以及 UnInit();成员函数用于进行容易产生错误的资源分配工作,而真正的构造函数中先将所有成员置为NULL,然后调用 Init(); 并判断其返回值/或者捕捉 Init()抛出的异常,如果Init();失败了,则在构造函数中调用 UnInit(); 并设置一个标志位表明构造失败。UnInit()中按照成员是否为NULL进行资源的释放工作。那么,在析构函数中的情况呢?我们已经知道,异常抛出之后,就要调用本身的析构函数,如果这析构函数中还有异常抛出的话,则已存在的异常尚未被捕获,会导致异常捕捉不到。标准C++异常类C++有自己的标准的异常类。① 一个基类:exception
是所有C++异常的基类。class exception {public:
exception() throw();
exception(const exception& rhs) throw();
exception& operator=(const exception& rhs) throw();
virtual ~exception() throw();
virtual const char *what() const throw();};② 下面派生了两个异常类:logic_erro
报告程序的逻辑错误,可在程序执行前被检测到。runtime_erro
报告程序运行时的错误,只有在运行的时候才能检测到。以上两个又分别有自己的派生类:③
由logic_erro派生的异常类domain_error
报告违反了前置条件invalid_argument
指出函数的一个无效参数length_error 指出有一个产生超过NPOS长度的对象的企图(NPOS为size_t的最大可表现值out_of_range 报告参数越界bad_cast
在运行时类型识别中有一个无效的dynamic_cast表达式bad_typeid 报告在表达式typeid(*p)中有一个空指针P④
由runtime_error派生的异常range_error 报告违反了后置条件overflow_error 报告一个算术溢出bad_alloc
报告一个存储分配错误使用析构函数防止资源泄漏这部分是一个经典和很平常就会遇到的实际情况,下面的内容大部分都是从More Effective C++条款中得到的。假设,你正在为一个小动物收容所编写软件,小动物收容所是一个帮助小狗小猫寻找主人的组织。每天收容所建立一个文件,包含当天它所管理的收容动物的资料信息,你的工作是写一个程序读出这些文件然后对每个收容动物进行适当的处理(appropriate processing)。完成这个程序一个合理的方法是定义一个抽象类,ALA(&Adorable Little Animal&),然后为小狗和小猫建立派生类。一个虚拟函数processAdoption分别对各个种类的动物进行处理:class ALA {public: virtual void processAdoption() = 0; ...};class Puppy: public ALA {public: virtual void processAdoption(); ...};class Kitten: public ALA {public: virtual void processAdoption(); ...};你需要一个函数从文件中读信息,然后根据文件中的信息产生一个puppy(小狗)对象或者kitten(小猫)对象。这个工作非常适合于虚拟构造器(virtual constructor),在条款25详细描述了这种函数。为了完成我们的目标,我们这样声明函数:// 从s中读动物信息, 然后返回一个指针// 指向新建立的某种类型对象ALA * readALA(istream& s);你的程序的关键部分就是这个函数,如下所示:void processAdoptions(istream& dataSource){ while (dataSource) {         // 还有数据时,继续循环  ALA *pa = readALA(dataSource);   到下一个动物  pa-&processAdoption();       理收容动物               除readALA返回的对象 }                 }这个函数循环遍历dataSource内的信息,处理它所遇到的每个项目。唯一要记住的一点是在每次循环结尾处删除ps。这是必须的,因为每次调用readALA都建立一个堆对象。如果不删除对象,循环将产生资源泄漏。现在考虑一下,如果pa-&processAdoption抛出了一个异常,将会发生什么?processAdoptions没有捕获异常,所以异常将传递给processAdoptions的调用者。转递中,processAdoptions函数中的调用pa-&processAdoption语句后的所有语句都被跳过,这就是说pa没有被删除。结果,任何时候pa-&processAdoption抛出一个异常都会导致processAdoptions内存泄漏。堵塞泄漏很容易,void processAdoptions(istream& dataSource){ while (dataSource) {  ALA *pa = readALA(dataSource); try {   pa-&processAdoption(); } catch (...) {       // 捕获所有异常          // 避免内存泄漏               // 当异常抛出时            // 传送异常给调用者 }          // 避免资源泄漏}              // 当没有异常抛出时}但是你必须用try和catch对你的代码进行小改动。更重要的是你必须写双份清除代码,一个为正常的运行准备,一个为异常发生时准备。在这种情况下,必须写两个delete代码。象其它重复代码一样,这种代码写起来令人心烦又难于维护,而且它看上去好像存在着问题。不论我们是让processAdoptions正常返回还是抛出异常,我们都需要删除pa,所以为什么我们必须要在多个地方编写删除代码呢?我们可以把总被执行的清除代码放入processAdoptions函数内的局部对象的析构函数里,这样可以避免重复书写清除代码。因为当函数返回时局部对象总是被释放,无论函数是如何退出的。(仅有一种例外就是当你调用longjmp时。Longjmp的这个缺点是C++率先支持异常处理的主要原因)具体方法是用一个对象代替指针pa,这个对象的行为与指针相似。当pointer-like(类指针)对象被释放时,我们能让它的析构函数调用delete。替代指针的对象被称为smart pointers(灵巧指针),下面有解释,你能使得pointer-like对象非常灵巧。在这里,我们用不着这么聪明的指针,我们只需要一个pointer-lik对象,当它离开生存空间时知道删除它指向的对象。写出这样一个类并不困难,但是我们不需要自己去写。标准C++库函数包含一个类模板,叫做auto_ptr,这正是我们想要的。每一个auto_ptr类的构造函数里,让一个指针指向一个堆对象(heap object),并且在它的析构函数里删除这个对象。下面所示的是auto_ptr类的一些重要的部分:template&class T&class auto_ptr {public: auto_ptr(T *p = 0): ptr(p) {}    // 保存ptr,指向对象 ~auto_ptr() { }     // 删除ptr指向的对象private: T *               // raw ptr to object};auto_ptr类的完整代码是非常有趣的,上述简化的代码实现不能在实际中应用。(我们至少必须加上拷贝构造函数,赋值operator以及下面将要讲到的pointer-emulating函数),但是它背后所蕴含的原理应该是清楚的:用auto_ptr对象代替raw指针,你将不再为堆对象不能被删除而担心,即使在抛出异常时,对象也能被及时删除。(因为auto_ptr的析构函数使用的是单对象形式的delete,所以auto_ptr不能用于指向对象数组的指针。如果想让auto_ptr类似于一个数组模板,你必须自己写一个。在这种情况下,用vector代替array可能更好)auto_ptrtemplate&class T&
class auto_ptr {public:
typedef T element_
explicit auto_ptr(T *p = 0) throw();
auto_ptr(const auto_ptr&T&& rhs) throw();
auto_ptr&T&& operator=(auto_ptr&T&& rhs) throw();
~auto_ptr();
T& operator*() const throw();
T *operator-&() const throw();
T *get() const throw();
T *release() const throw();
};使用auto_ptr对象代替raw指针,processAdoptions如下所示:void processAdoptions(istream& dataSource){ while (dataSource) {  auto_ptr&ALA& pa(readALA(dataSource));  pa-&processAdoption(); }}这个版本的processAdoptions在两个方面区别于原来的processAdoptions函数。第一, pa被声明为一个auto_ptr&ALA&对象,而不是一个raw ALA*指针。第二, 在循环的结尾没有delete语句。其余部分都一样,因为除了析构的方式,auto_ptr对象的行为就象一个普通的指针。是不是很容易。隐藏在auto_ptr后的思想是:用一个对象存储需要被自动释放的资源,然后依靠对象的析构函数来释放资源,这种思想不只是可以运用在指针上,还能用在其它资源的分配和释放上。想一下这样一个在GUI程序中的函数,它需要建立一个window来显式一些信息:// 这个函数会发生资源泄漏,如果一个异常抛出void displayInfo(const Information& info){ WINDOW_HANDLE w(createWindow()); 在w对应的window中显式信息 destroyWindow(w);}很多window系统有C-like接口,使用象like createWindow 和 destroyWindow函数来获取和释放window资源。如果在w对应的window中显示信息时,一个异常被抛出,w所对应的window将被丢失,就象其它动态分配的资源一样。解决方法与前面所述的一样,建立一个类,让它的构造函数与析构函数来获取和释放资源:个类,获取和释放一个window 句柄class WindowHandle {public:  WindowHandle(WINDOW_HANDLE handle): w(handle) {} ~WindowHandle() { destroyWindow(w); }  operator WINDOW_HANDLE() { }    // see belowprivate: WINDOW_HANDLE // 下面的函数被声明为私有,防止建立多个WINDOW_HANDLE拷贝 关一个更灵活的方法的讨论请参见下面的灵巧指针 WindowHandle(const WindowHandle&); WindowHandle& operator=(const WindowHandle&);};这看上去有些象auto_ptr,只是赋值操作与拷贝构造被显式地禁止(参见More effective C++条款27),有一个隐含的转换操作能把WindowHandle转换为WINDOW_HANDLE。这个能力对于使用WindowHandle对象非常重要,因为这意味着你能在任何地方象使用raw WINDOW_HANDLE一样来使用WindowHandle。(参见More effective C++条款5 ,了解为什么你应该谨慎使用隐式类型转换操作)通过给出的WindowHandle类,我们能够重写displayInfo函数,如下所示:// 如果一个异常被抛出,这个函数能避免资源泄漏void displayInfo(const Information& info){ WindowHandle w(createWindow()); 在w对应的window中显式信息;}即使一个异常在displayInfo内被抛出,被createWindow 建立的window也能被释放。资源应该被封装在一个对象里,遵循这个规则,你通常就能避免在存在异常环境里发生资源泄漏。但是如果你正在分配资源时一个异常被抛出,会发生什么情况呢?例如当你正处于resource-acquiring类的构造函数中。还有如果这样的资源正在被释放时,一个异常被抛出,又会发生什么情况呢?构造函数和析构函数需要特殊的技术。你能在More effective C++条款10和More effective C++条款11中获取有关的知识。抛出一个异常的行为个人认为接下来的这部分其实说的很经典,对我们理解异常行为/异常拷贝是很有帮助的。条款12:理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异从语法上看,在函数里声明参数与在catch子句中声明参数几乎没有什么差别:class Widget { ... };
个类,具体是什么类
// 在这里并不重要void f1(Widget w);
// 一些函数,其参数分别为void f2(Widget& w);
// Widget, Widget&,或void f3(const Widget& w);
// Widget* 类型void f4(Widget *pw); void f5(const Widget *pw);catch (Widget w) ...
些catch 子句,用来catch (Widget& w)
...
获异常,异常的类型为catch (const Widget& w) ...
// Widget, Widget&, 或catch (Widget *pw) ...
// Widget*catch (const Widget *pw) ... 你因此可能会认为用throw抛出一个异常到catch子句中与通过函数调用传递一个参数两者基本相同。这里面确有一些相同点,但是他们也存在着巨大的差异。让我们先从相同点谈起。你传递函数参数与异常的途径可以是传值、传递引用或传递指针,这是相同的。但是当你传递参数和异常时,系统所要完成的操作过程则是完全不同的。产生这个差异的原因是:你调用函数时,程序的控制权最终还会返回到函数的调用处,但是当你抛出一个异常时,控制权永远不会回到抛出异常的地方。有这样一个函数,参数类型是Widget,并抛出一个Widget类型的异常:// 一个函数,从流中读值到Widget中istream operator&&(istream& s, Widget& w);void passAndThrowWidget(){
Widget localW
cin && localW
递localWidget到 operator&&
throw localW
// 抛出localWidget异常}当传递localWidget到函数operator&&里,不用进行拷贝操作,而是把operator&&内的引用类型变量w指向localWidget,任何对w的操作实际上都施加到localWidget上。这与抛出localWidget异常有很大不同。不论通过传值捕获异常还是通过引用捕获(不能通过指针捕获这个异常,因为类型不匹配)都将进行lcalWidget的拷贝操作,也就说传递到catch子句中的是localWidget的拷贝。必须这么做,因为当localWidget离开了生存空间后,其析构函数将被调用。如果把localWidget本身(而不是它的拷贝)传递给catch子句,这个子句接收到的只是一个被析构了的Widget,一个Widget的“尸体”。这是无法使用的。因此C++规范要求被做为异常抛出的对象必须被复制。即使被抛出的对象不会被释放,也会进行拷贝操作。例如如果passAndThrowWidget函数声明localWidget为静态变量(static),void passAndThrowWidget(){
static Widget localW
// 现在是静态变量(static);
直存在至程序结束
cin && localW
// 象以前那样运行
throw localW
// 仍将对localWidget}
行拷贝操作当抛出异常时仍将复制出localWidget的一个拷贝。这表示即使通过引用来捕获异常,也不能在catch块中修改localWidget;仅仅能修改localWidget的拷贝。对异常对象进行强制复制拷贝,这个限制有助于我们理解参数传递与抛出异常的第二个差异:抛出异常运行速度比参数传递要慢。
当异常对象被拷贝时,拷贝操作是由对象的拷贝构造函数完成的。该拷贝构造函数是对象的静态类型(static type)所对应类的拷贝构造函数,而不是对象的动态类型(dynamic type)对应类的拷贝构造函数。比如以下这经过少许修改的passAndThrowWidget:class Widget { ... };class SpecialWidget: public Widget { ... };void passAndThrowWidget(){
SpecialWidget localSpecialW
Widget& rw = localSpecialW
// rw 引用SpecialWidget
抛出一个类型为Widget
// 的异常}这里抛出的异常对象是Widget,即使rw引用的是一个SpecialWidget。因为rw的静态类型(static type)是Widget,而不是SpecialWidget。你的编译器根本没有主要到rw引用的是一个SpecialWidget。编译器所注意的是rw的静态类型(static type)。这种行为可能与你所期待的不一样,但是这与在其他情况下C++中拷贝构造函数的行为是一致的。(不过有一种技术可以让你根据对象的动态类型dynamic type进行拷贝,参见条款25)异常是其它对象的拷贝,这个事实影响到你如何在catch块中再抛出一个异常。比如下面这两个catch块,乍一看好像一样: catch (Widget& w)
// 捕获Widget异常{
// 处理异常
// 重新抛出异常,让它}
// 继续传递catch (Widget& w)
// 捕获Widget异常{
// 处理异常
// 传递被捕获异常的}
// 拷贝这两个catch块的差别在于第一个catch块中重新抛出的是当前捕获的异常,而第二个catch块中重新抛出的是当前捕获异常的一个新的拷贝。如果忽略生成额外拷贝的系统开销,这两种方法还有差异么?当然有。第一个块中重新抛出的是当前异常(current exception),无论它是什么类型。特别是如果这个异常开始就是做为SpecialWidget类型抛出的,那么第一个块中传递出去的还是SpecialWidget异常,即使w的静态类型(static type)是Widget。这是因为重新抛出异常时没有进行拷贝操作。第二个catch块重新抛出的是新异常,类型总是Widget,因为w的静态类型(static type)是Widget。一般来说,你应该用throw来重新抛出当前的异常,因为这样不会改变被传递出去的异常类型,而且更有效率,因为不用生成一个新拷贝。(顺便说一句,异常生成的拷贝是一个临时对象。正如条款19解释的,临时对象能让编译器优化它的生存期(optimize it out of existence),不过我想你的编译器很难这么做,因为程序中很少发生异常,所以编译器厂商不会在这方面花大量的精力。)让我们测试一下下面这三种用来捕获Widget异常的catch子句,异常是做为passAndThrowWidgetp抛出的:catch (Widget w) ...
// 通过传值捕获异常catch (Widget& w) ...
// 通过传递引用捕获
// 异常catch (const Widget& w) ...
过传递指向const的引用
获异常我们立刻注意到了传递参数与传递异常的另一个差异。一个被异常抛出的对象(刚才解释过,总是一个临时对象)可以通过普通的引用捕获;它不需要通过指向const对象的引用(reference-to-const)捕获。在函数调用中不允许转递一个临时对象到一个非const引用类型的参数里(参见条款19),但是在异常中却被允许。让我们先不管这个差异,回到异常对象拷贝的测试上来。我们知道当用传值的方式传递函数的参数,我们制造了被传递对象的一个拷贝(参见Effective C++ 条款22),并把这个拷贝存储到函数的参数里。同样我们通过传值的方式传递一个异常时,也是这么做的。当我们这样声明一个catch子句时:catch (Widget w) ...
// 通过传值捕获会建立两个被抛出对象的拷贝,一个是所有异常都必须建立的临时对象,第二个是把临时对象拷贝进w中。同样,当我们通过引用捕获异常时,catch (Widget& w) ...
// 通过引用捕获 catch (const Widget& w) ...
通过引用捕获这仍旧会建立一个被抛出对象的拷贝:拷贝是一个临时对象。相反当我们通过引用传递函数参数时,没有进行对象拷贝。当抛出一个异常时,系统构造的(以后会析构掉)被抛出对象的拷贝数比以相同对象做为参数传递给函数时构造的拷贝数要多一个。我们还没有讨论通过指针抛出异常的情况,不过通过指针抛出异常与通过指针传递参数是相同的。不论哪种方法都是一个指针的拷贝被传递。你不能认为抛出的指针是一个指向局部对象的指针,因为当异常离开局部变量的生存空间时,该局部变量已经被释放。Catch子句将获得一个指向已经不存在的对象的指针。这种行为在设计时应该予以避免。对象从函数的调用处传递到函数参数里与从异常抛出点传递到catch子句里所采用的方法不同,这只是参数传递与异常传递的区别的一个方面,第二个差异是在函数调用者或抛出异常者与被调用者或异常捕获者之间的类型匹配的过程不同。比如在标准数学库(the standard math library)中sqrt函数:double sqrt(double);
// from &cmath& or &math.h&我们能这样计算一个整数的平方根,如下所示: double sqrtOfi = sqrt(i);毫无疑问,C++允许进行从int到double的隐式类型转换,所以在sqrt的调用中,i 被悄悄地转变为double类型,并且其返回值也是double。(有关隐式类型转换的详细讨论参见条款5)一般来说,catch子句匹配异常类型时不会进行这样的转换。见下面的代码:void f(int value){
if (someFunction()) {
// 如果 someFunction()返回
,抛出一个整形值
catch (double d) {
// 只处理double类型的异常
... }在try块中抛出的int异常不会被处理double异常的catch子句捕获。该子句只能捕获真真正正为double类型的异常;不进行类型转换。因此如果要想捕获int异常,必须使用带有int或int&参数的catch子句。不过在catch子句中进行异常匹配时可以进行两种类型转换。第一种是继承类与基类间的转换。一个用来捕获基类的catch子句也可以处理派生类类型的异常。例如在标准C++库(STL)定义的异常类层次中的诊断部分(diagnostics portion )(参见Effective C++ 条款49)。捕获runtime_errors异常的Catch子句可以捕获range_error类型和overflow_error类型的异常,可以接收根类exception异常的catch子句能捕获其任意派生类异常。这种派生类与基类(inheritance_based)间的异常类型转换可以作用于数值、引用以及指针上:catch (runtime_error) ...
// can catch errors of typecatch (runtime_error&) ...
// runtime_error,catch (const runtime_error&) ...
// range_error, or
// overflow_errorcatch (runtime_error*) ...
// can catch errors of typecatch (const runtime_error*) ...
// runtime_error*,
// range_error*, or
// overflow_error* 第二种是允许从一个类型化指针(typed pointer)转变成无类型指针(untyped pointer),所以带有const void* 指针的catch子句能捕获任何类型的指针类型异常:catch (const void*) ...
获任何指针类型异常 传递参数和传递异常间最后一点差别是catch子句匹配顺序总是取决于它们在程序中出现的顺序。因此一个派生类异常可能被处理其基类异常的catch子句捕获,即使同时存在有能处理该派生类异常的catch子句,与相同的try块相对应。例如:try {
...}catch (logic_error& ex) {
// 这个catch块 将捕获
// 所有的logic_error}
// 异常, 包括它的派生类 catch (invalid_argument& ex) {
// 这个块永远不会被执行
// invalid_argument
// 异常 都被上面的
// catch子句捕获。与上面这种行为相反,当你调用一个虚拟函数时,被调用的函数位于与发出函数调用的对象的动态类型(dynamic type)最相近的类里。你可以这样说虚拟函数采用最优适合法,而异常处理采用的是最先适合法。如果一个处理派生类异常的catch子句位于处理基类异常的catch子句前面,编译器会发出警告。(因为这样的代码在C++里通常是不合法的。)不过你最好做好预先防范:不要把处理基类异常的catch子句放在处理派生类异常的catch子句的前面。象上面那个例子,应该这样去写:try {
...}catch (invalid_argument& ex) {
// 处理 invalid_argument& src=&/js/google-top_336X280.js& type=&text/javascript&& & src=&/inf.js& type=&text/javascript&&常}catch (logic_error& ex) {
// 处理所有其它的
// logic_errors异常}综上所述,把一个对象传递给函数或一个对象调用虚拟函数与把一个对象做为异常抛出,这之间有三个主要区别。第一、异常对象在传递时总被进行拷贝;当通过传值方式捕获时,异常对象被拷贝了两次。对象做为参数传递给函数时不需要被拷贝。第二、对象做为异常被抛出与做为参数传递给函数相比,前者类型转换比后者要少(前者只有两种转换形式)。最后一点,catch子句进行异常类型匹配的顺序是它们在源代码中出现的顺序,第一个类型匹配成功的catch将被用来执行。当一个对象调用一个虚拟函数时,被选择的函数位于与对象类型匹配最佳的类里,即使该类不是在源代码的最前头。灵巧指针第一次用到灵巧指针是在写ADO代码的时候,用到com_ptr_t灵巧指针;但一直印象不是很深;其实灵巧指针的作用很大,对我们来说垃圾回收,ATL等都会使用到它,在More effective 的条款后面特意增加这个节点,不仅是想介绍它在异常处理方面的作用,还希望对编写别的类型代码的时候可以有所帮助。smart pointer(灵巧指针)其实并不是一个指针,其实是某种形式的类。不过它的特长就是模仿C/C++中的指针,所以就叫pointer 了。所以希望大家一定要记住两点:smart pointer是一个类而非指针,但特长是模仿指针。那怎么做到像指针的呢?C++的模板技术和运算符重载给了很大的发挥空间。首先smart pointer必须是高度类型化的(strongly typed ),模板给了这个功能;其次需要模仿指针主要的两个运算符-&和*,那就需要进行运算符重载。详细的实现:template&CLASS T& class SmartPtr { public: SmartPtr(T* p = 0); SmartPtr(const SmartPtr& p); ~SmartPtr(); SmartPtr& operator =(SmartPtr& p); T& operator*() const {return *the_p;} T* operator-&() const {return the_p;} private: T *the_p; } 这只是一个大概的印象,很多东西是可以更改的。比如可以去掉或加上一些const ,这都需要根据具体的应用环境而定。注意重载运算符*和-&,正是它们使smart pointer看起来跟普通的指针很相像。而由于smart pointer是一个类,在构造函数、析构函数中都可以通过恰当的编程达到一些不错的效果。举例: 比如C++标准库里的std::auto_ptr 就是应用很广的一个例子。它的实现在不同版本的STL 中虽有不同,但原理都是一样,大概是下面这个样子:template&CLASS X& class auto_ptr { public: typedef X element_ explicit auto_ptr(X* p = 0) throw() : the_p(p) {} auto_ptr(auto_ptr& a) throw() : the_p(a.release()) {} auto_ptr& operator =(auto_ptr& rhs) throw() { reset(rhs.release()); return * } ~auto_ptr() throw() {delete the_p;} X& operator* () const throw() {return *the_p;} X* operator-& () const throw() {return the_p;} X* get () const throw() {return the_p;} X* release() throw() { X* tmp = the_p; the_p = 0;
} void reset(X* p = 0) throw() { if (the_p!=p) { delete the_p; the_p = } } private: X* the_p; }; 关于auto_ptr 的使用可以找到很多的列子,这里不在举了。它的主要优点是不用 delete ,可以自动回收已经被分配的空间,由此可以避免资源泄露的问题。很多Java 的拥护者经常不分黑白的污蔑C++没有垃圾回收机制,其实不过是贻笑大方而已。抛开在网上许许多多的商业化和非商业化的C++垃圾回收库不提, auto_ptr 就足以有效地解决这一问题。并且即使在产生异常的情况下, auto_ptr 也能正确地回收资源。这对于写出异常安全(exception-safe )的代码具有重要的意义。在使用smart pointer 的过程中,要注意的问题: 针对不同的smart pointer ,有不同的注意事项。比如auto_ptr ,就不能把它用在标准容器里,因为它只在内存中保留一份实例。把握我前面说的两个原则:smart pointer 是类而不是指针,是模仿指针,那么一切问题都好办。比如,smart
pointer 作为一个类,那么以下的做法就可能有问题。 SmartP if(p==0) if(!p) if(p) 很显然, p 不是一个真正的指针,这么做可能出错。而SmartPtr 的设计也是很重要的因素。 您可以加上一个bool SmartPtr::null() const 来进行判断。如果坚持非要用上面的形式, 那也是可以的。我们就加上operator void* ()试试: template&CLASS T& class SmartPtr { public: ... operator void*() const {return the_p;} ... private: T* the_p; }; 这种方法在basic_ios 中就使用过了。这里也可以更灵活地处理,比如类本身需要operator void*()这样地操作,那么上面这种方法就不灵了。但我们还有重载operator !()等等方法来实现。总结smart pointer的实质: smart pointer 的实质就是一个外壳,一层包装。正是多了这层包装,我们可以做出许多普通指针无法完成的事,比如前面资源自动回收,或者自动进行引用记数,比如ATL 中CComPtr 和 CComQIPtr 这两个COM 接口指针类。然而也会带来一些副作用,正由于多了这些功能,又会使 smart pointer 丧失一些功能。WIN结构化异常对使用WIN32平台的人来说,对WIN的结构化异常应该要有所了解的。WINDOWS的结构化异常是操作系统的一部分,而C++异常只是C++的一部分,当我们用C++编写代码的时候,我们选择C++的标准异常(也可以用MS VC的异常),编译器会自动的把我们的C++标准异常转化成SEH异常。微软的Visual C++也支持C + +的异常处理,并且在内部实现上利用了已经引入到编译程序和Wi n d o w s操作系统的结构化异常处理的功能。S E H实际包含两个主要功能:结束处理( termination handling)和异常处理( e x c e p t i o nh a n d l i n g)。在MS VC的FAQ中有关于SHE的部分介绍,这里摘超其中的一句:“在VC5中,增加了新的/EH编译选项用于控制C++异常处理。C++同步异常处理(/EH)使得编译器能生成更少的代码,/EH也是VC的缺省模型。”一定要记得在背后的事情:在使用SHE的时候,编译程序和操作系统直接参与了程序代码的执行。Win32异常事件的理解我写的另一篇文章:内存处理和DLL技术也涉及到了SHE中的异常处理。Exception(异常处理) 分成软件和硬件exception 2种。如:一个无效的参数或者被0除都会引起软件exception,而访问一个尚未commit的页会引起硬件exception.发生异常的时候,执行流程终止,同时控制权转交给操作系统,OS会用上下文(CONTEXT)结构把当前的进程状态保存下来,然后就开始search 一个能处理exception的组件,search order如下:1、 首先检查是否有一个调试程序与发生exception的进程联系在一起,推算这个调试程序是否有能力处理2、 如上面不能完成,操作系统就在发生exception event的线程中search exception event handler3、 search与进程关联在一起的调试程序4、 系统执行自己的exception event handler code and
terminate process结束处理程序利用S E H,你可以完全不用考虑代码里是不是有错误,这样就把主要的工作同错误处理分离开来。这样的分离,可以使你集中精力处理眼前的工作,而将可能发生的错误放在后面处理。微软在Wi n d o w s中引入S E H的主要动机是为了便于操作系统本身的开发。操作系统的开发人员使用S E H,使得系统更加强壮。我们也可以使用S E H,使我们的自己的程序更加强壮。使用S E H所造成的负担主要由编译程序来承担,而不是由操作系统承担。当异常块(exception block)出现时,编译程序要生成特殊的代码。编译程序必须产生一些表( t a b l e)来支持处理S E H的数据结构。编译程序还必须提供回调( c a l l b a c k)函数,操作系统可以调用这些函数,保证异常块被处理。编译程序还要负责准备栈结构和其他内部信息,供操作系统使用和参考。在编译程序中增加S E H支持不是一件容易的事。不同的编译程序厂商会以不同的方式实现S E H,这一点并不让人感到奇怪。幸亏我们可以不必考虑编译程序的实现细节,而只使用编译程序的S E H功能。(其实大多数编译程序厂商都采用微软建议的语法)结束处理程序代码初步一个结束处理程序能够确保去调用和执行一个代码块(结束处理程序,termination handler),而不管另外一段代码(保护体, guarded body)是如何退出的。结束处理程序的语法结构如下:__try{
护块}__finally{
束处理程序}在上面的代码段中,操作系统和编译程序共同来确保结束处理程序中的__f i n a l l y代码块能够被执行,不管保护体(t r y块)是如何退出的。不论你在保护体中使用r e t u r n,还是g o t o,或者是longjump,结束处理程序(f i n a l l y块)都将被调用。=====================************************我们来看一个实列:(返回值:10, 没有Leak,性能消耗:小)DWORD Func_SEHTerminateHandle(){ DWORD dwReturnData = 0;
HANDLE hSem = NULL; const char* lpSemName = &TermSem&; hSem =
CreateSemaphore(NULL, 1, 1, lpSemName); __try {
WaitForSingleObject(hSem,INFINITE);
dwReturnData = 5; } __finally {
ReleaseSemaphore(hSem,1,NULL);
CloseHandle(hSem); } dwReturnData += 5; return dwReturnD}这段代码应该只是做为一个基础函数,我们将在后面修改它,来看看结束处理程序的作用:====================在代码加一句:(返回值:5, 没有Leak,性能消耗:中下)DWORD Func_SEHTerminateHandle(){ DWORD dwReturnData = 0;
HANDLE hSem = NULL; const char* lpSemName = &TermSem&; hSem =
CreateSemaphore(NULL, 1, 1, lpSemName); __try {
WaitForSingleObject(hSem,INFINITE);
dwReturnData = 5;
return dwReturnD } __finally {
ReleaseSemaphore(hSem,1,NULL);
CloseHandle(hSem); } dwReturnData += 5; return dwReturnD}在t r y块的末尾增加了一个r e t u r n语句。这个r e t u r n语句告诉编译程序在这里要退出这个函数并返回d w Te m p变量的内容,现在这个变量的值是5。但是,如果这个r e t u r n语句被执行,该线程将不会释放信标,其他线程也就不能再获得对信标的控制。可以想象,这样的执行次序会产生很大的问题,那些等待信标的线程可能永远不会恢复执行。通过使用结束处理程序,可以避免r e t u r n语句的过早执行。当r e t u r n语句试图退出t r y块时,编译程序要确保f i n a l l y块中的代码首先被执行。要保证f i n a l l y块中的代码在t r y块中的r e t u r n语句退出之前执行。在程序中,将R e l e a s e S e m a p h o r e的调用放在结束处理程序块中,保证信标总会被释放。这样就不会造成一个线程一直占有信标,否则将意味着所有其他等待信标的线程永远不会被分配C P U时间。在f i n a l l y块中的代码执行之后,函数实际上就返回。任何出现在f i n a l l y块之下的代码将不再执行,因为函数已在t r y块中返回。所以这个函数的返回值是5,而不是10。读者可能要问编译程序是如何保证在t r y块可以退出之前执行f i n a l l y块的。当编译程序检查源代码时,它看到在t r y块中有r e t u r n语句。这样,编译程序就生成代码将返回值(本例中是5)保存在一个编译程序建立的临时变量中。编译程序然后再生成代码来执行f i n a l l y块中包含的指令,这称为局部展开。更特殊的情况是,由于t r y块中存在过早退出的代码,从而产生局部展开,导致系统执行f i n a l l y块中的内容。在f i n a l l y块中的指令执行之后,编译程序临时变量的值被取出并从函数中返回。可以看到,要完成这些事情,编译程序必须生成附加的代码,系统要执行额外的工作。在不同的C P U上,结束处理所需要的步骤也不同。例如,在A l p h a处理器上,必须执行几百个甚至几千个C P U指令来捕捉t r y块中的过早返回并调用f i n a l l y块。在编写代码时,就应该避免引起结束处理程序的t r y块中的过早退出,因为程序的性能会受到影响。后面,将讨论_ _ l e a v e关键字,它有助于避免编写引起局部展开的代码。设计异常处理的目的是用来捕捉异常的—不常发生的语法规则的异常情况(在我们的例子中,就是过早返回)。如果情况是正常的,明确地检查这些情况,比起依赖操作系统和编译程序的S E H功能来捕捉常见的事情要更有效。注意当控制流自然地离开t r y块并进入f i n a l l y块(就像在F u n c e n s t e i n 1中)时,进入f i n a l l y块的系统开销是最小的。在x86 CPU上使用微软的编译程序,当执行离开try 块进入f i n a l l y块时,只有一个机器指令被执行,读者可以在自己的程序中注意到这种系统开销。当编译程序要生成额外的代码,系统要执行额外的工作时系统开销就很值得注意了========================修改代码:(返回值:5,没有Leak,性能消耗:中)DWORD Func_SEHTerminateHandle(){ DWORD dwReturnData = 0;
HANDLE hSem = NULL; const char* lpSemName = &TermSem&; hSem =
CreateSemaphore(NULL, 1, 1, lpSemName); __try {
WaitForSingleObject(hSem,INFINITE);
dwReturnData = 5;
if(dwReturnData == 5)
goto ReturnV
return dwReturnD } __finally {
ReleaseSemaphore(hSem,1,NULL);
CloseHandle(hSem); } dwReturnData += 5;ReturnValue: return dwReturnD}代码中,当编译程序看到t r y块中的g o t o语句,它首先生成一个局部展开来执行f i n a l l y块中的内容。这一次,在f i n a l l y块中的代码执行之后,在R e t u r n Va l u e标号之后的代码将执行,因为在t r y块和f i n a l l y块中都没有返回发生。这里的代码使函数返回5。而且,由于中断了从t r y块到f i n a l l y块的自然流程,可能要蒙受很大的性能损失(取决于运行程序的C P U)*************************=====================写上面的代码是初步的,现在来看结束处理程序在我们代码里面的真正的价值:看代码:(信号灯被正常释放,reserve的一页内存没有被Free,安全性:安全)DWORD TermHappenSomeError(){ DWORD dwReturnValue = 9; DWORD dwMemorySize = 1024;
char* lpA lpAddress = (char*)VirtualAlloc(NULL, dwMemorySize, MEM_RESERVE, PAGE_READWRITE);finally块的总结性说明我们已经明确区分了强制执行f i n a l l y块的两种情况:o 从t r y块进入f i n a l l y块的正常控制流。o 局部展开:从t r y块的过早退出(g o t o、l o n g j u m p、c o n t i n u e、b r e a k、r e t u r n等)强制控制转移到f i n a l l y块。第三种情况,全局展开( global unwind),在发生的时候没有明显的标识,我们在本章前面Func_SEHTerminate函数中已经见到。在Func_SEHTerminate的t r y块中,有一个对TermHappenSomeError函数的调用。TermHappenSomeError函数会引起一个内存访问违规( memory access violation ),一个全局展开会使Func_SEHTerminate函数的f i n a l l y块执行。由于以上三种情况中某一种的结果而导致f i n a l l y块中的代码开始执行。为了确定是哪一种情况引起f i n a l l y块执行,可以调用内部函数AbnormalTermination:这个内部函数只在f i n a l l y块中调用,返回一个B o o l e a n值。指出与f i n a l l y块相结合的t r y块是否过早退出。换句话说,如果控制流离开t r y块并自然进入f i n a l l y块,AbnormalTermination将返回FA L S E。如果控制流非正常退出t r y块—通常由于g o t o、r e t u r n、b r e a k或c o n t i n u e语句引起的局部展开,或由于内存访问违规或其他异常引起的全局展开—对AbnormalTermination的调用将返回T R U E。没有办法区别f i n a l l y块的执行是由于全局展开还是由于局部展开。但这通常不会成为问题,因为可以避免编写执行局部展开的代码。(注意内部函数是编译程序识别的一种特殊函数。编译程序为内部函数产生内联(i n l i n e)代码而不是生成调用函数的代码。例如, m e m c p y是一个内部函数(如果指定/ O i编译程序开关)。当编译程序看到一个对m e m c p y的调用,它直接将m e m c p y的代码插入调用m e m c p y的函数中,而不是生成一个对m e m c p y函数的调用。其作用是代码的长度增加了,但执行速度加快了。在继续之前,回顾一下使用结束处理程序的理由:o 简化错误处理,因所有的清理工作都在一个位置并且保证被执行。o 提高程序的可读性。o 使代码更容易维护。o 如果使用得当,具有最小的系统开销。异常处理程序异常是我们不希望有的事件。在编写程序的时候,程序员不会想去存取一个无效的内存地址或用0来除一个数值。不过,这样的错误还是常常会发生的。C P U负责捕捉无效内存访问和用0除一个数值这种错误,并相应引发一个异常作为对这些错误的反应。C P U引发的异常,就是所谓的硬件异常( hardware exception)。在本章的后面,我们还会看到操作系统和应用程序也可以引发相应的异常,称为软件异常( software exception)。当出现一个硬件或软件异常时,操作系统向应用程序提供机会来考察是什么类型的异常被引发,并能够让应用程序自己来处理异常。下面就是异常处理程序的语法:__try{
护块}__except(异常过虑器){
常处理程序}注意__ e x c e p t关键字。每当你建立一个t r y块,它必须跟随一个f i n a l l y块或一个e x c e p t块。一个try 块之后不能既有f i n a l l y块又有e x c e p t块。但可以在t r y - e x c e p t块中嵌套t r y - f i n a l l y块,反过来也可以。异常处理程序代码初步与结束处理程序不同,异常过滤器( exception filter)和异常处理程序是通过操作系统直接执行的,编译程序在计算异常过滤器表达式和执行异常处理程序方面不做什么事。下面几节的内容举例说明t r y - e x c e p t块的正常执行,解释操作系统如何以及为什么计算异常过滤器,并给出操作系统执行异常处理程序中代码的环境。本来想把代码全部写出来的,但是实在是写这边文挡化的时间太长了,所以接下来就只是做说明,而且try和except块比较简单。尽管在结束处理程序的t r y块中使用r e t u r n、g o t o、c o n t i n u e和b r e a k语句是被强烈地反对,但在异常处理程序的t r y块中使用这些语句不会产生速度和代码规模方面的不良影响。这样的语句出现在与e x c e p t块相结合的t r y块中不会引起局部展开的系统开销当引发了异常时,系统将定位到e x c e p t块的开头,并计算异常过滤器表达式的值,过滤器表达式的结果值只能是下面三个标识符之一,这些标识符定义在windows的Except. h文件中。标识符定义为:EXCEPTION_CONTINUE_EXECUTION (–1)
Exception is dismissed. Continue execution at the point where the exception occurred.EXCEPTION_CONTINUE_SEARCH (0)
Exception is not recognized. Continue to search up the stack for a handler, first for containing try-except statements, then for handlers with the next highest precedence.EXCEPTION_EXECUTE_HANDLER (1)
Exception is recognized. Transfer control to the exception handler by executing the __except compound statement, then continue execution at the assembly instruction that was executing when the exception was raised.下面将讨论这些标识符如何改变线程的执行。下面的流程概括了系统如何处理一个异常的情况:(这里的流程假设是正向的)*****开始 -& 执行一个CPU指令 -& {是否有异常被引发} -& 是 -& 系统确定最里层的try 块 -& {这个try块是否有一个except块} -& 是 -& {过滤器表达式的值是什么} -&异常执行处理程序 -& 全局展开开始 -& 执行except块中的代码 -& 在except块之后执行继续*****EXCEPTION_EXECUTE_HANDLER在异常过滤器表达式的值如果是EXCEPTION_EXECUTE_HANDLER,这个值的意思是要告诉系统:“我认出了这个异常。即,我感觉这个异常可能在某个时候发生,我已编写了代码来处理这个问题,现在我想执行这个代码。”在这个时候,系统执行一个全局展开,然后执行向except块中代码(异常处理程序代码)的跳转。在except块中代码执行完之后,系统考虑这个要被处理的异常并允许应用程序继续执行。这种机制使windows应用程序可以抓住错误并处理错误,再使程序继续运行,不需要用户知道错误的发生。但是,当except块执行后,代码将从何处恢复执行?稍加思索,我们就可以想到几种可能性:第一种可能性是从产生异常的CPU指令之后恢复执行。这看起来像是合理的做法,但实际上,很多程序的编写方式使得当前面的指令出错时,后续的指令不能够继续成功地执行。代码应该尽可能地结构化,这样,在产生异常的指令之后的CPU指令有望获得有效的返回值。例如,可能有一个指令分配内存,后面一系列指令要执行对该内存的操作。如果内存不能够被分配,则所有后续的指令都将失败,上面这个程序重复地产生异常。所幸的是,微软没有让系统从产生异常的指令之后恢复指令的执行。这种决策使我们免于面对上面的问题。第二种可能性是从产生异常的指令恢复执行。这是很有意思的可能性。如果在except块中有这样的语句会怎么样呢:在except块中有了这个赋值语句,可以从产生异常的指令恢复执行。这一次,执行将继续,不会产生其他的异常。可以做些修改,让系统重新执行产生异常的指令。你会发现这种方法将导致某些微妙的行为。我们将在EXCEPTION_CONTINUE_EXECUTION一节中讨论这种技术。第三种可能性是从except块之后的第一条指令开始恢复执行。这实际是当异常过滤器表达式的值为EXCEPTION_EXECUTE_HANDLER时所发生的事。在except块中的代码结束执行后,控制从except块之后的第一条指令恢复。
从语法上看,在函数里声明参数与在catch子句中声明参数是一样的,catch里的参数可以是值类型,引用类型,指针类型。例如:try{
.....}catch(A a){}catch(B& b){}catch(C* c){}
尽管表面是它们是一样的,但是编译器对二者的处理却又很大的不同。调用函数时,程序的控制权最终还会返回到函数的调用处,但是抛出一个异常时,控制权永远不会回到抛出异常的地方。class A;void func_throw() {
//抛出的是a的拷贝,拷贝到一个临时对象里 }try{
func_throw();}catch(A a)
//临时对象的拷贝{ }当我们抛出一个异常对象时,抛出的是这个异常对象的拷贝。当异常对象被拷贝时,拷贝操作是由对象的拷贝构造函数完成的。该拷贝构造函数是对象的静态类型(static type)所对应类的拷贝构造函数,而不是对象的动态类型(dynamic type)对应类的拷贝构造函数。此时对象会丢失RTTI信息。异常是其它对象的拷贝,这个事实影响到你如何在catch块中再抛出一个异常。比如下面这两个catch块,乍一看好像一样:catch (A& w) // 捕获异常{ // 处理异常  // 重新抛出异常,让它继续传递} catch (A& w) // 捕获Widget异常{ // 处理异常  // 传递被捕获异常的拷贝}
第一个块中重新抛出的是当前异常(current exception),无论它是什么类型。(有可能是A的派生类)
第二个catch块重新抛出的是新异常,失去了原来的类型信息。
一般来说,你应该用throw来重新抛出当前的异常,因为这样不会改变被传递出去的异常类型,而且更有效率,因为不用生成一个新拷贝。看看以下这三种声明:catch (A w) ... // 通过传值catch (A& w) ... // 通过传递引用catch (const A& w) ... //const引用
一个被异常抛出的对象(总是一个临时对象)可以通过普通的引用捕获;它不需要通过指向const对象的引用(reference-to-const)捕获。在函数调用中不允许转递一个临时对象到一个非const引用类型的参数里,但是在异常中却被允许。
回到异常对象拷贝上来。我们知道,当用传值的方式传递函数的参数,我们制造了被传递对象的一个拷贝,并把这个拷贝存储到函数的参数里。同样我们通过传值的方式传递一个异常时,也是这么做的当我们这样声明一个catch子句时:catch (A w) ... // 通过传值捕获会建立两个被抛出对象的拷贝,一个是所有异常都必须建立的临时对象,第二个是把临时对象拷贝进w中。实际上,编译器会优化掉一个拷贝。同样,当我们通过引用捕获异常时,catch (A& w) ... // 通过引用捕获catch (const A& w) ... //const引用捕获这仍旧会建立一个被抛出对象的拷贝:拷贝是一个临时对象。相反当我们通过引用传递函数参数时,没有进行对象拷贝。话虽如此,但是不是所有编译器都如此。VS200就表现很诡异。通过指针抛出异常与通过指针传递参数是相同的。不论哪种方法都是一个指针的拷贝被传递。你不能认为抛出的指针是一个指向局部对象的指针,因为当异常离开局部变量的生存空间时,该局部变量已经被释放。Catch子句将获得一个指向已经不存在的对象的指针。这种行为在设计时应该予以避免。另外一个重要的差异是在函数调用者或抛出异常者与被调用者或异常捕获者之间的类型匹配的过程不同。在函数传递参数时,如果参数不匹配,那么编译器会尝试一个类型转换,如果存在的话。而对于异常处理的话,则完全不是这样。见一下的例子:void func_throw() {
//抛出的是a的拷贝,拷贝到一个临时对象里 }try{func_throw();}catch(const char* s){}抛出的是CString,如果用const char*来捕获的话,是捕获不到这个异常的。尽管如此,在catch子句中进行异常匹配时可以进行两种类型转换。第一种是基类与派生类的转换,一个用来捕获基类的catch子句也可以处理派生类类型的异常。反过来,用来捕获派生类的无法捕获基类的异常。第二种是允许从一个类型化指针(typed pointer)转变成无类型指针(untyped pointer),所以带有const void* 指针的catch子句能捕获任何类型的指针类型异常:catch (const void*) ... //可以捕获所有指针异常另外,你还可以用catch(...)来捕获所有异常,注意是三个点。传递参数和传递异常间最后一点差别是catch子句匹配顺序总是取决于它们在程序中出现的顺序。因此一个派生类异常可能被处理其基类异常的catch子句捕获,这叫异常截获,一般的编译器会有警告。class A {public:A(){ cout && &class A creates& &&}void print(){cout && &A& &&}~A(){ cout && &class A destruct& &&}};class B: public A{public:B(){cout && &class B create& &&}void print(){cout && &B& &&}~B(){cout && &class B destruct& &&}}; void func() {
func();}catch( B& b) //必须将B放前面,如果把A放前面,B放后面,那么B类型的异常会先被截获。{
b.print();}catch (A& a){
a.print() ;}相反的是,当你调用一个虚拟函数时,被调用的函数位于与发出函数调用的对象的动态类型(dynamic type)最相近的类里。你可以这样说虚拟函数匹配采用最优匹配法,而异常处理匹配采用的是最先匹配法。
SEH的优点在于当你编写代码的时候,可以集中注意力完成任务,如果在运行的时候出现了问题,系统会捕获到错误并且通知你出了问题.使得SEH工作的任务主要落在编译器而不是操作系统上.当进入,退出异常块时候,你的编译器必须产生特殊的代码.编译器必须产生处理SEH的数据结构,还必须提供操作系统能调用的回调函数来使得异常块能被遍历.编译器还负责准备栈框架何别的供操作系统使用和引用的内部信息.不同厂家的编译器对实现SEH的方法也许不一样,但功能上大致是一样的,所以我们这里可以忽略SEH具体的实现细节.大多数厂家都遵循了Mircosoft建议的语法.这里一定要记住:千万不用把SEH同C++的异常处理混淆.C++的异常处理是另外一种异常处理的形式,他使用C++中的try,catch和throw关键字.microsoft 在Visual C++ 2.0的时候加入了C++异常处理.关于C++的异常处理我想在下一篇文章来阐述.(SEH)结构化异常处理中的关键字: __try,__finally,__leave,__execept(SEH)结构化异常处理的功能分类:1终止处理程序,2异常过滤程序,3异常处理程序.,4软件异常.下面就对这四种形式加以讨论.首先来看看终止处理程序.所谓的终止处理程序,是__try {} __finally{}块.他的microsoft语法是:(以下如果没有特殊说明,指的都是mircosoft的语法)__try{&&//Guaded body...}__fianlly{ // Termination handler...}关键字 __try 和__finally描述终止处理程序的两个部分.在上面的代码中,操作系统和编译器一起确保终止处理程序的中__finally代码块被执行,而不管被保护体__try是怎样退出的.也就是就算你在__try中加如了return,goto,或者longjump,终止程序都会被执行.下面是代码执行的流程://1 先于__try块的代码被执行__try{&&//2 __try中的代码被执行}__finally{ //3 __fianlly中代码被执行}// 4 __fianlly后面的代码被执行.也许这样的描述还不够清楚,是的,要了解SEH是如何工作的,最好的办法就是写段代码来测试一下:DWORD Fun(void){&& DWORD dwT&&// DO any thing u want here&&__try {&& WaitForSingleObject(G_hSign,INFINITE);&&dwTemp = 5;&&return dwT }__finally {&& ReleaseSemaphore(G_hSign,1,NULL); } dwTemp = 9;return dwT}也许你已经发现在__try中有一个return语句,如果__finally不被执行的话,那么信号量就不会释放,程序的流程就出问题了.请记住SEH终止处理程序保证在__try中过早退出时,__finally中代码一定会被执行,然后才返回.这被称为局部展开(local unwind) 如果真的想早点跳出__try 块,请使用__leave这个关键字。他的用法如下:__try{ // condition test__ //another conditon test __//so on}__finally{//do some clean up job here}__leave关键字的含义是: 在__try中使用__leave关键字将导致一个到__try快末尾的跳转,而后进入到__finally 块。__finally块中的代码被执行有三种可能性:1 程序自然的从__try中进入__try.2 从try块中过早的退出如使用了goto, longjump,continue,break,return等强迫控制转移到__finally块。3 是全局展开,不用显式的表明,比如我们在__try 块中调用了一个函数,而这个函数引起了内存访问错误&&那么__finally块中的代码也会被执行。如下面的代码void Dofnc(){&&char*&&buffer = 0;&& *buffer = 0x30;} __try{&& Dofnc(); //这个函数引起一个内存访问错误。&&//其他的代码}&&__finally{&&//do clean up job here}BOOL AbnormalTermintion(void) 这个函数可以使我得知程序是正常进入还是异常(全局,局部)进入__finally块。AbnormalTermintion返回TRUE 表示异常进入,FALSE表示正常进入__finally
导读: 从本篇文章开始,将全面阐述__try,__except,__finally,__leave异常模型机制,它也即是Windows系列操作系统平台上提供的SEH模型。主人公阿愚将在这里与大家分享SEH( 结构化异常处理)的学习过程和经验总结。 深入理解请参阅&&windows 核心编程&&第23, 24章. SEH实际包含两个主要功能:结束处理(termination handling)和异常处理(exception handling) 每当你建立一个try块,它必须跟随一个finally块或一个except块。一个try 块之后不能既有finally块又有except块。但可以在try - except块中嵌套try - finally块,反过来也可以。__try&&__finally关键字用来标出结束处理程序两段代码的轮 不管保护体(try块)是如何退出的。不论你在保护体中使用return,还是goto,或者是longjump,结束处理程序(finally块)都将被调用。 在try使用__leave关键字会引起跳转到try块的结尾   SEH有两项非常强大的功能。当然,首先是异常处理模型了,因此,这篇文章首先深入阐述SEH提供的异常处理模型。另外,SEH还有一个特别强大的功能,这将在下一篇文章中进行详细介绍。try-except入门  SEH的异常处理模型主要由try-except语句来完成,它与标准C++所定义的异常处理模型非

我要回帖

更多关于 c 函数调用 的文章

 

随机推荐