异常的引出
如过你写过不少的程序的话,相信你应该遇到过一些程序所不能处理的错误而导致程序崩溃的问题吧,比如说:操作野指针,访问空指针,函数的除零错误,数组越界,在栈上开辟空间过大导致栈溢出等等……对于我们人来说,犯错并不可怕,可怕的是不改正错误,对于程序来说也是如此。程序执行的时候,难免会发生这样或者那样的错误,要想改正错误,所使用的编程语言得提供处理错误的机制;
学习过C语言的人都知道,C语言中的错误处理的方式有 终止程序 和 返回错误码;这两种方式确实可以处理程序中的大量错误,但是C语言的这两种处理错误的方式都有点不足;
- 比如说 终止程序这种方式,当程序中发生错误的时候,直接就终止整个程序,但是我们希望我们的程序在发生错误的语句之后还要执行,比如说我们还需要释放内存空间,直接终止程序导致内存空间没有释放,造成内存泄漏的问题,如果这是一个服务器程序,就会导致服务器资源紧缺,服务器运行越来越慢,具有卡死的风险。
- 再或者说返回错误码这种方式,程序员需要自己根据返回的错误码去 错误码表中找对应的错误 ,如果返回的错误码比较多,查找起来是一件很难受的事情。说白了就是,返回错误码的方式不直接,程序员不能直接清楚的知道对应的错误是什么。
基于C语言中对错误处理的种种不便,C++语言呢 完善了对错误的处理机制(C++语言本来就是基于C语言所产生的,C++语言要兼容C语言的绝大部分语法),这种新的处理错误的机制就是异常。
如何使用C++中的异常机制
C++中要想使用异常来处理错误,需要通过 try、catch、throw 三个关键字来进行
- try 具有尝试的意思,后面跟一个代码块,意思是尝试一下代码块中的代码,如果该代码块中抛出了异常,就交给类型匹配的 catch 代码块处理。如果没有抛出异常,就执行 catch 代码块后面的语句。
- catch 具有捕捉,抓住的意思,catch后面跟一个圆括号,圆括号后面跟一个花括号;圆括号中的内容表示捕捉的异常的类型,花括号中就可以处理该异常了;try代码块后面可以跟多个catch 代码块。(注意:捕捉的异常类型 和抛出的异常类型 是匹配的,这里的匹配有两种匹配方式 1、抛出A类型的异常,被接收A类型异常的catch语句捕获;2、抛出子类类型的异常,被接收该父类类型的catch语句捕获。)
- throw 具有扔、抛的意思,throw 后面跟抛出的异常对象。抛出的异常对象一旦被捕获,程序就跳转到捕获异常的catch代码块中执行,如果到main函数中都还不能被捕获,则终止程序。
#include <iostream>
using namespace std;
int func(int &a, int &b)
{
if(0 == b)
throw "这是一个除零错误";
int c = a / b;
return c;
}
int main()
{
try
{
int a = 1;
int b = 0;
func(a, b);
}
catch(const char* err_message)
{
cout << err_message << endl;
}
catch(...) // ... 表示捕获任意类型的异常
{
cout << "未知异常" << endl;
}
return 0;
}
总的来说就是,C++中的异常是通过对象的形式抛出来的,该对象的类型决定了 匹配哪个 catch 代码块 来处理,匹配的catch代码块是 函数调用链中与该对象类型匹配 且 离抛出异常位置最近的那一个。那抛出的异常对象到底是什么呢?异常对象是一个局部对象,局部对象出了作用域就会销毁,所以抛出该局部对象的时候,会生成一个临时变量,这个临时变量会在被捕获后销毁;这里类似于函数的传址返回,可以看出右值引用的移动语义也有提高抛出异常的效率。
函数调用链中异常栈是如何展开和匹配的
异常的使用难免弯弯绕绕,所以我们要搞清楚异常是如何匹配的,说白了就是通过 throw 抛出的异常对象应该被哪个catch代码块捕获处理。
当通过 throw 抛出异常对象的时候,首先要检查 throw 本身是否在 try 代码块内部,如果在的话,再去找匹配的 catch 代码块,如果找到了匹配的 catch 代码块的话,就跳到 匹配的 catch 代码块中执行,如果没有找到匹配的 catch 代码块,就会退出当前函数栈,在上一层函数栈中继续寻找匹配的 catch 代码块。如果返回到 main 函数的函数栈中依旧没有找到对应的catch代码块,就会终止该程序。所以为了保险起见,通常会在最后加一个 catch(...) 表示捕获任意类型的异常,避免程序终止。执行完 catch 代码块中的内容之后,会继续执行 catch 代码块后面的内容。
异常的重新抛出
异常为什么需要重新抛出呢?
当一个函数捕获到它不能或不应该处理的异常时,重新抛出这个异常可以确保错误信息被传递到能够处理该异常的代码中去。这样,调用者有机会捕获并处理这个异常,或者进一步地将它传递给它的调用者。
异常如何重新抛出?
直接在捕获异常对象的 catch 代码块中使用 throw 关键字,直接上代码:
#include <iostream>
using namespace std;
int func2(int &a, int &b)
{
if(0 == b)
throw "这是一个除零错误";
int c = a / b;
return c;
}
void func1()
{
int a = 1;
int b = 0;
try
{
func2(a,b);
}
catch(...)
{
cout << "重新抛出异常" << endl;
throw; // 使用throw关键字重新抛出异常
}
}
int main()
{
try
{
func1();
}
catch(const char* err_message)
{
cout << err_message << endl;
}
return 0;
}
使用异常可能存在的问题
众所周知,程序是按照一定的执行流执行的,但总有一些操作试图打破程序的执行流,比如:continue语句,break语句,goto语句(不推荐使用),return语句;在加上我们刚刚学习的 try…catch语句 也是可以打破程序的执行流的,continue、break、return语句都是可控的,但是 try…catch语句 会直接跳转到匹配的 catch 代码块中执行,这也就意味着处于 throw 和该catch代码块之间的语句不会被执行,如果这是一些涉及资源释放的语句的话,就会造成资源没有被释放,从而导致内存泄漏的问题。如果是多线程的代码,异常的抛出导致 unlock() 未被执行,就会导致死锁问题。如何解决呢?通过智能指针解决。
由此可见,对于异常的使用需要小心小心再小心。而且在一些场景中是不能抛异常的,比如,不要在 构造函数 和 析构函数 中抛异常。
- 不要在构造函数中抛异常:构造函数是用来初始化对象的,如果在构造函数中抛异常,可能导致对象初始化不完整,影响后续的使用。
- 不要在析构函数中抛异常:析构函数是用来清理对象中的资源的,如果在析构函数中抛异常,可能导致对象中资源清理不完全,如果析构函数中需要 释放资源or解锁,就会造成内存泄漏or死锁的问题。
异常的优缺点
优点:
1.异常可以更加清晰的展示出错误的信息,可以更好的帮助我们定位程序中的bug。
-
在实际开发中,通常会自定义一套异常体系,定义一个基类,不同的模块可能会抛出的异常定义为派生类,这样,抛出的异常是哪个模块的就清晰明了了。
2.处理错误速度更快
-
异常体系中,一旦抛出异常,程序直接跳转到匹配的 catch 代码块中执行;如果是返回错误码的方式的话,需要层层返回,在最外层才能拿到错误码。
3.弥补了一些场景下处理错误的不足
- 比如:有些函数没有返回值,不方便使用错误码返回错误信息。
缺点
1.try catch语句会造成程序的执行流乱跳,导致调试和分析问题时比较困难;也有可能导致不可预料的问题(在使用异常可能会存在的问题中已经分析过了)
2.C++中的异常体系定义的不好,大家自己定义自己的异常体系,非常混乱。
3.异常要是使用不规范的话,会增加而外的使用成本。