【C++】异常简述(一):C语言中的异常处理机制
1.简介
问题:
1、错误处理的方法有哪些?
2、使用函数返回值还是抛出异常?
3、PHP 原框架下的异常处理机制是怎样的?
4、PHP Yii框架下的错误处理方案是怎样的?有什么参考意义?
错误处理的方法有哪些?
函数返回值:既return true|false,数组,对象 等
使用成员变量保存错误信息:在对象内部预设一个 errors变量(可能更多的时候是一个数组),把内部处理的报错信息暂存起来,最后使用getErrors函数获取错误信息。
抛出异常
触发事件:需要框架支持。
暂时只想到这么多了,盼指正!
使用函数返回值还是抛出异常
如上所述四种处理方法,成员变量的方式多数使用在处理结果不用立即返回的情况,譬如对多个数据字段进行校验返回校验结果,使用场景比较明确。触发事件就主要依赖于框架来实现,复杂的处理逻辑可以考虑,使用场景也相对明确。而函数返回值和抛出异常则用得比较纠结,在编写代码的时候就时常需要考虑什么时候使用异常处理,什么时候考虑返回值,而且在嵌套较深的时候还会让人好好纠结一番。
函数返回值的特点
1)函数返回值比较直接,可以使用true|flase直接表达处理结果,使用状态代码来表示更多的执行结果(推荐使用http的状态码,易读易懂),使用数据或者对象来携带更多的数据,性能较高。
2)返回值的修改、添加、减少会对依赖方带来改动,如果调用的地方很多,修改起来也很麻烦。添加返回值后还需要调用方添加相应的处理代码,往往表现为一个if语句。
3)嵌套很深时,层层都需要对返回值进行处理,层层都需要显式判断,会带来不少冗余代码,而且经常出现处理漏状态的情况。
异常处理
1)异常处理是面向对象语言的处理方案,把错误信息抽象为固定的对象,携带着对象固有的报错信息、格式等等,具有较高的可读性,可能从异常名字就可以判断报错类型。
2)使用异常来处理报错信息,可以让程序变得更简洁,代码只需要关注主流程,省了很多状态判断。同时开发语言的异常处理机制封装了很详细的报错信息,便于定位问题。
3)对调用方依赖较少,新增的异常也不需要一定要求调用方去处理,调用方只需要关注该处理的异常即可。即使深层嵌套也可以把底层异常有效暴露给顶层。
4)异常处理是一种对象处理,而且try和catch也会对数据的出入栈带来消耗,所以性能稍低,这点不难理解。
5)开发人员需要对各种各样的异常创建类定义,开发成本较高。
综上所述,使用哪种方式来反馈错误,的确是要看场景。个人觉得,操作频率高的代码模块尽量少使用异常处理,但是要确保返回信息的简洁。而一些失败因素无法穷举的场景,如果不会出现性能问题,则考虑使用异常。
PHP 原框架下的异常处理机制是怎样的
使用异常的基本思路是抛出异常、捕捉异常、处理异常。如下代码所示,可以在任何你希望的地方,通过throw语句来抛出异常对象。
抛出异常
throw new Exception("错误描述,描述清楚错误原因和可能的解决方案...");
1
捕捉异常
如果异常没有被捕获,而且又没用使用 set_exception_handler() 作相应的处理的话,那么将发生一个严重的错误(致命错误),并且输出 “Uncaught Exception” (未捕获异常)的错误消息。
try
{
//If the exception is thrown, this text will not be shown
}
catch(Exception $e)
{
echo 'Message: ' .$e->getMessage();
}
以上是PHP语言中最基本的异常处理代码,Exception对象是PHP内置的异常类,只是简单得接收异常message,然后展示message。
自定义异常类
一个更成熟的做法是根据自己系统的业务特点来定义异常类,然后处理特定业务场景的异常,实现一种另类的条件选择结构。。如下例子,在找不到对应的商品的时候可以抛出找不到商品异常对象。
class goodsNotFoundException extends Exception
{
public function errorMessage()
{
//error message
$errorMsg = '找不到 '.$this->getMessage().' 这个商品.';
return $errorMsg;
}
}
设置顶层异常处理器 (Top Level Exception Handler)
可以设置顶层异常处理类来处理所有异常。
set_exception_handler('allException');
1
扩展阅读
Exception 的源码阅读:
class Exception {
protected $message = 'Unknown exception'; //异常信息
private $string;
protected $code = 0; //用户自定义异常代码
protected $file; //发生异常的文件名
protected $line; //发生异常的代码行号
private $trace;
private $previous;
final private function __clone () {} //还不知道什么场景下需要用到
function __construct($message = null, $code = 0){} //构造方法
/* 不可重载的方法 */
final function getMessage(){} //返回异常信息
final function getCode(){} //返回异常代码
final function getFile(){} //返回发生异常的文件名
final function getLine(){} //返回发生异常的代码行号
final function getTrace(){} //backtrace() 数组
final function getTraceAsString(){} //已格成化成字符串的 getTrace() 信息
/* 可重载的方法 */
function __toString(){} //可输出的字符串
}
上面这段代码只为说明内置异常处理类Exception的结构,并不是一段有实际意义的可用代码,在IDE里查看PHP的源码基本都是这样的结构代码,内部实现是C代码,在src包里面可以找到。如果使用自定义的类作为异常处理类,则必须是扩展内置异常处理类Exception,非Exception类的子类是不能作为异常处理类使用的。如果在扩展内置处理类Exception时重新定义构造函数的话,建议同时调用parent::construct()来检查所有的变量是否已被赋值。当对象要输出字符串的时候,可以重载__toString()并自定义输出的样式。可以在自定义的子类中,直接使用内置异常处理Exception类中的所有成员属性,但不能重新改写从该父类中继承过来的成员方法,因为该类的大多数公有方法都是final的。
网上摘录了一段样例代码,供参考!
/* 自定义的一个异常处理类,但必须是扩展内异常处理类的子类 */
class MyException extends Exception{
//重定义构造器使第一个参数 message 变为必须被指定的属性
public function __construct($message, $code=0){
//可以在这里定义一些自己的代码
//建议同时调用 parent::construct()来检查所有的变量是否已被赋值
parent::__construct($message, $code);
}
public function __toString() { //重写父类方法,自定义字符串输出的样式
return __CLASS__.":[".$this->code."]:".$this->message."<br>";
}
public function customFunction() { //为这个异常自定义一个处理方法
echo "按自定义的方法处理出现的这个类型的异常<br>";
}
}
try {
//使用自定义的异常类捕获一个异常,并处理异常
$error = '允许抛出这个错误';
throw new MyException($error); //创建一个自定义的异常类对象,通过throw语句抛出
echo 'Never executed'; //从这里开始,try代码块内的代码将不会再被执行
} catch (MyException $e) { //捕获自定义的异常对象
echo '捕获异常: '.$e; //输出捕获的异常消息
$e->customFunction(); //通过自定义的异常对象中的方法处理异常
}
echo '你好呀'; //程序没有崩溃继续向下执行
PHP Yii框架下的错误处理方案是怎样的?有什么参考意义?
yii框架的异常定义集中在vendor\yiisoft\yii2\base目录下,但是限于个人能力,还有部分代码没看懂…
UML的类图结构如下:
1、其中从PHP原生异常对象中扩展 ErrorException,用于代表所有虚拟机级别的异常,可能需要重启服务才可以重新提供服务。
2、扩展UnexpectedValueException,用于不可知的异常,也就是模糊无法定位具体原因的异常。
3、扩展BadMethodCallException,用于调用不正确的类、方法、错误参数等。
4、扩展Exception,代表框架内部一些专有的报错信息,例如加载配置文件失败、架构初始化异常等等。
从Yii框架的异常类的定义来看,PHP语言的内部异常类的确不足够用于所有的场景,而不同名字的异常又充当了 errorCode的角色,无形中也是带框架内部加了不少的if分支。
摘要:catch(exception &ex)是捕获所有标准库定义中的类std:exception;catch(...)则是捕获所有的异常。
异常是由语言提供的运行时刻错误处理的一种方式。提到错误处理,即使不提到异常,你大概也已经有了丰富的经验,但是为了可以清楚的看到异常的好处,我们还是不妨来回顾一下常用的以及不常用的错误处理方式。
1.1常用的错误处理方式
返回值。我们常用函数的返回值来标志成功或者失败,甚至是失败的原因。但是这种做法最大的问题是如果调用者不主动检查返回值也是可以被编译器接受的,你也奈何不了他:)这在C++中还导致另外一个问题,就是重载函数不能只有不同的返回值,而有相同的参数表,因为如果调用者不检查返回值,则编译器会不知道应该调用哪个重载函数。当然这个问题与本文无关,我们暂且放下。只要谨记返回值可能被忽略的情况即可。
全局状态标志。例如系统调用使用的errno。返回值不同的是,全局状态标志可以让函数的接口(返回值、参数表)被充分利用。函数在退出前应该设置这个全局变量的值为成功或者失败(包括原因),而与返回值一样,它隐含的要求调用者要在调用后检查这个标志,这种约束实在是同样软弱。全局变量还导致了另外一个问题,就是多线程不安全:如果多个线程同时为一个全局变量赋值,则调用者在检查这个标志的时候一定会非常迷惑。如果希望线程安全,可以参照errno的解决办法,它是线程安全的。
setjmp()/longjmp()。可以认为它们是远程的goto语句。根据我的经验,它们好象确实不常被用到,也许是多少破坏了结构化编程风格的原因吧。在C++中,应该是更加的不要用它们,因为致命的弱点是longjmp()虽然会unwindingstack(这个词后面再说),但是不会调用栈中对象的析构函数--够致命吧。对于不同的编译器,可能可以通过加某个编译开关来解决这个问题,但太不通用了,会导致程序很难移植。
现在我们再来看看异常能解决什么问题。对于返回值和errno遇到的尴尬,对异常来说基本上不存在,如果你不捕获(catch)程序中抛出的异常,默认行为是导致abort()被调用,程序被终止(coredump)。因此你的函数如果抛出了异常,这个函数的调用者或者调用者的调用者,也就是在当前的callstack上,一定要有一个地方捕获这个异常。而对于setjmp()/longjmp()带来的栈上对象不被析构的问题对异常来说也是不存在的。那么它是否破坏了结构化(对于OOparadigms,也许应该说是破坏了流程?)呢?显然不是,有了异常之后你可以放心的只书写正确的逻辑,而将所有的错误处理归结到一个地方,这不是更好么?
在C语言中1和2两种方式需要配合经典的if else语句进行代码逻辑的跳转和处理。综上所述,在C++中大概异常可以全面替代其它的错误处理方式了,可是如果代码中到处充斥着try/throw/catch也不是件好事,欲知异常的使用技巧,请保持耐心继续阅读:)
人的一生会遇到很多大起大落,尤其是程序员.
程序员写好的程序,论其消亡形式无非三种:无疾而终、自杀、他杀.
当然作为一名程序员,最乐意看到自己写的程序能够无疾而终,因此尽快的学习异常处理机制是非常重要的!
使自己的程序在遇到错误时能够克服错误,更健壮,而不是遇到错误就愤愤自杀.
因此,在简述C++的异常机制之前,本文先来简述一下C语言中的异常处理机制.
在C语言中,传统的错误处理方式有如下几种:
1.直接终止程序(自杀)
例如:
1 2 3 4 5 6 |
|
当用gcc编译完后,执行,会打印“浮点数例外”,然后程序结束.
这种情况是不允许的,无条件终止程序的库无法运用到不能当机的程序里。
2.返回一个错误的值,附加错误码
这种错误处理方式也很常见,比如我们用C语言打开一个文件失败时:
1 2 3 4 5 6 7 8 9 |
|
因为我当前处于Linux系统下,没有GetLastError()函数,所以在Linux下使用全局变量errno来演示.
这种情况,比较常用,但是有时不合适,例如返回错误码是int,每个调用都要检查错误值,极不方便,也容易让程序规模加倍
3.返回一个合法的值,让程序处于某种非法的状态
这种例子,最常见的就是atoi函数,例如:
1 2 3 4 5 6 7 8 9 |
|
虽然b为0,但是有一个全局变量表示了这个0属于非法值,不是由字符串0转过来的.
这种情况,很容易误导调用者,万一调用者没有去检查全局变量errno或者通过其他方式检查错误,那是一个灾难,而且这种方式在并发的情况下不能很好工作
4.调用一个预先准备好在出现"错误"的情况下使用的函数.
这种异常处理情况比较少见,那我就现场直编了一个:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
5、通过暴力的方式解决
暴力的方式有两种,abort()函数和常见exit()函数.例如依然处理除0问题,代码可以这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
6、使用goto语句
虽然,goto语句十分强大,但违背了程序的顺序执行,打乱的程序的执行流,盲目的使用goto语句可能会出现意想不到的错误,因此,并不推荐使用goto语句.
但,尽管如此,为了探索C语言的异常处理机制,我还是实现一下goto语句处理异常的代码.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
7、使用setjmp()与longjmp()
goto语句虽然可以跳来跳去,但标记与goto必须处于同一作用域内.
想从一个函数跳转到另一个函数,就必须使用setjmp与longjmp组合.
实例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
注意事项:
1、setjmp必须先调用,在异常位置通过调用longjmp以恢复先前被保存的程序执行点,否则将导致 不可预测的结果,甚至程序崩溃。
2、在调用setjmp的函数返回之前调动longjmp,否则结果不可预料。
setjmp与longjmp存在以下缺陷:
1、函数的使用者必须非常靠近函数调用的地方编写错误处理代码,无疑使代码变的臃肿笨拙。
2、setjmp()和longjmp()并不能够很好的支持C++面向对象的语义。
以上情况,便是C语言中的异常处理常见的机制,C++提供了更为完善的异常处理机制,我将在下文中简述.
常用连接地址: