异常

写在前面

异常处理机制

异常处理

什么是异常

异常(Exception)是程序在运行时可能出现的会导致程序运行终止的错误。这种错误是不能通过编译系统检查出来的。常见的异常如下:

  • 系统资源不足。例如,内存不足,不可以动态申请内存空间;磁盘空间不足,不能打开新的输出文件等。
  • 用户操作错误导致运算关系不正确。例如,出现分母为0。数学运算溢出,数组越界,参数类型不能转换等。

这些问题是由于运行时系统或者是用户的操作带来的,结果就是导致程序无法继续运行(没有处理这些问题发生之后的逻辑),导致程序崩溃。

如果不处理异常会怎么样?

出现异常程序直接崩溃,用户体验极差。

异常处理的方式

拿c++来说:

异常这个东西肯定是要处理的但是怎么处理是个需要好好商量的。刚学c++的时候,或者看别人处理异常的代码的时候,很容易就知道异常的处理就是throw,try,catch等等这些关键字的使用。刚学的时候只知道在异常出现的地方抛出异常,可以在外部捕获异常处理异常。但是对于这样的模式的由来和好处其实还是很有意思的。

现有的异常处理模式(后面会总结),将问题的检测和解决过程分离,程序的一部分负责检测问题的出现,解决问题的任务传递给程序的另一部分。这样的好处是巨大的,如果将异常的检测和处理写在一起那么对于写这个逻辑的人员很大的压力因为出现的这些运行时异常是他原有逻辑以外的,这样不仅使得这部分的代码变得更长使得逻辑也更不清晰。使用异常处理机制之后,写这段程序的人只需要将可预见的异常封装起来交给上层的调用者,自己不用去关心怎么处理这些异常。上层的调用者负责检测底层有没有异常抛出,根据异常的类型去实现不同的处理。这样上层的人也不用去了解底层的实现是怎样的,他只需要关心底层的返回结果和抛出的异常而已。

异常的检测和解决分离使得程序更加清晰,任务的分配更加的有条理,这是多人合作的时候所必要的条件。还是那句话如果代码是你一个去写去维护,你可以不考虑这些但是多人合作的时候,分工必须明确,尽量让每个人思考属于自己的问题,让合作更加的高效。(为了快也为了好)

c++,java,python 基本采用一样的异常处理机制

c++,java通过throw 关键字抛出异常,python通过raise(引发)异常。
以c++来说,抛出的表达式的类型决定了后面将使用哪段处理代码(就近原则)。
当执行到throw语句之后,剩下的语句将不再执行(被try块包围的代码块内的)。
程序的控制权将从throw转移到与之匹配到的catch模块当中。
这个catch块可以是相同作用域的同级catch也可能是上层调用者的catch(或者更上)。

当抛出一个异常之后会执行栈展开知道找到第一个匹配的catch块或者展开到main函数还是没找到此时程序退出。而一个异常如果被置之不理程序将调用标准库函数terminate终止程序。
栈展开过程当中退出的语句块当中的局部变量都会被销毁。编译器保证会正确的析构这些变量再销毁。

异常处理机制的语法

几个注意点:

  • throw 表达式; 表达式的类型可以是原生数据类型,也可以是类类型,也可以是数组和函数类型。匹配是通过最终表达式的静态编译类型精确匹配的。
  • throw 抛出的表达式 将用于编译器构造异常对象,异常对象是在编译器管理的空间内构造的。一旦栈展开,被推出的作用域当中的局部变量将被销毁,所以抛出的表达式当中的如果包含指针是指向的局部对象那么必然导致错误。
  • catch (参数列表){处理代码} :一个try块应该包含能将出现异常的全部范围的内容或者是将会出现异常的调用。一个try块可以接着多个catch块。catch的用法跟普通的函数基本一样,通过形参获取异常对象,如果catch内部需要修改异常对象的内容再次往上层抛出则形参应该是引用形式传入,如果不改变则可以值传递,当然如果不使用异常对象可以只写异常类型。
  • 异常对象一般会采用继承体系,为了能够使用异常对象继承体系的多态,最好catch的形参传递的是引用的形式获取异常对象。
  • catch块是以出现的先后顺序主意进行匹配的,所以越是精确的catch块越要放在前面越是模糊的catch块越要放在后面,以免屏蔽别的catch块。
  • c++11的noexcept异常说明符

为啥要加上这个异常说明符?

对用户和编译器来说预先知道某个函数不会产生异常是有很大的好处的。
对于用户,当看见一个函数是被声明noexcept,那么就不用对该函数做异常检测和处理非常的省事。
对于编译器,就可以对该函数进行特殊的优化,因为可能会抛出异常的函数不适用于这些优化操作。

所以各方面来说,在已知函数是不会抛出异常的情况下加上noexcept将带来很多的好处。
一旦一个函数做了noexcept的说明那么该函数如果在函数内部还是throw异常(也可能是内部调用的函数抛出的异常),那么编译器会直接调用terminate函数终止程序。所以一个函数如果说明是noexcept的就必须在所以的声明和定义语句当中都会出现要么一次都不出现。

函数指针的声明也可以指明noexcept

一个有趣的点:

c++以前是支持指定函数的异常可能抛出的异常的类型(只能抛出这里面出现的类型的异常)。通过throw (异常类型) 指出:

void funv(int a) throw(Exception)

c++ 11 之后这个规则已经被取消:原因
但是为了向后兼容如果出现throw()这样的地方就等价于全面的noexcept
有意思的是Java的函数是支持这样的方式的。

public void testException() throws IOException

Java通过在函数定义的时候使用throws 异常类型 说明该函数可能跑出的异常的类型。
我个人是感觉保留这样的规则可能会更加的直观。

noexcept运算符

c++11同时还引进了一个noexcept运算符,该运算符可以计算一个表达式是否会抛出异常。
如果抛出异常返回false如果不会则范湖true。用法和sizeof差不多,不会计算表达式的值。
切记参数是表达式。

标准库当中的异常类

标准库的异常采用类继承体系。
分为基类exception :只定义了拷贝构造函数(创建异常对象时都是采用的拷贝构造),拷贝赋值运算符,虚析构函数,一个名为what()的虚成员函数。
这个what函数返回一个const char* 的指针,用于提供有关异常的信息。

标准库定义四大异常类别:类型转换异常,运行时异常,逻辑异常,内存分配异常。

自定义异常:

可以通过继承相应的类型的标准库定义的异常类型,通过改写构造函数,what函数就可以实现自己的异常类。

最后给出Java,c++,python三类语言在异常处理上的例子

  • java
try{
     //try块中放可能发生异常的代码。
     //如果执行完try且不发生异常,则接着去执行finally块和finally后面的代码(如果有的话)。
     //如果发生异常,则尝试去匹配catch块。

}catch(SQLException SQLexception){
    //每一个catch块用于捕获并处理一个特定的异常,或者这异常类型的子类。Java7中可以将多个异常声明在一个catch中。
    //catch后面的括号定义了异常类型和异常参数。如果异常与之匹配且是最先匹配到的,则虚拟机将使用这个catch块来处理异常。
    //在catch块中可以使用这个块的异常参数来获取异常的相关信息。异常参数是这个catch块中的局部变量,其它块不能访问。
    //如果当前try块中发生的异常在后续的所有catch中都没捕获到,则先去执行finally,然后到这个函数的外部caller中去匹配异常处理器。
    //如果try中没有发生异常,则所有的catch块将被忽略。

}catch(Exception exception){
    //...
}finally{

    //finally块通常是可选的。
   //无论异常是否发生,异常是否匹配被处理,finally都会执行。
   //一个try至少要有一个catch块,否则, 至少要有1个finally块。但是finally不是用来处理异常的,finally不会捕获异常。
  //finally主要做一些清理工作,如流的关闭,数据库连接的关闭等。 
}

java在try和catch的使用上和c++基本一致,但是有finally 块用于做清理工作值得注意。

Java支持throws声明:
如果一个方法内部的代码会抛出检查异常(checked exception),而方法自己又没有完全处理掉,则javac保证你必须在方法的签名上使用throws关键字声明这些可能抛出的异常,否则编译不通过。throws仅仅是将函数中可能出现的异常向调用者声明,而自己则不具体处理。采取这种异常处理的原因可能是:方法本身不知道如何处理这样的异常,或者说让调用者处理更好,调用者需要为可能发生的异常负责。

public void foo() throws ExceptionType1 , ExceptionType2 ,ExceptionTypeN
{ 
     //foo内部可以抛出 ExceptionType1 , ExceptionType2 ,ExceptionTypeN 类的异常,或者他们的子类的异常对象。
}
  • python
try:
<语句>        #运行别的代码
except <名字>:
<语句>        #如果在try部份引发了'name'异常
except <名字>,<数据>:
<语句>        #如果引发了'name'异常,获得附加的数据
else:
<语句>        #如果没有异常发生

python的except就像c++和Java当中的catch一样的作用,用于捕获异常。
如果在try子句执行时没有发生异常,python将执行else语句后的语句(如果有else的话),然后控制流通过整个try语句。
使用raise语句自己触发异常(等价于其他语言的throw)
python和Java一样也可以定义finally语句。try-finally 语句无论是否发生异常都将执行最后的代码。

c++ :

额外一点就是可以通过catch(…)捕获任意的异常。
c++ 里是没有finally块的,为什么没有呢?因为不需要。

Java使用finally块的操作将try块获得的资源安全的释放。因为这是Java的不得已的操作。因为Java的内存管理基本是交给Java虚拟机的GC进行管理的,资源的释放并不是第一时间的,所以其实Java当中是没有析构函数这一概念的。所以当资源出了其产生的作用域时必须由我们手动释放否则其释放的时间将不受我们控制,所以Java必须由finally块作为异常出现之后必须执行的代码块去手动的释放资源(关闭打开的文件等等操作)。python也一样,所用的使用GC的语言都是这尿性。

c++就不一样了,c++当中的资源采用RAII的规则进行管理,使用一个局部对象封装所管理的资源,在该对象的构造函数当中获取资源,在其析构函数当中正确的释放资源,只要能够保证该对象的析构函数能够被执行,就能够保证其管理的资源能够被正确的释放。c++的异常管理机制保证了在栈展开的同事出每个局部空间的时候每个创建的局部变量都会得到正确的析构且析构当中不会再抛出异常,所以资源能够在出作用域的时候得到释放。这样的释放操作不用我们程序员去操心,因为调用析构函数的事情是编译器去操作的,也就没必要引进finally块这样多余的操作。

还有一些分析可以看C++中没有finally,那么应该在哪里关闭资源?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值