1. 异常的概念
异常处理机制是C++中一种强大的错误处理方式。它允许程序中独立开发的部分,在运行时针对出现的问题进行通信并做出相应的处理。异常将问题的检测与问题的解决过程分离开来:程序的一部分负责检测错误,然后将解决问题的任务传递给程序的另一部分。检测环节无需知道问题处理模块的所有细节。
与C语言主要通过错误码(对错误信息进行分类编号)来处理错误不同,C++的异常机制是抛出一个对象。这个对象可以包含比简单的错误码更全面、更丰富的信息,使得错误处理更加灵活和强大。
2. 异常的抛出与捕获
在C++中,当程序出现问题时,我们通过 throw 一个对象来引发一个异常。被选中的异常处理代码(catch 块)是由调用链中与抛出对象类型匹配,且距离抛出位置最近的那一个决定的。当 throw 语句执行时,其后的代码将不再被执行。程序的执行流会直接从 throw 的位置,跳转到与之匹配的catch模块。控制权的转移可能导致调用链中的函数提前退出。同时,在沿着调用链查找匹配的 catch 块的过程中,沿途创建的所有局部对象都会被销毁(这一过程称为栈展开 )。由于抛出的异常对象可能是一个局部对象,因此系统会生成该对象的一个拷贝,这个拷贝对象将在对应的catch 子句执行完毕后被销毁。这个过程类似于函数的传值返回。
示例代码:基本的异常抛出与捕获
下面是一个简单的示例,演示了异常如何在多层函数调用中被抛出和捕获。
cpp
double Divide(int a, int b)
{
// 当 b == 0 时抛出异常
if (b == 0)
{
string s("Divide by zero condition!");
throw s; // 抛出 string 类型的异常对象
}
else
{
return ((double)a / (double)b);
}
}
void Func()
{
int len, time;
cin >> len >> time;
try
{
cout << Divide(len, time) << endl;
}
catch (const char* errmsg) // 尝试捕获 const char* 类型的异常
{
cout << errmsg << endl;
}
cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
}
int main()
{
while (1)
{
try
{
Func(); // 调用可能抛出异常的函数
}
catch (const string& errmsg) // 捕获 string 类型的异常
{
cout << errmsg << endl;
}
catch (...) // 捕获任意类型的异常,通常放在最后作为"兜底"
{
cout << "未知异常" << endl;
}
}
return 0;
}
分析:
Divide 函数中,如果除数为0,会抛出一个 string 类型的异常。
Func 函数中,它只尝试捕获 const char* 类型的异常,因此无法处理 Divide 抛出的 string 异常。由于在 Func 内部没有找到匹配的 catch,异常会沿着调用链继续向上传递到 main 函数。最终,main 函数中的 catch (const string& errmsg) 成功捕获并处理了这个异常。
3. 栈展开
抛出异常后,程序会暂停当前函数的执行,并开始寻找与之匹配的 catch 子句,这个过程就是栈展开。
当前函数查找 :首先检查 throw 语句本身是否位于 try 块内部。如果是,则在该 try 块后面查找匹配的 catch 子句。
向上传递 :如果当前函数中没有找到匹配的 catch,或者有 try/catch 但类型不匹配,则当前函数会终止,其局部对象会被销毁,然后控制权返回给调用它的上层函数。
重复查找 :在上层函数中,重复步骤1的查找过程。如果仍然找不到,就继续向上层传递。
终止程序 :如果最终到达 main 函数,依然没有找到匹配的 catch 子句,程序会调用标准库的 terminate 函数终止运行。
异常处理 :如果在某一层找到了匹配的 catch 子句,程序会跳转到该 catch 块中执行处理代码。
4. 查找匹配的处理代码
在查找匹配的 catch 子句时,默认要求抛出对象的类型与 catch 声明的类型严格匹配。但存在几种例外情况,这些例外使得异常处理更加灵活:
从非const向const的转换 :允许将抛出的非const对象,匹配到接收const引用的 catch 子句(权限缩小)。
数组/函数到指针的转换 :允许将数组类型转换为指向数组元素类型的指针,或将函数类型转换为指向函数的指针。
派生类向基类的转换 :允许将派生类对象匹配到接收基类类型的 catch 子句。这是面向对象编程中非常实用的一种方式,通常用于设计异常类体系。
最佳实践 :在 main 函数的最后,通常建议添加一个 catch(...) 子句。它可以捕获任意类型的异常,防止因未捕获的异常而导致程序意外终止。虽然它无法获取具体的错误信息,但能保证程序最基本的健壮性。
5. 异常重新抛出
有时,我们在 catch 到一个异常对象后,可能需要对错误进行分类处理。对于某些特定类型的错误,我们可能希望在当前函数中进行特殊处理;而对于其他类型的错误,则希望将其重新抛出,交给外层的调用链去处理。
重新抛出一个捕获到的异常非常简单,只需要在 catch 块中直接使用 throw; 语句即可。它会将当前捕获的异常对象原封不动地继续向上抛出。
cpp
try {
// 一些可能抛出多种异常的代码
}
catch (int errCode) {
if (errCode == 1) {
// 对错误码为1的情况进行特殊处理
cout << "处理特定错误" << endl;
} else {
// 其他错误码,重新抛出,交给上层处理
throw;
}
}
catch (...) {
// 对未知异常进行记录或转换后,也可以选择重新抛出
cout << "记录未知异常" << endl;
throw; // 重新抛出
}
总结:C++的异常处理机制通过 try、throw 和 catch 三个关键字,提供了一种结构化的、类型安全的错误处理方式。它能够将错误检测与错误处理分离,并通过栈展开和异常重新抛出等特性,为构建健壮的程序提供了坚实的基础。