C++异常处理try、catch 没有finally
程序的错误大致可以分为三种,分别是语法错误、逻辑错误和运行时错误:
1) 语法错误在编译和链接阶段就能发现,只有 100% 符合语法规则的代码才能生成可执行程序。语法错误是最容易发现、最容易定位、最容易排除的错误,程序员最不需要担心的就是这种错误。
2) 逻辑错误是说我们编写的代码思路有问题,不能够达到最终的目标,这种错误可以通过调试来解决。
3) 运行时错误是指程序在运行期间发生的错误,例如除数为 0、内存分配失败、数组越界、文件不存在等。C++ 异常(Exception)机制就是为解决运行时错误而引入的。
运行时错误如果放任不管,系统就会执行默认的操作,终止程序运行,也就是我们常说的程序崩溃(Crash)。
C++ 提供了异常(Exception)机制,让我们能够捕获运行时错误,给程序一次“起死回生”的机会,或者至少告诉用户发生了什么再终止程序。
捕获异常
我们可以借助 C++ 异常机制来捕获上面的异常,避免程序崩溃。捕获异常的语法为:
try{
// 可能抛出异常的语句
}catch(exceptionType variable){
// 处理异常的语句
}
这就好比,catch 告诉 try:你去检测一下程序有没有错误,有错误的话就告诉我,我来处理,没有就pass!
#include<iostream>
#include<exception>
using namespace std;
int main(int argc, char *argv[])
{
string str = "https://home.cnblogs.com/u/gschain/";
try {
char ch = str[100];
cout<<ch<<endl;
} catch (exception &e) {
cout<<"ch out of bound"<<endl;
}
try {
char ch1 = str.at(100);
cout<<ch1<<endl;
} catch (exception &e) {
cout<<"ch1 out of bound"<<endl;
}
return 0;
}
运行结果:
ch1 out of bound
可以看出,第一个 try 没有捕获到异常,输出了一个没有意义的字符(垃圾值)。因为[ ]不会检查下标越界,不会抛出异常,所以即使有错误,try 也检测不到。
换句话说,发生异常时必须将异常明确地抛出,try 才能检测到;如果不抛出来,即使有异常 try 也检测不到。所谓抛出异常,就是明确地告诉程序发生了什么错误。
第二个 try 检测到了异常,并交给 catch 处理,执行 catch 中的语句。需要说明的是,异常一旦抛出,会立刻被 try 检测到,并且不会再执行异常点(异常发生位置)后面的语句。本例中抛出异常的位置是第 17 行的 at() 函数,它后面的 cout 语句就不会再被执行,所以看不到它的输出。
说得直接一点,检测到异常后程序的执行流会发生跳转,从异常点跳转到 catch 所在的位置,位于异常点之后的、并且在当前 try 块内的语句就都不会再执行了;即使 catch 语句成功地处理了错误,程序的执行流也不会再回退到异常点,所以这些语句永远都没有执行的机会了。本例中,第 18 行代码就是被跳过的代码。
执行完 catch 块所包含的代码后,程序会继续执行 catch 块后面的代码,就恢复了正常的执行流。
抛出(Throw)--> 检测(Try) --> 捕获(Catch)
try{
throw "Unknown Exception"; //抛出异常
cout<<"This statement will not be executed."<<endl;
}catch(const char* &e){
cout<<e<<endl;
}
多级 catch
当异常发生时,程序会按照从上到下的顺序,将异常类型和 catch 所能接收的类型逐个匹配。一旦找到类型匹配的 catch 就停止检索,并将异常交给当前的 catch 处理(其他的 catch 不会被执行)。如果最终也没有找到匹配的 catch,就只能交给系统处理,终止程序的运行。
#include<iostream>
#include<exception>
using namespace std;
class Base{ };
class Derived: public Base{ };
int main(int argc, char *argv[])
{
try{
throw Derived(); //抛出自己的异常类型,实际上是创建一个Derived类型的匿名对象
cout<<"This statement will not be executed."<<endl;
}catch(int){
cout<<"Exception type: int"<<endl;
}catch(char *){
cout<<"Exception type: cahr *"<<endl;
}catch(Base){ //匹配成功(向上转型)
cout<<"Exception type: Base"<<endl;
}catch(Derived){
cout<<"Exception type: Derived"<<endl;
}
return 0;
}
输出结果:
Exception type: Base
catch 在匹配过程中的类型转换
C/C++ 中存在多种多样的类型转换,以普通函数(非模板函数)为例,发生函数调用时,如果实参和形参的类型不是严格匹配,那么会将实参的类型进行适当的转换,以适应形参的类型,这些转换包括:
算数转换:例如 int 转换为 float,char 转换为 int,double 转换为 int 等。
向上转型:也就是派生类向基类的转换,请查看《C++向上转型(将派生类赋值给基类)》了解详情。
const 转换:也即将非 const 类型转换为 const 类型,例如将 char * 转换为 const char *。
数组或函数指针转换:如果函数形参不是引用类型,那么数组名会转换为数组指针,函数名也会转换为函数指针。
用户自定的类型转换。
#include <iostream>
using namespace std;
int main(){
int nums[] = {1, 2, 3};
try{
throw nums;
cout<<"This statement will not be executed."<<endl;
}catch(const int *){
cout<<"Exception type: const int *"<<endl;
}
return 0;
}
运行结果:
Exception type: const int *
nums 本来的类型是int [3],但是 catch 中没有严格匹配的类型,所以先转换为int *,再转换为const int *。
throw用作异常规范
throw 关键字除了可以用在函数体中抛出异常,还可以用在函数头和函数体之间,指明当前函数能够抛出的异常类型,这称为异常规范(Exception specification),有些教程也称为异常指示符或异常列表。请看下面的例子:
double func (char param) throw (int);
这条语句声明了一个名为 func 的函数,它的返回值类型为 double,有一个 char类型的参数,并且只能抛出int类型的异常。如果抛出其他类型的异常,try将无法捕获,只能终止程序。
如果函数会抛出多种类型的异常,那么可以用逗号隔开:
double func (char param) throw (int, char, exception);
如果函数不会抛出任何异常,那么( )中什么也不写:
double func (char param) throw ();
如此,func() 函数就不能抛出任何类型的异常了,即使抛出了,try 也检测不到。
int disp() throw(int, int)
{
int i = 1;
if (i == 1) throw 1, 3;
return i;
}
C++语言本身或者标准库抛出的异常都是 exception 的子类,称为标准异常(Standard Exception)。你可以通过下面的语句来捕获所有的标准异常:
try{
//可能抛出异常的语句
}catch(exception &e){
//处理异常的语句
}
为何C++不提供“finally”结构
因为C++提供了另一种 机制,完全可以取代finally,而且这种机制几乎总要比finally工作得更好:就是——“分配资源即初始化”。
在C++中通常使用RAII,即Resource Aquisition Is Initialization.
就是将资源封装成一个类,将资源的初始化封装在构造函数里,释放封装在析构函数里。要在局部使用资源的时候,就实例化一个local object。
在抛出异常的时候,由于local object脱离了作用域,自动调用析构函数,会保证资源被释放