c++异常处理
来源:互联网 发布:手机淘宝登录首页登录 编辑:程序博客网 时间:2024/05/17 23:52
一、什么是异常处理
一句话:异常处理就是处理程序中的错误。
二、为什么需要异常处理,以及异常处理的基本思想
C++之父Bjarne Stroustrup在《The C++ Programming Language》中讲到:一个库的作者可以检测出发生了运行时错误,但一般不知道怎样去处理它们(因为和用户具体的应用有关);另一方面,库的用户知道怎样处理这些错误,但却无法检查它们何时发生(如果能检测,就可以再用户的代码里处理了,不用留给库去发现)。
Bjarne Stroustrup说:提供异常的基本目的就是为了处理上面的问题。基本思想是:让一个函数在发现了自己无法处理的错误时抛出(throw)一个异常,然后它的(直接或者间接)调用者能够处理这个问题。
The fundamental idea is that a function that finds a problem it cannot cope with throws an exception, hoping that its (direct or indirect) caller can handle the problem.
也就是《C++ primer》中说的:将问题检测和问题处理相分离。
Exceptions let us separate problem detection from problem resolution
一种思想:在所有支持异常处理的编程语言中(例如java),要认识到的一个思想:在异常处理过程中,由问题检测代码可以抛出一个对象给问题处理代码,通过这个对象的类型和内容,实际上完成了两个部分的通信,通信的内容是“出现了什么错误”。当然,各种语言对异常的具体实现有着或多或少的区别,但是这个通信的思想是不变的。
三、异常出现之前处理错误的方式
在C语言的世界中,对错误的处理总是围绕着两种方法:一是使用整型的返回值标识错误;二是使用errno宏(可以简单的理解为一个全局整型变量)去记录错误。当然C++中仍然是可以用这两种方法的。
这两种方法最大的缺陷就是会出现不一致问题。例如有些函数返回1表示成功,返回0表示出错;而有些函数返回0表示成功,返回非0表示出错。
还有一个缺点就是函数的返回值只有一个,你通过函数的返回值表示错误代码,那么函数就不能返回其他的值。当然,你也可以通过指针或者C++的引用来返回另外的值,但是这样可能会令你的程序略微晦涩难懂。
四、异常为什么好
在如果使用异常处理的优点有以下几点:
1. 函数的返回值可以忽略,但异常不可忽略。如果程序出现异常,但是没有被捕获,程序就会终止,这多少会促使程序员开发出来的程序更健壮一点。而如果使用C语言的error宏或者函数返回值,调用者都有可能忘记检查,从而没有对错误进行处理,结果造成程序莫名其面的终止或出现错误的结果。
2. 整型返回值没有任何语义信息。而异常却包含语义信息,有时你从类名就能够体现出来。
3. 整型返回值缺乏相关的上下文信息。异常作为一个类,可以拥有自己的成员,这些成员就可以传递足够的信息。
4. 异常处理可以在调用跳级。这是一个代码编写时的问题:假设在有多个函数的调用栈中出现了某个错误,使用整型返回码要求你在每一级函数中都要进行处理。而使用异常处理的栈展开机制,只需要在一处进行处理就可以了,不需要每级函数都处理。
五、C++中使用异常时应注意的问题
任何事情都是两面性的,异常有好处就有坏处。如果你是C++程序员,并且希望在你的代码中使用异常,那么下面的问题是你要注意的。
1. 性能问题。这个一般不会成为瓶颈,但是如果你编写的是高性能或者实时性要求比较强的软件,就需要考虑了。
(如果你像我一样,曾经是java程序员,那么下面的事情可能会让你一时迷糊,但是没办法,谁叫你现在学的是C++呢。)
2. 指针和动态分配导致的内存回收问题:在C++中,不会自动回收动态分配的内存,如果遇到异常就需要考虑是否正确的回收了内存。在java中,就基本不需要考虑这个,有垃圾回收机制真好!
3. 函数的异常抛出列表:java中是如果一个函数没有在异常抛出列表中显式指定要抛出的异常,就不允许抛出;可是在C++中是如果你没有在函数的异常抛出列表指定要抛出的异常,意味着你可以抛出任何异常。
4. C++中编译时不会检查函数的异常抛出列表。这意味着你在编写C++程序时,如果在函数中抛出了没有在异常抛出列表中声明的异常,编译时是不会报错的。而在java中,eclipse的提示功能真的好强大啊!
5. 在java中,抛出的异常都要是一个异常类;但是在C++中,你可以抛出任何类型,你甚至可以抛出一个整型。(当然,在C++中如果你catch中接收时使用的是对象,而不是引用的话,那么你抛出的对象必须要是能够复制的。这是语言的要求,不是异常处理的要求)。
6. 在C++中是没有finally关键字的。而java和python中都是有finally关键字的。
六、异常的基本语法
1. 抛出和捕获异常
很简单,抛出异常用throw,捕获用try……catch。
捕获异常时的注意事项:
1. catch子句中的异常说明符必须是完全类型,不可以为前置声明,因为你的异常处理中常常要访问异常类的成员。例外:只有你的catch子句使用指针或者引用接收参数,并且在catch子句内你不访问异常类的成员,那么你的catch子句的异常说明符才可以是前置声明的类型。
2. catch的匹配过程是找最先匹配的,不是最佳匹配。
3. catch的匹配过程中,对类型的要求比较严格。不允许标准算术转换和类类型的转换。(类类型的转化包括两种:通过构造函数的隐式类型转化和通过转化操作符的类型转化)。
4. 和函数参数相同的地方有:
① 如果catch中使用基类对象接收子类对象,那么会造成子类对象分隔(slice)为父类子对象(通过调用父类的复制构造函数);
② 如果catch中使用基类对象的引用接受子类对象,那么对虚成员的访问时,会发生动态绑定,即会多态调用。
③ 如果catch中使用基类对象的指针,那么一定要保证throw语句也要抛出指针类型,并且该指针所指向的对象,在catch语句执行是还存在(通常是动态分配的对象指针)。
5. 和函数参数不同的地方有:
① 如果throw中抛出一个对象,那么无论是catch中使用什么接收(基类对象、引用、指针或者子类对象、引用、指针),在传递到catch之前,编译器都会另外构造一个对象的副本。也就是说,如果你以一个throw语句中抛出一个对象类型,在catch处通过也是通过一个对象接收,那么该对象经历了两次复制,即调用了两次复制构造函数。一次是在throw时,将“抛出到对象”复制到一个“临时对象”(这一步是必须的),然后是因为catch处使用对象接收,那么需要再从“临时对象”复制到“catch的形参变量”中; 如果你在catch中使用“引用”来接收参数,那么不需要第二次复制,即形参的引用指向临时变量。
② 该对象的类型与throw语句中体现的静态类型相同。也就是说,如果你在throw语句中抛出一个指向子类对象的父类引用,那么会发生分割现象,即只有子类对象中的父类部分会被抛出,抛出对象的类型也是父类类型。(从实现上讲,是因为复制到“临时对象”的时候,使用的是throw语句中类型的(这里是父类的)复制构造函数)。
③ 不可以进行标准算术转换和类的自定义转换:在函数参数匹配的过程中,可以进行很多的类型转换。但是在异常匹配的过程中,转换的规则要严厉。
④ 异常处理机制的匹配过程是寻找最先匹配(first fit),函数调用的过程是寻找最佳匹配(best fit)。
2. 异常类型
上面已经提到过,在C++中,你可以抛出任何类型的异常。(哎,竟然可以抛出任何类型,刚看到到这个的时候,我半天没反应过来,因为java中这样是不行的啊)。
注意:也是上面提到过的,在C++中如果你throw语句中抛出一个对象,那么你抛出的对象必须要是能够复制的。因为要进行复制副本传递,这是语言的要求,不是异常处理的要求。(在上面“和函数参数不同的地方”中也讲到了,因为是要复制先到一个临时变量中)
3. 栈展开
栈展开指的是:当异常抛出后,匹配catch的过程。
抛出异常时,将暂停当前函数的执行,开始查找匹配的catch子句。沿着函数的嵌套调用链向上查找,直到找到一个匹配的catch子句,或者找不到匹配的catch子句。
注意事项:
1. 在栈展开期间,会销毁局部对象。
① 如果局部对象是类对象,那么通过调用它的析构函数销毁。
② 但是对于通过动态分配得到的对象,编译器不会自动删除,所以我们必须手动显式删除。(这个问题是如此的常见和重要,以至于会用到一种叫做RAII的方法,详情见下面讲述)
2. 析构函数应该从不抛出异常。如果析构函数中需要执行可能会抛出异常的代码,那么就应该在析构函数内部将这个异常进行处理,而不是将异常抛出去。
原因:在为某个异常进行栈展开时,析构函数如果又抛出自己的未经处理的另一个异常,将会导致调用标准库 terminate 函数。而默认的terminate 函数将调用 abort 函数,强制从整个程序非正常退出。
3. 构造函数中可以抛出异常。但是要注意到:如果构造函数因为异常而退出,那么该类的析构函数就得不到执行。所以要手动销毁在异常抛出前已经构造的部分。
4. 异常重新抛出
语法:使用一个空的throw语句。即写成: throw;
注意问题:
① throw; 语句出现的位置,只能是catch子句中或者是catch子句调用的函数中。
② 重新抛出的是原来的异常对象,即上面提到的“临时变量”,不是catch形参。
③ 如果希望在重新抛出之前修改异常对象,那么应该在catch中使用引用参数。如果使用对象接收的话,那么修改异常对象以后,不能通过“重新抛出”来传播修改的异常对象,因为重新抛出不是catch形参,应该使用的是 throw e; 这里“e”为catch语句中接收的对象参数。
5. 捕获所有异常(匹配任何异常)
语法:在catch语句中,使用三个点(…)。即写成:catch (…) 这里三个点是“通配符”,类似 可变长形式参数。
常见用法:与“重新抛出”表达式一起使用,在catch中完成部分工作,然后重新抛出异常。
6. 未捕获的异常
意思是说,如果程序中有抛出异常的地方,那么就一定要对其进行捕获处理。否则,如果程序执行过程中抛出了一个异常,而又没有找到相应的catch语句,那么会和“栈展开过程中析构函数抛出异常”一样,会 调用terminate 函数,而默认的terminate 函数将调用 abort 函数,强制从整个程序非正常退出。
7. 构造函数的函数测试块
对于在构造函数的初始化列表中抛出的异常,必须使用函数测试块(function try block)来进行捕捉。语法类型下面的形式:
- c#中的异常处理
- 异常处理 - [C++]
- c异常处理
- Objective-c异常处理
- C语言异常处理
- c 异常处理
- C 异常处理设计
- [C++] 异常处理
- C++“异常处理”
- objective-c 异常处理
- 【C++】栈空栈满异常处理
- C++:异常处理
- c++_10: 异常处理
- 【C#】异常处理
- C/C++异常处理
- C++‘异常’处理机制
- C语言异常处理
- 异常处理---C语言
- 如何让密码和学号匹配
- source insight快捷键及使用技巧
- MMU paging
- 为一个text 控件添加一个样式,用于提示输入信息
- 【转】非侵入式设计 和侵入式设计 意思?
- c++异常处理
- UML(二)用例图
- 周鸿祎:把自己当成打工的,一辈子都是打工的
- 熊乃瑾自制蛋糕探望孤儿 岁末温情分享关爱
- 安装使用cuteFTP注意事项
- IOS几种简单有效的数组排序方法
- + - ! function($) (), function 前面的符号意思
- 架构设计的非侵入性原则
- Linux下matlab中文乱码的解决