导图社区 错误与异常处理思维导图
C这是一篇关于 错误与异常处理的思维导图。该思维导图归纳总结了多种针对错误与日常处理的方法和经验,非常实用。
编辑于2021-08-17 18:55:28C++错误与异常处理
识别和处理错误
使用异常处理,可以提高程序的健壮性。
在设计程序的时候,不但要考虑程序的功能,还要考虑程序的可维护性
好的做法是,根据不同的功能,把代码放在不同的文件中,使用函数把代码分成小功能块。
一般情况下,一个函数的代码不应该超过200行。如果代码超过200行,就要考虑把其中一部分代码定义为函数。
对于一个大的系统,函数的调用关系会很复杂,函数的调用链会很深。
函数调用栈越深,系统越脆弱。当调用中某一个函数出现错误的时候,会影响上层函数的处理,很容易导致系统崩溃。出问题的函数越靠近调用链的尾部,出问题造成的破坏越大。
错误通常指程序可以产生预期的结果,但是这个结果不是程序员期望的。
错误一般是指该结果不是期望的,但是可以预料到,可以想办法避免进一步带来的不正常。
方法
利用函数返回值识别错误
函数在处理数据的时候,如果发现了错误的数据,可以用一个特殊的结果来表示处理不成功。
int printf(const char *, ...)
输出函数有一个返回值,代表了该输出语句输出的字符的个数。如果该函数执行失败,则返回一个负数。
对错误结果做相应处理
对函数的返回结果进行处理。在大型的系统设计中,对错误处理的代码要占到所有代码的一半以上。
继续使用错误的结果,会导致不可预料的结果。因此错误剔除得越早,造成的危害就越小。
示例
fopen函数打开一个文件,打开成功,返回文件指针,若失败则返回NULL,因此需要对fopen的返回结果进行判断。同理也要对fread和 fwrite 的返回结果进行判断,看是否成功。对函数返回结果进行判断,可以大幅提高程序的健壮性,但是也存在很大的局限性,例如:
将用户函数和出错处理程序放在一起,出错程序的编写比较烦琐。也就是说在哪里发现错误,哪里就要处理错误。
使用返回值表现错误,可以传递的信息有限。
错误处理对别人的错误无能为力。比如调用了别人的函数,而该函数中对错误的处理不够完善。
调用者经常会忽略函数的返回值,也就是说对于函数的返回值,如果不加处理,编译器是不会提出警告的。
鉴于这种情况,C++中提供了更加完美的处理方法,也就是异常处理。
异常的定义
异常就是运行时出现的不正常,例如运行时耗尽了内存或者遇到意外的非法输入。异常存在于程序的正常功能之外,并要求程序立即处理。
前面介绍了错误处理,错误都是可以预料的。但是当发生没有预料到的错误的时候,就会造成破坏。
对于这样的不可预料的错误,都可能造成系统的崩溃。异常就是不确定的,会产生破坏的错误,上面的代码都会造成异常。上面的异常是由系统产生的,在C++中也提供了产生异常的机制。
在很多时候,代码有错误判断,但是不知道该如何处理。这个时候就可以人为地产生一个异常,把这个错误反馈到上层调用中。
与错误的区别
错误是可以预见到并且知道如何处理的情况,而异常是指不可预见的情况或者出错但不知道怎么处理的情况。
要使用函数返回值来传递错误,必须逐层地传递。每一层函数调用都要处理,否则错误的传递就停止了。也就是说每一层函数都要有对错误的处理,这样的处理相当烦琐。而异常处理则没有这样的情况。
使用异常,可以把出现错误的代码和处理错误的代码分离,使结构更加清晰。使用异常,有一个异常抛出的地方,有一个异常处理的地方,其他函数都不需要考虑这个异常。
使用异常,可以把内层错误直接转移到适当的外层来处理,简化了处理流程。且异常传递的信息可以非常丰富,不受函数返回类型的限制。
抛出异常
如果程序发生异常情况,而在当前环境中获取不到异常处理的足够信息,可以将异常抛出。抛出异常就是创建一个包含出错信息的对象,把该对象送到更大的环境中去。
主动抛出异常
当一段程序中发现错误数据,但是该程序不知道如何处理的时候,可以抛出异常。在C++中,使用throw关键字来抛出异常
语法
throw 错误信息;
在上面的语句中,throw是关键字,表明要抛出一个异常。错误信息是一个变量,可以是任何类型,它存储了异常的必要信息。
异常的类型和函数需要的返回类型没有任何关系。
throw 1;
抛出一个整数的异常。
throw "abcde";
抛出一个字符串的异常
需要注意的是,throw后面应该是一个变量,而不仅仅是一个类型。
throw int;
写法是错误的,因为int是一个类型而不是一个值。如果只想抛出一个整型的异常而并不关心其值是多少
throw int();
上面的语句会新创建一个int型变量,并且默认用0来初始化,然后抛出。
一旦用throw抛出异常,程序的执行将会被终止,从本函数开始,查找可以处理该异常的地方。如果本函数中找不到匹配的处理,则退出本函数,到上一层查找,一直找到整个函数调用链的头部。如果找到main函数还没有找到合适的处理,则程序会失败。
示例
在主函数中调用f,在f中调用g。在第一次调用的时候,没有抛出异常,因此进入函数和退出函数的输出都可以执行;在第二次调用的时候,在g里抛出异常,退出函数,因此只有进入函数的输出,没有退出函数的输出。
自定义异常
上面抛出的异常值,可以是C++的内部数据类型,也可以是自定义的类型,也就是自定义类。任何自定义的类对象都可以作为异常值。
不过在C++的标准库中定义了几个异常类型。在<stdexcept>头文件中定义了这些异常类
在 C++的标准中,推荐在运行库时使用这几个异常类,或者从这几个异常类派生。也就是说C++的标准库的实现中,可能会抛出上面列出的异常类。因此,自定义的异常类可以从上面列出的标准C++异常类继承,也可以不从任何类继承。下面是几个自定义异常类的定义:
上面定义了两个异常类,一个从标准异常类继承,而另一个则不从任何异常类继承。要抛出这样的异常类,跟前面介绍的抛出异常方法一样
throw myError();
throw myExcept;
捕获异常
如果一个函数抛出一个异常,它必须假定该异常能被捕获和处理。正如前面提到的,允许对一个问题集中在一处解决,这也正是C++语言异常处理的一个优点。
try块
如果在函数内抛出一个异常,系统将在异常抛出时退出函数。如果不想在异常抛出时退出函数,可在函数内创建一个特殊块用于捕获这个异常并处理错误。这个块就是try块。try块由关键字try引导
try { // 可能会产生异常的代码 }
try块可以嵌套出现。
可能产生异常的代码必须放在try块的里面,放在try块外面的代码产生的异常不能被该try块所捕获。
异常处理器
catch
异常处理器由多个catch函数块组成,catch函数块中的参数列表只能有一个参数,用于匹配由throw抛出的异常的类型。
异常处理器都必须紧跟在try块之后,try块后面至少有一个catch块。
一旦某个异常被抛出,异常处理机制将会按照我们书写的catch块的代码顺序依次寻找匹配的异常处理器。
一旦找到一个相匹配的异常处理器,则像调用函数一样进入到该处理器里进行处理。
在查找匹配catch块期间,找到的catch块不必是与异常最匹配的那个catch块,相反,将选中第一个找到的可以处理该异常的catch块。
因此,在catch子句列表中,最特殊的catch必须最先出现。catch与switch不同,不需要在每个case块的后面加break,以中断后面程序的执行。
在测试块中不同的函数调用可能会产生相同的异常情况,这时候只需要一个异常处理器。
示例
在函数中抛出一个整型异常,在主函数中捕获这个异常。
函数func1和func2都可以抛出int类型的异常,因此只有一个int类型的异常处理器。整个try块在for循环之内,因此异常的抛出不影响for循环的运行。如果只关心异常的类型,则异常处理器中的标识符可以省略。
异常对象
在抛出异常的时候,被抛出的变量称为异常对象
异常对象的生存周期
因为在处理异常的时候会释放局部存储,所以被抛出的对象就不能再局部存储
而是用throw表达式初始化一个特殊对象。异常对象由编译器管理,而且保证驻留在可能被激活的任意catch块都可以访问的空间。
异常对象被传给对应的catch块,传递的过程类似于函数参数的传递。如果使用值传递的方式,那么catch块中得到的将是异常对象的一个副本,要想在catch块中得到异常对象,那就需要用引用传递。
在这种情况下,catch块中得到的就是异常对象本身,而不是异常对象的副本。在处理完异常之后,异常对象会被销毁。
栈展开
抛出异常的时候,将暂停当前函数的执行,开始查找匹配的 catch 子句。
首先检查 throw 本身是否在try块内部
如果是,检查与该try块相关的catch子句,看其中是否有一个与被抛出对象相匹配。
如果找到匹配的catch子句,就处理异常;
如果找不到,就退出当前函数(释放当前函数的内存并撤销局部对象),并且继续在调用函数中查找。
如果对抛出异常的函数调用是在try块中
则检查与该try块相关的catch子句
如果找到匹配的catch子句,就处理异常;
如果找不到匹配的catch子句,调用函数也退出,并且继续在调用这个函数的函数中查找。
这个过程,称之为栈展开(stack unwinding),沿嵌套函数调用链继续向上,直至为异常找到一个catch子句。只要找到能够处理异常的catch子句,就进入该catch子句,并在该处理代码中继续执行。当catch结束的时候,在紧接着与该try块相关的最后一个catch子句之后的点继续执行。
示例
当函数参数为3时,抛出字符串类型的异常,这个异常可以被函数f的异常处理器所捕获,因此会继续执行 try 块后面的输出语句。
当函数参数为 2 时,抛出 double 类型的异常,函数 f中不能处理该异常,因此直接退出该函数,到函数g中继续查找,可以找到对应的异常处理器,因此g中try块后面的输出语句继续执行。
当函数参数为1时,抛出int类型的异常,栈展开一直到main函数中。
重新抛出
在异常的处理过程中,有可能单个catch不能完全处理一个异常,catch可能确定该异常必须由函数调用链中更上层的函数来处理。这个时候catch可以重新抛出(rethrow)这个异常,重新抛出可以将异常传递给函数调用链中更上层的函数。重新抛出是后面不跟类型或表达式的一个throw
throw
空throw将重新抛出异常对象,它只能出现在catch或者从catch调用的函数中。重新抛出的过程,其实就是中断当前的异常处理器的处理,把异常对象沿函数调用链向上继续传递。重新抛出所抛出的异常对象,还是原来的那个异常对象,而不是形参的对象。
上面代码重新抛出异常时,异常对象的值还是原来的1,而不是修改之后的2。要修改形参的值,可以用引用的方法
重新抛出抛出的还是原来的异常对象,不会新产生异常对象。要想在重新抛出中修改异常对象,需要使用引用传递。
捕获全部异常
所有的异常都必须被处理,否则就会使程序终止。因此当不能确定所有异常的类型的时候,可以捕获全部异常。
语法
在这里用“…”来表示所有的异常,可以跟所有的异常类型匹配。
在这种情况下捕获的异常,不能知道异常的类型,也不能得到异常对象。
前面提到过,在catch子句列表中,最特殊的catch必须最先出现。而 catch(…)可以捕获所有的异常,是应用范围最广的异常处理方法,因此它必须在列表的最后出现,否则后面的catch肯定不能匹配上。
函数与异常
当一个函数有可能会产生异常的时候,其调用者很希望知道该函数所有可能抛出的异常类型,否则就不知道如何编写程序来捕获所有可能的异常情况。
C++语言提供了异常规格说明语法,可以清晰地告诉使用者函数抛出的异常的类型。
异常规格说明存在于函数说明中,位于参数列表之后。异常规格说明再次使用了关键字throw
void f() throw (类型1, 类型2, 类型3);
前面的函数定义可以是任何类型的函数定义
void f() throw( int , char);
上面的语句规定只能抛出int和char类型的异常,而不能抛出其他类型的异常。
在抛出异常的时候,throw 后面必须是异常值,而在进行异常规格说明的时候, throw后面需要有括号,括号里面是异常的类型。
void f();
则意味着可能抛出任何一种异常。因此,不提供任何异常规格说明,并不是不抛出异常,而是可以抛出任何类型的异常。
void f() throw ();
不抛出任何异常
也就是说其异常类型列表为空,在这种情况下则不会产生任何异常。
当函数中试图抛出不在异常规格说明里面的异常的时候,编译器会调用 unexpected()函数。可以使用 set_unexpected()函数来把自定义的函数设置为unexpected()函数。
visual studio编译器不支持异常规格说明,因此下面代码应该在非visual studio的环境下运行才能得到预期的结果。
示例
异常安全的函数
当一个函数抛出异常的时候,会结束当前的执行,在调用链上查找可以处理该异常的 catch块,这个过程可能导致某些数据的不完善。因此使用异常的一个原则就是:
尽量设计异常安全的函数。
异常安全的函数是指即使在这个函数的执行过程中出现异常,也不会影响重要的数据或状态。
要设计异常安全的函数,要求在调用可能会产生异常的语句之前,函数必须提供下面几个保证:
函数提供基本保证,允诺如果一个异常被抛出,程序中剩下的每一个东西都处于合法的状态,没有对象或数据结构被破坏。
函数提供强力保证,允诺如果一个异常被抛出,程序的状态不会发生变化。调用这样的函数,如果它们成功了,它们就完全成功;如果它们失败了,程序的状态就跟它们从来没有被调用过一样。
尽量把可能抛出异常的代码写在函数的前面,让异常尽早抛出,即在改变重要数据或状态之前抛出异常。
使用异常的注意事项
在catch块中,只能重新抛出原来的异常,而不能抛出新的异常。
在抛出异常之后,在该函数的调用链上,一直到可以正确地处理该异常为止,中间的函数的栈空间都会被释放。也就是说对于栈上的类对象,会调用其析构函数,因此在析构函数中不要抛出异常。
在定义catch块的时候,只要碰上可以处理该异常的catch语句,该异常的处理就结束了。因此对于捕捉全部异常catch(…)这样的定义,应该放在catch块的末尾。
要设计异常安全的函数。
综合示例
不同的函数抛出不同的异常,而主程序则分别处理这些异常。