一、异常对象的基本概念
异常对象是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;
}
关键结论:永远优先使用引用捕获异常对象,避免拷贝和切片。
三、异常对象的生命周期
异常对象的生命周期是最容易踩坑的点,核心规则如下:
- 创建时机 :
throw语句执行时,由运行时在异常存储区创建; - 销毁时机 :
- 如果
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()终止程序,不会创建异常对象。
五、总结
- 核心本质:异常对象是运行时在专用存储区创建的临时对象,承载异常信息,生命周期由异常处理机制管理,与栈/堆无关;
- 捕获原则 :优先使用
引用捕获(无拷贝、支持多态),避免值捕获导致的切片和性能开销; - 重新抛出 :用
throw;保留原异常对象,禁止throw e;(会创建新拷贝、丢失多态); - 避坑要点 :绝对不要抛出局部对象的引用/指针,防止悬垂引用;优先使用继承
std::exception的类作为异常对象。