C++异常对象

一、异常对象的基本概念

异常对象是C++异常处理机制的核心实体:

  • 定义 :当程序执行throw 表达式;时,表达式的结果会被用来创建一个异常对象 (exception object),它专门承载异常信息,在throw抛出点和catch捕获块之间传递错误数据。
  • 存储位置 :异常对象既不在栈上也不在堆上,而是由C++运行时(runtime)在专用的异常存储区(编译器管理的独立内存区域)创建,生命周期完全由异常处理机制控制。
  • 核心作用:隔离错误发生点和错误处理点,让错误信息能安全、可靠地跨函数/作用域传递。

二、异常对象的创建与传递

1. 异常对象的创建(throw阶段)

throw表达式的执行过程本质是创建异常对象的过程:

cpp 复制代码
throw 表达式;
  • 第一步:计算表达式的值(比如throw 10会计算出int类型的10,throw std::runtime_error("error")会构造一个std::runtime_error对象);
  • 第二步:运行时会根据表达式的类型,在异常存储区拷贝/移动构造一个新的异常对象(注意:不是直接使用表达式的原对象,原对象如果是局部变量,抛出后会销毁,但异常对象不受影响);
  • 第三步:暂停当前代码执行,跳转到匹配的catch块。

基础示例

cpp 复制代码
#include <iostream>
#include <stdexcept>
using namespace std;

void divide(int a, int b) {
    if (b == 0) {
        // 创建std::runtime_error类型的异常对象,存储"Divide by zero"
        throw runtime_error("Divide by zero"); 
    }
    cout << "Result: " << a / b << endl;
}

int main() {
    try {
        divide(10, 0);
    } catch (runtime_error& e) { // 捕获异常对象
        cout << "Error: " << e.what() << endl;
    }
    return 0;
}

输出:Error: Divide by zero

2. 异常对象的捕获(catch阶段)

catch块的核心是匹配并绑定异常对象,有两种捕获方式,差异极大:

捕获方式 语法示例 本质 优缺点
值捕获 catch (runtime_error e) 拷贝异常对象到catch的参数e 缺点:拷贝开销+切片问题(派生类异常会被截断为基类);优点:简单(不推荐)
引用捕获(推荐) catch (runtime_error& e) 直接绑定到异常存储区的原对象 优点:无拷贝开销+支持多态;缺点:无(C++异常处理的最佳实践)

值捕获的切片问题示例

cpp 复制代码
// 自定义派生异常类
class MyError : public runtime_error {
public:
    MyError(const string& msg) : runtime_error(msg) {}
    // 重写what(),体现多态
    const char* what() const noexcept override {
        return "MyError: Custom error";
    }
};

void throw_derived() {
    throw MyError("test"); // 抛出派生类异常对象
}

int main() {
    try {
        throw_derived();
    } catch (runtime_error e) { // 值捕获:切片为runtime_error
        cout << e.what() << endl; // 输出"test"(丢失派生类的多态信息)
    } catch (MyError& e) { // 引用捕获:匹配派生类
        cout << e.what() << endl; // 输出"MyError: Custom error"
    }
    return 0;
}

关键结论:永远优先使用引用捕获异常对象,避免拷贝和切片。

三、异常对象的生命周期

异常对象的生命周期是最容易踩坑的点,核心规则如下:

  1. 创建时机throw语句执行时,由运行时在异常存储区创建;
  2. 销毁时机
    • 如果catch块正常执行完毕(没有重新抛出),异常对象会被自动销毁;
    • 如果catch块中用throw;重新抛出,异常对象的生命周期会延续到下一个匹配的catch块;
    • 如果没有匹配的catch块,程序调用std::terminate()终止,异常对象也会被销毁。
1. 重新抛出异常对象(关键细节)

重新抛出有两种写法,效果天差地别:

cpp 复制代码
void rethrow_demo() {
    try {
        throw MyError("original error");
    } catch (MyError& e) {
        cout << "Caught: " << e.what() << endl;
        
        // 写法1:推荐!重新抛出原异常对象(无参数throw)
        throw; 
        
        // 写法2:错误!创建新的拷贝,丢失多态信息
        // throw e; 
    }
}
  • throw;:直接将原异常对象传递给外层catch,不创建新对象,保留所有多态信息;
  • throw e;:以e为模板创建新的异常对象 ,如果e是基类引用,会丢失派生类信息(切片)。
2. 禁止抛出局部对象的引用/指针(致命坑)

异常对象存储在专用区域,但如果抛出局部对象的引用/指针,会导致"悬垂引用/指针":

cpp 复制代码
void bad_throw() {
    string local_msg = "Invalid input";
    // 错误1:抛出局部对象的引用(local_msg出函数会销毁,异常对象的引用指向垃圾)
    // throw &local_msg; 
    
    // 错误2:抛出局部对象的指针(同理,指针悬空)
    // throw local_msg; // 正确:抛出拷贝(异常对象是string的拷贝,local_msg销毁不影响)
}

结论:只能抛出类对象(会被拷贝到异常存储区),绝对不能抛出局部对象的引用/指针。

四、异常对象的关键特性

1. 支持多态(核心价值)

C++标准库的异常体系(std::exception及其派生类)完全依赖异常对象的多态性:

cpp 复制代码
#include <iostream>
#include <stdexcept>
using namespace std;

// 自定义异常类(继承std::exception)
class FileError : public exception {
public:
    const char* what() const noexcept override {
        return "File not found";
    }
};

class NetworkError : public exception {
public:
    const char* what() const noexcept override {
        return "Network timeout";
    }
};

void do_something(int type) {
    if (type == 1) throw FileError();
    if (type == 2) throw NetworkError();
}

int main() {
    try {
        do_something(1);
    } catch (exception& e) { // 基类引用捕获所有派生类异常
        cout << "Error: " << e.what() << endl; // 输出"File not found"(多态)
    }
    return 0;
}

这是C++异常处理的核心用法:用基类引用捕获所有派生类异常,统一处理。

2. 类型限制
  • 异常对象不能是数组类型函数类型(如果抛出,会被自动转换为指针);
  • 推荐使用类类型 (尤其是继承std::exception的类)作为异常对象,而非内置类型(int/char*)------类类型可以携带更多错误信息(比如what()方法)。
3. noexcept与异常对象

如果函数声明为noexcept(表示不会抛出异常),但实际抛出了异常:

cpp 复制代码
void func() noexcept {
    throw runtime_error("error"); // 违反noexcept承诺
}

运行时会直接调用std::terminate()终止程序,不会创建异常对象

五、总结

  1. 核心本质:异常对象是运行时在专用存储区创建的临时对象,承载异常信息,生命周期由异常处理机制管理,与栈/堆无关;
  2. 捕获原则 :优先使用引用捕获(无拷贝、支持多态),避免值捕获导致的切片和性能开销;
  3. 重新抛出 :用throw;保留原异常对象,禁止throw e;(会创建新拷贝、丢失多态);
  4. 避坑要点 :绝对不要抛出局部对象的引用/指针,防止悬垂引用;优先使用继承std::exception的类作为异常对象。
相关推荐
baiduopenmap1 天前
【智图译站】GENREGION——高准确度、高可扩展的城市区域自动划分方法
开发语言·百度地图
蚰蜒螟1 天前
Redis网络层深度解析:数据如何写回客户端
java·开发语言·bootstrap
No0d1es1 天前
2025年12月 GESP CCF编程能力等级认证Python五级真题
开发语言·python·青少年编程·等级考试·gesp·ccf
风送雨1 天前
Go 语言进阶学习:第 2 周 —— 接口、反射与错误处理进阶
开发语言·学习·golang
福楠1 天前
模拟实现stack、queue、priority_queue
c语言·开发语言·数据结构·c++
峰上踏雪1 天前
Go(Golang)Windows 环境配置关键点总结
开发语言·windows·golang·go语言
我不是8神1 天前
go语言语法基础全面总结
开发语言·golang·xcode
No0d1es1 天前
2025年12月 GESP CCF编程能力等级认证Python一级真题
开发语言·python·青少年编程·gesp·ccf
小六子成长记1 天前
C++:map和set重点解析
开发语言·c++