C++异常处理全攻略:捕获、栈展开与重新抛出
一. 异常
1.1 异常概念
在编程中,异常(Exception)是指在程序执行过程中出现的错误或意外情况,它会导致程序的正常流程中断,程序控制会转移到一个专门处理错误的区域。异常处理机制的主要目的是让程序在遇到错误时能够优雅地退出,或者做出相应的处理,避免程序崩溃。
1.2 异常的抛出和捕获
- 下面通过一个代码来演示该过程:
#include <iostream>
#include <stdexcept> // 包含标准异常类
using namespace std;
// 自定义异常类(继承自exception)
class NegativeValueError : public exception {
public:
const char* what() const noexcept override {
return "错误:不允许负值参数";
}
};
// 可能抛出异常的函数
double calculate_sqrt(double value) {
if (value < 0) {
throw NegativeValueError(); // 抛出自定义异常
}
return sqrt(value);
}
// 另一个可能抛出异常的函数
int divide_numbers(int a, int b) {
if (b == 0) {
throw runtime_error("错误:除数不能为零"); // 抛出标准异常
}
if (a % b != 0) {
throw "结果不是整数"; // 抛出原始字符串(不推荐,仅作演示)
}
return a / b;
}
int main() {
try {
// 测试平方根计算
cout << "计算sqrt(4): ";
cout << calculate_sqrt(4) << endl;
cout << "计算sqrt(-1): ";
cout << calculate_sqrt(-1) << endl; // 这里会抛出异常
// 测试除法运算
cout << "\n10 / 2 = ";
cout << divide_numbers(10, 2) << endl;
cout << "5 / 0 = ";
cout << divide_numbers(5, 0) << endl; // 这里会抛出异常
cout << "7 / 3 = ";
cout << divide_numbers(7, 3) << endl; // 这里会抛出异常
} catch (const NegativeValueError& e) { // 捕获自定义异常
cerr << "捕获到自定义异常: " << e.what() << endl;
} catch (const runtime_error& e) { // 捕获标准异常
cerr << "捕获到运行时错误: " << e.what() << endl;
} catch (const char* msg) { // 捕获原始字符串异常
cerr << "捕获到字符串异常: " << msg << endl;
} catch (...) { // 捕获所有其他异常
cerr << "捕获到未知异常" << endl;
}
cout << "\n程序继续执行..." << endl;
return 0;
}
输出结果:
计算sqrt(4): 2
计算sqrt(-1): 捕获到自定义异常: 错误:不允许负值参数
程序继续执行…
- 从该语句cout << calculate_sqrt(-1) << endl; 抛出(throw)异常后,后续代码不再继续执行,跳出try{}catch{}的作用域。
- 通过catch捕获抛出的异常,并对其进行处理。
- 抛出异常后,离抛出异常最近的那个类型对其进行处理。
- 抛出异常对象后,会生成一个异常对象的临时拷贝,catch执行完后会自动销毁它。
1.3 栈展开
栈展开(Stack Unwinding)是 C++ 异常处理机制中的核心概念之一。当异常被抛出且未被当前作用域捕获时,程序会沿着调用链逐层销毁局部对象(即栈上的对象),直到找到匹配的 catch 块为止。这个过程称为栈展开。
- 下面通过一个代码来总结一些特点:
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass(int id) : id_(id) {
cout << "对象 " << id_ << " 构造" << endl;
}
~MyClass() {
cout << "对象 " << id_ << " 析构" << endl;
}
private:
int id_;
};
void func3() {
MyClass obj3(3); // 局部对象
throw runtime_error("异常发生!");
}
void func2() {
MyClass obj2(2); // 局部对象
func3();
}
void func1() {
MyClass obj1(1); // 局部对象
func2();
}
int main() {
try {
func1();
} catch (const exception& e) {
cerr << "捕获异常: " << e.what() << endl;
}
cout << "程序继续执行..." << endl;
return 0;
}
输出结果:
对象 1 构造
对象 2 构造
对象 3 构造
对象 3 析构
对象 2 析构
对象 1 析构
捕获异常: 异常发生!
程序继续执行…
- 详细过程:
- 抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的catch语句,首先检查throw本身是否在try内部,如果在则寻找与之匹配的catch语句,若有则跳到catch的地方进行处理。
- 如果当前函数没有try/catch语句,或者有但类型不匹配,则退出当前函数,继续在调用该函数的函数中进行查找,这个过程成为栈展开。
- 如果到main函数之前还没有,程序会调用terminate终止程序运行。
- 如果找到,catch匹配的代码继续运行。
1.3.1 查找匹配的处理代码
- 如果有多个与抛出异常类型相同的catch语句,选择与栈展开过程与之最近的一个执行。
- 特殊情况:允许从非常量向常量的类型转换,即权限缩小;允许数组转换成指向数组元素类型的指针,函数转换成指向函数指针;允许派生类向基类进行转换,在继承体系中司空见惯。
- 抛出异常后如果到达main函数,我们是不希望程序终止的,所以一般main函数会使用catch(…),它可以捕获任意类型的异常,但不知道异常具体是什么。
上述文字说的很抽象,通过代码来讲解上述枯燥的文字。
- 示例代码:
#include <iostream>
#include <stdexcept>
using namespace std;
// 派生类转基类示例
class Base { virtual void foo() {} };
class Derived : public Base {};
// 自定义异常类
class MyException : public exception {
public:
const char* what() const noexcept override {
return "自定义异常:派生类转基类失败";
}
};
int main() {
try {
// 类型转换示例 1:非常量转常量
int num = 42;
const int& cnum = num;
// 类型转换示例 2:数组转指针
int arr[5] = { 1,2,3,4,5 };
int* ptr = arr;
// 类型转换示例 3:函数转指针
void (*funcPtr)(int) = [](int x) { cout << "Lambda: " << x << endl; };
funcPtr(10);
// 类型转换示例 4:派生类转基类
Derived d;
Base* b = &d;
// 异常抛出示例
if (b == nullptr) throw MyException();
throw runtime_error("运行时错误:空指针异常");
}
catch (const MyException& e) { // 精确捕获
cerr << "捕获特定异常: " << e.what() << endl;
}
catch (const exception& e) { // 标准异常捕获
cerr << "捕获标准异常: " << e.what() << endl;
}
catch (...) { // 兜底捕获(无法获取具体信息)
cerr << "捕获未知异常(无法获取详细信息)" << endl;
}
return 0;
}
exception是所有异常的基类,可以捕获任何异常。
输出结果:
Lambda: 10
捕获标准异常: 运行时错误:空指针异常
1.4 异常重新抛出
异常重新抛出(Re-throwing Exceptions)是 C++ 异常处理中的重要机制,允许在捕获异常后将其原样或修改后再次抛出。
语法:
try {
// 可能抛出异常的代码
} catch (const SomeException& e) {
// 预处理逻辑(如记录日志)
throw; // 重新抛出原始异常(保持类型不变)
}
示例代码:
#include <iostream>
#include <stdexcept>
using namespace std;
void process_data(int value) {
if (value < 0) {
throw invalid_argument("值不能为负数");
}
// 处理数据...
}
int main() {
try {
process_data(-5);//发生异常
} catch (const exception& e) {//处理异常
cerr << "底层捕获异常: " << e.what() << endl;
// 添加额外处理逻辑(如记录日志)
// throw; // 重新抛出原始异常(类型保持为 invalid_argument)
// 或者抛出新异常(类型改变)
throw runtime_error("数据预处理失败: " + string(e.what()));//异常重新抛出
}
return 0;
}
输出结果:
底层捕获异常: 值不能为负数
terminate called after throwing an instance of ‘std::runtime_error’
what(): 数据预处理失败: 值不能为负数
1.5 异常安全
抛出异常后,可能会导致资源没有机会被正确回收,从而导致内存泄漏。RAII模式可以完美解决该问题。析构函数抛出异常,也需谨慎处理。
1.6 异常规范
- 对于⽤⼾和编译器⽽⾔,预先知道某个程序会不会抛出异常⼤有裨益,知道某个函数是否会抛出异常有助于简化调⽤函数的代码。
- C++98中函数参数列表的后⾯接throw(),表⽰函数不抛异常,函数参数列表的后⾯接throw(类型1,类型2…)表⽰可能会抛出多种类型的异常,可能会抛出的类型⽤逗号分割。该设计模式过于复杂,C++11使用noexcept表示不会抛出异常。
- 如果一个函数已经抛出异常,而这个函数又被noexcept修饰,程序调用terminate终止程序。
- noexcept(expression)还可以作为⼀个运算符去检测⼀个表达式是否会抛出异常,可能会则返回false,不会就返回true。
二. 标准库异常
C++标准库也定义了⼀套⾃⼰的⼀套异常继承体系库,基类是exception,所以我们⽇常写程序,需要在主函数捕获exception即可,要获取异常信息,调⽤what函数,what是⼀个虚函数,派⽣类可以重写。通过基类指针或引用实现多态行为。
三. 最后
本文系统讲解了C++异常处理机制,涵盖异常概念、抛出捕获、栈展开、异常重新抛出、异常安全及标准异常体系等内容。通过代码示例演示了自定义异常、类型转换异常、资源管理等核心场景,强调RAII模式在异常安全中的关键作用,并对比了C++不同版本异常规范的演变。掌握这些机制可编写更健壮的C++程序,有效处理运行时错误,避免资源泄漏