目录
在C语言中有两种传统的错误处理机制:
- 强制终止程序,比如assert等,当发生诸如内存错误,除0错误时就会终止程序。缺陷:比较暴力,用户难以接受。
- 返回错误码,缺陷:需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通过把错误码放到errno中,表示错误。
也就是说,在C语言中基本都是使用返回错误码的方式处理错误,部分情况下使用终止程序处理非常严重的错误。
而在C++中通常采用“抛异常”的方式来处理错误。C++异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误。
C++异常的基本用法就是抛出异常(throw)和尝试捕获异常(try - catch),即一个异常的基础写法通常包含throw、try、catch这三个部分。其中,throw用于抛出异常;try,用于尝试捕获代码块中的异常;catch,用于匹配捕获到的不同类型的异常,并作出不同的反馈。
写法格式如下:
try
{
// try a throw
}
catch (ExceptionName e1)
{
// catch - e1
}
catch (ExceptionName e2)
{
// catch - e2
}
catch (...)
{
// catch - others
}
其中,catch(...)表示捕获其它任意类型的异常,通常用于捕获未知或者未设置的异常,以预防抛出异常而没捕获的情况发生。
而抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象。
用法示例如下:
void test()
{
// ...
throw "const char*型的异常"; // 常量字符串是const char*型的
}
int main()
{
try
{
// 其中,这里也可以直接throw抛出一个异常
test();
}
catch (const int errint)
{
cout << "int型的异常" << endl; // 抛出的是const char* 的异常,所以不会匹配到这里
}
catch (const char* errmsg)
{
cout << errmsg << endl; // 类型匹配,所以最终打印结果就是:const char*型的异常
}
catch (...)
{
cout << "未知异常" << endl; // 用于防止抛出异常而没捕获的情况发生。
}
return 0;
}
当我们的异常体系很小时,catch的类型与抛出的类型完全匹配是完全行得通的,但如果我们的异常体系变得庞大时,如果还是有一个throw的类型就写一个catch语句就会显得十分冗杂。
所以我们可以利用C++的多态机制来灵活地处理这种情况。catch一个父类引用,这样就可以仅用一个catch语句块就可以处理多种相同体系的异常问题了。例如:
// 基类异常
class baseException
{
public:
virtual string what()
{
return "baseException.";
}
};
// student子类异常
class studentException : public baseException
{
public:
virtual string what()
{
return "student error!";
}
};
// teacher子类异常
class teacherException : public baseException
{
public:
virtual string what()
{
return "teacher error!";
}
};
// 主函数,测试异常
int main()
{
try
{
throw studentException(); // try中抛出子类异常
}
catch (baseException& e) // 父类引用遇到子类对象形成多态
{
cout << e.what() << endl;
}
return 0;
}
C++中还支持异常的重新抛出,即在catch语句中继续抛出异常。那么为什么要重新抛出异常呢?例如考虑如下场景:(内容参考:C++中异常处理中的异常重新抛出的一种用法)
假设存在一个第三方库,我们需要使用自己的函数进行调用,有如下代码:
#include<string>
#include <iostream>
using namespace std;
/*第三方库中函数 void func(int i)
异常代码 -1:运行时错误
-2:数据超界异常
*/
void func(int i)
{
if(i<0)
throw -1;
if(i>100)
throw -2;
}
int main()
{
try
{
func(199);
}
catch (int i)
{
cout<<"Error Code: "<<i<<endl;
}
return 0;
}
//运行结果为 Error Code: -2
此时我们根本不知道-2代表什么意思,只能去查找函数的手册,不仅麻烦,而且不直观。所以我们可以考虑对异常重新抛出,那么改进后的代码如下:
#include<string>
#include <iostream>
using namespace std;
/*第三方库中函数 void func(int i)
异常代码 -1:运行时错误
-2:数据超界异常
*/
void func(int i)
{
if(i<0)
throw -1;
if(i>100)
throw -2;
}
//这是我们自己的库。调用第三方库的函数void func(int i)
void myFunc(int i)
{
try
{
func(i);
}
catch(int i)
{
switch(i)
{
case -1:
throw "Runtime Error";
break;
case -2:
throw "Data Error";
break;
}
}
}
int main()
{
try
{
myFunc(199);
}
catch (const char *s)
{
cout<<"Error Code: "<<s<<endl;
}
return 0;
}
//结果输出为 Error Code: Data Error
当执行到throw语句时,首先会检查当前的throw语句是否在try块内部,如果是,就在当前函数栈中查找匹配的catch语句。如果匹配到了则直接跳到catch的地方执行。如果没有相匹配的catch块,则退出当前函数栈,在上层函数栈帧中继续查找尝试匹配。如果到达main函数的栈,都没有匹配的catch,就会终止程序,有些编译器还会报错。例如:
上述沿着调用链查找匹配的catch块的过程叫栈展开或者栈解旋。也就是说异常被抛出后,从进入try块起,到异常被抛掷前(遇到throw之前),这期间在栈上构造的所有对象,都会被自动析构,其中析构的顺序与构造的顺序相反。其原因自然就与函数栈帧的开辟与释放分不开了,其具体细节就不再过多的阐述了。
需要注意的是,throw在一个函数中的效果和return有些类似。当执行到throw语句之后就会立即去寻找匹配的try语句块并跳到对应的catch语句块中,就不会再执行throw后续的代码了。
一般来说,为了代码的可读性与规范型,抛出异常的函数通常需要在函数声明部分,以throw(...)的形式声明抛出异常的类型(声明和定义分开时,两个都可以写声明throw部分)示例如下:
// 这里表示这个函数会抛出A、B、C、D中的某种类型的异常
void fun() throw(A,B,C,D);
// 这里表示这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);
// 这里表示这个函数不会抛出异常
void* operator new (std::size_t size, void* ptr) throw();
void* operator new (std::size_t size, void* ptr) noexcept; // C++11支持
其中,早期用throw()来表示没有异常抛出,C++11之后可以用noexcept代替。
函数的异常抛出声明并不影响函数的任何功能,即它并没有实质性地影响函数的运行,只是一个便于代码阅读的声明。也就是说,如果实际抛出的异常和声明部分的类型不对应,并不会导致任何运行和编译错误。不过还是要保持统一的,因为如果不保持统一,那么这个异常抛出声明不但没有提高效率,反而还可能会造成很多麻烦。
实际中,并不是我们想抛什么异常就抛什么异常,这样会导致捕捉的时候不好捕捉。而是会建立一个异常体系,结合多态的性质,在抛出异常时,只需要用基类进行捕捉即可。
其中,在C++库中也建立了一个异常体系。也给我们提供了一些异常类。我们可以在程序中使用这些标准异常,它们就是以父子类的层次结构组织起来的(图片摘自:C++ 异常处理 | 菜鸟教程)
说明如下:
- try和catch语句块不能省略后面的大括号,且try和catch之间不能有其它语句。
- try和catch必须匹配使用,即如果只有try没有catch就会报错。
- catch(...)表示捕获其它任意类型的异常,通常用于捕获未知或者未设置的异常,以预防抛出异常而没捕获的情况发生。
- 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象。
- 被选中的处理代码的调用链是,找到类型匹配且离抛出异常位置最近的catch语句块。
- 捕获是根据抛出的类型进行捕获的,捕获之后可以继续抛出新的异常。
- 如果抛出了异常但没捕获,程序会异常终止。而如果没有捕获到异常则会跳过整个异常捕获部分,包括catch(...)语句块
- throw与return类似,异常抛出后会立即结束try块与原函数,所以throw后面部分的代码不会被执行。
- 实际中抛出和捕获的类型不一定要类型完全匹配,可以抛出派生类对象,使用基类引用来捕获,这个在实际生活中很实用。