一.异常的概念:
- 程序在运行的过程中可能产生异常。
- 异常(Exception)与Bug的区别:
异常是程序运行时可预料的执行分支。
Bug是程序中的错误,是不被预期的运行方式。 - 异常(Exception)和Bug的对比:
- 异常:运行时产生除0的情况,需要打开的外部文件不存在,数组访问时越界
- Bug:使用野指针
堆数组使用结束未释放
选择排序无法处理长度为0的数组
二、语言异常处理
C语言
强制终止
- 使用标准C库提供了abort()和exit()两个函数,它们可以强行终止程序的运行,其声明处于头文件中。(
exit
,abort
是stdlib标准库中的内置函数,函数void abort(void)
中止程序执行,直接从调用的地方跳出。)
#include<stdio.h>
#include<stdlib.h>
double diva(double num1,double num2){
//两数相除函数
double re;
re=num1/num2;
return re;
}
int main(){
double a,b,result;
printf("请输入第一个数字:");
scanf("%lf",&a);
printf("请输入第二个数字:");
scanf("%lf",&b);
if(0==b) exit(0);/*or abort();*/ //如果除数为0终止程序17
result=diva(a,b);
printf("相除的结果是: %.2lf\n",result);
return 0;
}
assert触发
- 使用assert(断言)宏调用,当程序出错时,就会引发一个abort()。(
assert
定义在assert.h头文件中)
void assert(int expression);
expression -- 这可以是一个变量或任何 C 表达式。如果 expression 为 TRUE,
assert() 不执行任何动
作.如果 expression 为 FALSE,assert() 会在标准错误 stderr 上显示错误消
息,并中止程序执行。
#include <assert.h>
#include <stdio.h>
int main(){
int a;
char str[50];
printf("请输入一个整数值: ");
scanf("%d", &a);
assert(a >= 10);
printf("输入的整数是: %d\n", a);
printf("请输入字符串: ");
scanf("%s", str);
assert(str != NULL);
printf("输入的字符串是: %s\n", str);
return(0);
}
errno设置
- 使用errno全局变量,由C运行时库函数提供,位于头文件中。 库宏 extern int errno 是通过系统调用设置的,在错误事件中的某些库函数表明了什么发生了错误。(不常用难以理解)
Goto函数内跳转
- 使用goto语句,当出错时跳转。C 语言中的 goto 语句允许把控制无条件转移到同一函数内的被标记的语句。
注意:在任何编程语言中,都不建议使用 goto 语句。因为它使得程序的控制流难以跟踪,使程序难以理解和难以修改。任何使用 goto 语句的程序可以改写成不需要使用 goto 语句的写法。
#include <stdio.h>
int main (){
/* 局部变量定义 */
int a = 10;
/* do 循环执行 */
LOOP:do{
if( a == 15){
/* 跳过迭代 */
a = a + 1;
goto LOOP;
}
printf("a 的值: %d\n", a);
a++;
}while( a < 20 );
return 0;
}
setjmp,longjmp跳转。
- 我们都知道goto在c语言中会经常用到,但是goto的跳转只能再同一个函数内进行跳转。如果我不想在同一个函数内进行跳转,而是在不同函数之间进行跳转,这就需要用到setjmp 和 longjmp函数。
- 宏 int setjmp(jmp_buf environment) :创建本地的jmp_buf缓冲区并且初始化,用于将来跳转回此处。这个子程序保存程序的调用环境于env参数所指的缓冲区,env将被longjmp使用。如果是从setjmp直接调用返回,setjmp返回值为0。如果是从longjmp恢复的程序调用环境返回,setjmp返回非零值。
- 函数 void longjmp(jmp_buf environment, int value) 恢复最近一次调用 setjmp() 宏时保存的环境,jmp_buf 参数的设置是由之前调用 setjmp() 生成的。
#include <stdio.h>
#include <setjmp.h>
static jmp_buf buf; //全局变量不建议定义,若无初始赋值则为0
void second(void) {
printf("second\n"); // 打印
longjmp(buf,1); // 跳回setjmp的调用处 - 使得setjmp返回值为1
}
void first(void) {
second();
printf("first\n"); // 不可能执行到此行
}
int main() {
if ( ! setjmp(buf) ) {
first(); // 进入此行前,setjmp返回0
} else { // 当longjmp跳转回,setjmp返回1,因此进入此行
printf("main\n"); // 打印
}
return 0;
}
输出结果:
second
main
C++
throw表达式
- 异常检测部分使用了throw 表达式来表示它遇到了无法处理的问题。我们说throw引发了异常。
那么如何添加自己写的错误信息呢?可以用 头文件定义的其他异常
类,比如 runtime_error,表示只有在运行时才能检测出的问题。示
例如下:
#include <iostream>
using namespace std;
int main(){
int a=5,b=0;
if(b==0)
throw runtime_error("Error: b=0");
return 0;
}
//output:
terminate called after throwing an instance of 'std::runtime_error'
what(): Error: b=0
其中 what() 是C++异常类型唯一定义的成员函数,该函数没有任何参数,返回
值是一个 const char* 的字符串。
try 语句块
- 异常处理部分使用try语句块处理异常。try语句块以关键字try开始,并以一个或多个catch子句结束。try 语句块中代码抛出的异常通常被某个catch子句处理。catch子句也被称为异常处理代码。
#include <iostream>
using namespace std;
int main(){
double m ,n;
cin >> m >> n;
try {
cout << "before dividing." << endl;
if( n == 0)
throw -1; //抛出int类型异常
else
cout << m / n << endl;
cout << "after dividing." << endl;
}
catch(double d) {
cout << "catch(double) " << d << endl;
}
catch(int e) {
cout << "catch(int) " << e << endl;
}
cout << "finished" << endl;
return 0;
}
异常类
- 一套异常类,用于在throw表达式和相关catch子句之间传递异常的具体信息。
C++ 标准库中有一些类代表异常,这些类都是从 exception 类派生 而来的。常用的几个异 常类如图 1 所示。
bad_typeid、bad_cast、bad_alloc、ios_base::failure、out_of_range 都是 exception 类的派生类。C++ 程序在碰到某些异常时,即使程序中没有写 throw 语句,也会自动拋出上述异常类的对象。这些异常类还都有名为 what 的成员函数,返回字符串形式的异常描述信息。使用这些异常类需要包含头文件 stdexcept。
下面分别介绍以上几个异常类。本节程序的输出以 Visual Studio
2010为准,Dev C++ 编译
的程序输出有所不同。
1) bad_typeid
使用 typeid 运算符时,如果其操作数是一个多态类的指针,
而该指针的值为 NULL,则会拋出此异常。
2) bad_cast
在用 dynamic_cast 进行从多态基类对象(或引用)到派生类
的引用的强制类型转换时,如果转换是不安全的,则会拋出此
异常。程序示例如下:
#include <iostream>
#include <stdexcept>
using namespace std;
class Base{
virtual void func() {}
};
class Derived : public Base{
public:
void Print() {}
};
void PrintObj(Base & b){
try {
Derived & rd = dynamic_cast <Derived &>(b);
//此转换若不安全,会拋出 bad_cast 异常
rd.Print();
}
catch (bad_cast & e) {
cerr << e.what() << endl;
}
}
int main(){
Base b;
PrintObj(b);
return 0;
}
程序的输出结果如下:
Bad dynamic_cast!
在 PrintObj 函数中,通过 dynamic_cast 检测 b 是否引用的是一个 Derived 对象,如果是,就调用其 Print 成员函数;如果不是,就拋出异常,不会调用 Derived::Print。
3) bad_alloc
在用 new 运算符进行动态内存分配时,如果没有足够的内存,则会
引发此异常。程序示例如下:
#include <iostream>
#include <stdexcept>
using namespace std;
int main()
{
try {
char * p = new char[0x7fffffff]; //无法分配这么多空间,会抛出异常
}
catch (bad_alloc & e) {
cerr << e.what() << endl;
}
return 0;
}
程序的输出结果如下:
bad allocation
ios_base::failure
在默认状态下,输入输出流对象不会拋出此异常。如果用流对象的 exceptions 成员函数设置了一些标志位,则在出现打开文件出错、读到输入流的文件尾等情况时会拋出此异常。此处不再赘述。
4) out_of_range
用 vector 或 string 的 at 成员函数根据下标访问元素时,如果下标
越界,则会拋出此异常。
例如:
#include <iostream>
#include <stdexcept>
#include <vector>
#include <string>
using namespace std;
int main()
{
vector<int> v(10);
try {
v.at(100) = 100; //拋出 out_of_range 异常
}
catch (out_of_range & e) {
cerr << e.what() << endl;
}
string s = "hello";
try {
char c = s.at(100); //拋出 out_of_range 异常
}
catch (out_of_range & e) {
cerr << e.what() << endl;
}
return 0;
}
程序的输出结果如下:
invalid vector
<T>
subscript
invalid string position
如果将v.at(100)换成v[100],将s.at(100)换成s[100],程序就不会引发异常(但可能导致程序崩溃)。因为 at 成员函数会检测下标越界并拋出异常,而 operator[] 则不会。operator [] 相比 at 的好处就是不用判断下标是否越界,因此执行速度更快。
Java
java 的异常处理机制可以让程序具有极好的容错性,让程序更加健壮。当程序运行出现意外情形时,系统会自动生成一个Exception 对象来通知程序,从而实现将“业务功能实现代码”和“错误处理代码”分离,提供更好的可读性
使用try-.catch-finally捕获异常
-
Java 异常处理机制为:抛出异常,捕捉异常,处理异常。
-
Java–.catch 语法格式:
try {
//业务功能代码
}
catch(异常类1 e1){
//异常处理代码1
}
catch(异常类2 e2){
//异常处理代码2
}
catch(异常类n en){
//异常处理代码n
}
-
在使用try…catch捕获处理异常时需要注意:
①不要过度使用异常,不能使用异常处理机制来代替正常的流程控制语句;
②异常捕获时,一定要先捕获小异常,再捕获大异常。否则小异常将无法被捕获;
③避免出现庞大的try块;
④避免使用catch (Exception e) {};
⑤不要略异常
class Main {
public static void main(String[] args) {
try {
// code that generate exception
int divideByZero = 5 / 0;
System.out.println("Rest of code in try block");
}catch (ArithmeticException e) {
System.out.println("ArithmeticException => " + e.getMessage());
}
}
}
throws抛出
throws是另一种处理异常的方式,它不同于try...catch...finally,throws仅
仅是将函数中可能出现的异常向调用者声明,而自己则不具体处理。
throws声明:如果一个方法内部的代码会抛出检查异常(checked
exception),而方法自己又没有完全处理掉或并不能确定如何处理这种异常,
则javac保证你必须在方法的签名上使用throws关键字声明这些可能抛出的异
常,表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理,
否则编译不通过。
在方法声明中用throws语句可以声明抛出异常的列表,throws后面的异常
类型可以是方法中产生的异常类型,也可以是它的父类。采取这种异常处理的
原因可能是:方法本身不知道如何处理这样的异常,或者说让调用者处理更好,
调用者需要为可能发生的异常负责。
- 语法格式:
修饰符 返回值类型 方法名() throws ExceptionType1 , ExceptionType2
,ExceptionTypeN{
//方法内部可以抛出 ExceptionType1 , ExceptionType2 ,ExceptionTypeN
类的异常,或者他们的子类的异常对象。
}
import java.io.*;
public class ThrowsTest {
public static void main(String[] args) {
ThrowsTest t = new ThrowsTest();
try {
t.readFile();
} catch (IOException e) {
e.printStackTrace();
}
}
public void readFile() throws IOException {
FileInputStream in = new FileInputStream("hello.txt");
int b;
b = in.read();
while (b != -1) {
System.out.print((char) b);
b = in.read();
}
in.close();
}
}
手动抛出throw
- 程序员也可以通过throw语句手动显式的抛出一个异常。throw语句的后面可以抛出的异常必须是Throwable或其子类的实例。下面的语句在编译时将会产生语法错误:
throw new String(“你抛我试试.”);
- throw 语句必须写在函数中,执行throw 语句的地方就是一个异常抛出点,它和由JRE自动形成的异常抛出点没有任何差别。
public class Main {
public static void main(String[] args) {
try {
Student s = new Student();
s.regist(-1001);
System.out.println(s);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
class Student {
private int id;
public void regist(int id) throws Exception {
if (id > 0) {
this.id = id;
} else {
// System.out.println("您输入的数据非法!");
//手动抛出异常对象
// throw new RuntimeException("您输入的数据非法!");
throw new Exception("您输入的数据非法!");
//错误的
// throw new String("不能输入负数");
}
}
@Override
public String toString() {
return "Student [id=" + id + "]";
}
}
throws和throw的区别:
throw是语句抛出一个异常。
语法:throw (异常对象);
throw e;
throws是方法可能抛出异常的声明。
(用在声明方法时,表示该方法可能要抛出异常)
语法:[(修饰符)](返回值类型)(方法名)([参数列表])
[throws(异常类)]{......}
public void doA(int a) throws Exception1,Exception3
{......}
throw语句用在方法体内,表示抛出异常,由方法体内的语句处理。
throws出现在方法函数头,表示在抛出异常,由该方法的调用者来处理。
throws主要是声明这个方法会抛出这种类型的异常,使它的调用者知道要捕获
这个异常。
throw是具体向外抛异常的动作,所以它是抛出一个异常实例。
throws说明你有那个可能倾向。
throw的话,那就是你把那个倾向变成真实的了。
两者都是消极处理异常的方式(这里的消极并不是说这种方式不好),只是
抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函
数的上层调用处理。
- 以上仅为简介Java基础的异常处理若需详细了解点击下列链接
作者:我是波哩个波的Java异常处理详解
Python的异常机制
Python的异常机制主要依赖try 、except 、else、 finally 和raise五个关键字:try关键字后缩进的代码块简称try块,它里面放置的是可能引发异常的代码;在except后对应的是异常类型和一个代码块,用于表明该except块处理这种类型的代码块;在多个except 块之后可以放一个else块,表明程序不出现异常时还要执行else块;最后还可以跟一个finally 块,finally 块用于回收在try块里打开的物理资源,异常机制会保证finally块总被执行;raise用于引发一个实际的异常,raise 可以单独作为语句使用,引发一个具体的异常对象。
常见的Python异常
AssertionError:断言语句(assert)失败
在以前的分支和循环章节中讲过断言语句(assert),当assert这个关键字
后边的条件为假的时候,程序将终止并抛出AssertionError异常。assert语
句一般是在测试程序的时候用于在代码中置入检查点:
AttributeError:尝试访问未知的对象属性
当试图访问的对象属性不存在时抛出的异常:
IndexError:索引超出序列的范围
在使用序列的时候就常常会遇到IndexError异常,原因是索引超出序列范围
的内容:
KeyError:字典查找一个不存在的关键字
当试图在字典中查找一个不存在的关键字时就会引发KeyError异常,因此建
议使用dict.get()方法:
NameError:尝试访问一个不存在的变量
当尝试访问一个不存在的变量时,Python会抛出NameError异常:
OSError:操作系统产生的异常
OSError顾名思义就是操作系统产生的异常,像打开一个不存在的文件会引
发FileNotFoundError,而这个FileNotFoundError就是OSError的子类。
上面的例子已经演示过,这里就不再赘述了。
~~
SyntaxError:Python的语法错误
如果遇到SyntaxError是Python的语法错误,这时Python的代码并不能继续
执行,你应该找到并改正错误:
TypeError:不同类型间的无效操作
有些类型不同是不能相互进行计算的,否则会抛出TypeError异常:
ZeroDivisionError:除数为零
我们都知道除数不能为零,所以除以零就会引发ZeroDivisionError异常:
try-except语句
try-except语句格式如下:
try:
检测范围
except Exception[as reason]:
出现异常(Exception)后的处理代码
导致OSError异常的原因有很多(例如FileNotFoundError、FileExistsError、PermissionError等),所以可能会更在意错误的具体内容,这里可以使用as把具体的错误消息打印出来:
-
针对不同异常设置多个except
一个try语句可以和多个except语句搭配,分别对感兴趣的异常进行检测处理:
-
对多个异常统一处理
except后面还可以跟多个异常,然后对这些异常进行统一的处理:
-
捕获所有异常
如果你无法确定要对哪一类异常进行处理,只是希望在try语句块里一旦出现任何异常,可以给用户一个“看得懂”的提醒,那么可以这么做:
在上述实验中文件打开了,但并没有执行关闭文件的命令,这是我们就可使用finally语句结束
raise语句
有些读者可能会思考,我的代码能不能自己抛出一个异常呢?答案是可以的,你
可以使用raise语句抛出一个异常:
抛出的异常还可以带参数,表示异常的解释:
三、总结
你不可能总是对的
我们是人,也会经常犯错,程序员也不例外,就算是经验丰富的码农,也不能保证
写的代码百分之百没有任何问题。另外,作为一个合格的程序员,在编程时要意识
到一点,那就是永远不要相信你的用户,所以要多方面考虑,这样写出来的程序才
会更加安全稳定。
那么既然程序总会出问题,那我们就需要用适当的方法去解决问题。程序出现逻辑错误或者用户输入不合法都会引发异常,但这些异常并不是致命的,不会导致程序崩溃,可以利用语言提供的异常处理机制,在异常出现的时候及时捕获,并从内部自我消化掉。