【C++进阶篇】走进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): 捕获到自定义异常: 错误:不允许负值参数
程序继续执行…

  1. 从该语句cout << calculate_sqrt(-1) << endl; 抛出(throw)异常后,后续代码不再继续执行,跳出try{}catch{}的作用域。
  2. 通过catch捕获抛出的异常,并对其进行处理。
  3. 抛出异常后,离抛出异常最近的那个类型对其进行处理。
  4. 抛出异常对象后,会生成一个异常对象的临时拷贝,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 析构
捕获异常: 异常发生!
程序继续执行…

  • 详细过程:
  1. 抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的catch语句,首先检查throw本身是否在try内部,如果在则寻找与之匹配的catch语句,若有则跳到catch的地方进行处理。
  2. 如果当前函数没有try/catch语句,或者有但类型不匹配,则退出当前函数,继续在调用该函数的函数中进行查找,这个过程成为栈展开。
  3. 如果到main函数之前还没有,程序会调用terminate终止程序运行。
  4. 如果找到,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 异常规范

  1. 对于⽤⼾和编译器⽽⾔,预先知道某个程序会不会抛出异常⼤有裨益,知道某个函数是否会抛出异常有助于简化调⽤函数的代码。
  2. C++98中函数参数列表的后⾯接throw(),表⽰函数不抛异常,函数参数列表的后⾯接throw(类型1,类型2…)表⽰可能会抛出多种类型的异常,可能会抛出的类型⽤逗号分割。该设计模式过于复杂,C++11使用noexcept表示不会抛出异常。
  3. 如果一个函数已经抛出异常,而这个函数又被noexcept修饰,程序调用terminate终止程序。
  4. noexcept(expression)还可以作为⼀个运算符去检测⼀个表达式是否会抛出异常,可能会则返回false,不会就返回true。

二. 标准库异常

C++标准库也定义了⼀套⾃⼰的⼀套异常继承体系库,基类是exception,所以我们⽇常写程序,需要在主函数捕获exception即可,要获取异常信息,调⽤what函数,what是⼀个虚函数,派⽣类可以重写。通过基类指针或引用实现多态行为。
在这里插入图片描述

三. 最后

本文系统讲解了C++异常处理机制,涵盖异常概念、抛出捕获、栈展开、异常重新抛出、异常安全及标准异常体系等内容。通过代码示例演示了自定义异常、类型转换异常、资源管理等核心场景,强调RAII模式在异常安全中的关键作用,并对比了C++不同版本异常规范的演变。掌握这些机制可编写更健壮的C++程序,有效处理运行时错误,避免资源泄漏

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值