目录
一、C语言处理错误
1、终止程序
2、返回错误码
二、C++异常
异常的抛出和匹配原则
异常安全
异常规范
异常优缺点
传统处理错误的两种方式:
使用assert是直接终止程序,包含头文件assert.h即可
缺点:出现错误直接终止程序,用户使用起来比较难受
这里的错误码在Linux中用的比较多,Linux底层就是用C语言写的
缺点:错误码比较多,出现错误后需要自己去查找对应的错误,
异常是一种处理错误的方式
异常需要用到下面三个关键字:throw、catch、try
throw:当问题出现时,程序会抛一个异常
catch:catch是捕捉异常的类型
try:try是捕获的一段区域,后面跟着一个或多个catch块
下面举个简单的除数为0抛异常的例子:
先执行main函数,进入try,执行Test(),输入a、b,再进入Div函数中,判断除数即b是否为0,如果为0,则直接throw错误信息到main函数的catch语句中,打印出来:
如果输入的是正常数字,那么进入Div函数后,执行else语句,再返回到Test函数,再返回到main函数,接下来不执行catch语句,直接跳过catch语句,到return 0:
1、异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。
例如上面的代码如果多一个catch(int err) { cout << err << endl; },现在有两个catch语句,编译器会根据throw的类型决定进入那个catch语句
2、被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
如果改变一下上面的代码:
我们在Test函数中,也加了try,catch语句,这时如果出现除0错误,throw会往回找,谁在这个调用链中离抛异常位置最近,就执行谁的catch语句,结果如下:
可见执行的是Test函数的catch语句
但是需要注意的是,如果Test函数有catch语句,但是与throw的类型不匹配,这时也不会执行Test的catch语句,需要类型匹配
如果出现特殊情况,没有一个函数中的catch与throw的类型匹配,则会直接报错,终止程序,所以异常是必须被捕获的
3、catch(...)可以捕获任意类型的异常,缺点是不知道异常错误是什么。
由于上面说到的,异常必须被捕获,但是担心出现未捕获的异常而导致程序直接崩溃,所以给最后加了一个保险的catch语句,即:catch(...),用于捕获任意类型的异常,当然优先捕获前面的异常,都匹配不上,才会匹配catch(...)
输入5 0,发生除0错误,但是没有类型匹配的catch语句,所以执行catch(...),打印出了未知错误
4、抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象, 所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。
抛异常一般都会抛一个对象,这个对象一般都是被定义出来的,假设自己写一个Exception类,里面有错误信息与错误编号:
运行结果如下,既能打印出错误信息,又能打印出错误编号:
而main函数中的const Exception& e,也并不是引用的Div函数中创建的Exception e("发生除0错误", 2)这个e对象
因为这个e对象是临时对象,出了作用域会被销毁,所以main函数中引用的是Exception e这个e对象的拷贝对象
5、实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类(子类)对象, 使用基类(父类)捕获
之所以有这个原则,是因为如果一个项目非常大,分了很多个小组做这个项目,每个小组都抛一个异常,那catch这里语句会非常多,不利于使用,所以引出了这个原则,在公司中非常有用
抛派生类对象,用基类捕获,前面学过赋值兼容转换,可以让派生类进行切割/切片处理,从而让基类捕获,所以不管多少人想要抛异常,只要继承统一的一个基类,这时只需要捕获这个基类就达到目的了
try中先调用TestAdd,然后抛AddErr异常
这里的what使用了多态的语法,父类子类都实现了what
抛异常的是子类用父类捕获,子类对象进入catch (const Exception& e)中,切片处理,调用子类的what函数,结果为:
不要在构造函数中抛异常,否则可能导致对象不 完整或没有完全初始化
不要在析构函数内抛出异常,否则可能导致资源泄漏
C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,死锁等问题,C++经常使用RAII来解决以上问题,关于RAII在接下来的智能指针博客进行讲解。
C++98有下面的异常规范,需要注意的是这里的异常规范并不是强制的
①异常规范是为了让使用者知道函数可能抛出的异常有哪些,例如:
void test() throw(x, y);
表示test函数,可能会抛x/y的异常
②函数的后面接throw(),表示函数不抛异常
void test() throw();
表示test函数,不会抛异常
③若无异常接口声明,则此函数可以抛掷任何类型的异常
因为异常规范并不是强制的
④C++11新增关键字noexcept,函数的后面接throw(),表示函数不抛异常:
void test() noexcept;
表示test函数,不会抛异常
优点:
1、异常相比错误码的方式可以清晰准确的展示出错误的各种信息,可以帮助我们更好的定位程序的bug。
2、返回错误码的传统方式如果返回了错误,那么我们得层层返回错误,最外层才能拿到错误,而如果是异常体系,抛出的异常异常会直接跳到main函数中catch捕获的地方,main函数会直接处理错误。
3、很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们也需要使用异常。
4、部分函数使用异常更好处理,比如引用返回的函数(类似T&),不使用异常的话,出现越界等错误时只能终止程序
缺点:
1、异常会导致程序的执行流乱跳,因为throw不会一层层的执行
2、C++没有垃圾回收机制,资源需要自己管理。容易导致内存泄漏、死锁等异常安全问题,需要使用RAII来处理资源的管理问题
3、C++标准库的异常体系定义得不好,所以大家各自定义各自的异常体系,非常的混乱。
4、异常尽量规范使用,否则捕获时非常混乱
总而言之:异常是利大于弊的,是建议使用时规范使用