C++ 异常处理
在 C++ 中,异常处理(Exception Handling)提供了一种在运行时捕获、传播和处理错误的机制,使得错误处理逻辑与正常业务逻辑分离,提高代码可读性和健壮性。其核心由三部分构成:try
、throw
和 catch
,并辅以一系列语言和库特性。下面分章详解。
1. 异常的抛出:throw
- 使用
throw
语句抛出一个异常对象,类型可以是内建或自定义类型(但最好是继承自std::exception
)。 - 推荐抛出按值:
throw MyError("message");
,让异常对象在抛出时拷贝(或移动)到异常机制内部。
void readFile(const std::string& path) {
std::ifstream in(path);
if (!in) {
throw std::runtime_error("Cannot open file: " + path);
}
// ...
}
2. 异常的捕获:try
/ catch
- 在可能抛出异常的代码块外,用
try
括起;随后跟一组catch
分支,按声明顺序匹配异常类型。 - 捕获可按类型匹配,也可通过引用捕获以避免切片(object slicing)。
try {
readFile("data.txt");
} catch (const std::runtime_error& e) {
std::cerr << "运行时错误: " << e.what() << "\n";
} catch (const std::exception& e) {
std::cerr << "其他标准异常: " << e.what() << "\n";
} catch (...) {
std::cerr << "未知异常\n";
}
catch(...)
:捕获所有异常,但无法获取异常信息,只建议在最外层做兜底处理。- 捕获顺序:子类异常应放在基类之前,否则会被基类分支先捕获。
3. 异常类型设计
- 标准异常:C++ 标准库提供了一系列异常类型,均继承自
std::exception
:std::runtime_error
,std::logic_error
及其子类std::bad_alloc
,std::bad_cast
,std::out_of_range
等
- 自定义异常:继承自
std::exception
或其子类,并重写what()
提供错误信息。
class FileError : public std::runtime_error {
public:
explicit FileError(const std::string& msg)
: std::runtime_error("FileError: " + msg) {}
};
4. 异常传播与栈展开
- 当异常抛出后,控制流退出当前函数,开始 栈展开(stack unwinding):
- 调用当前函数的所有局部对象析构函数(RAII)
- 返回到调用者,继续寻找合适的
catch
- 如果到达
main()
也未被捕获,调用std::terminate()
,程序通常会异常终止。
5. 资源管理与 RAII
- RAII(Resource Acquisition Is Initialization)模式:将资源(内存、文件句柄、锁等)封装在对象构造/析构中管理,保证在异常发生时资源自动释放。
- 智能指针(
std::unique_ptr
/std::shared_ptr
)和标准容器都支持 RAII,几乎免除裸指针的资源泄漏风险。
void process() {
std::unique_ptr<FILE, decltype(&fclose)> fp(fopen("data.bin","rb"), &fclose);
if (!fp) throw FileError("open failed");
// 读写文件
} // 即使抛出异常,fclose(fp.get()) 会被自动调用
6. noexcept
规范
- C++11 引入
noexcept
关键字,用于标注函数 不会抛出异常。 - 优点:
- 编译器可进行更多优化(如移动而非拷贝)
- 在
noexcept
函数中抛出异常会调用std::terminate()
,确保行为一致。
void f() noexcept {
// 保证不抛
}
void g() noexcept(false) {
// 与不标注等价,可抛
}
- STL 容器和算法在必要移动操作上会优先选择
noexcept
移动构造函数,以免在容器扩容时抛出导致资源不一致。
7. 异常安全保障等级
在编写能抛出异常的函数时,常考虑以下三种异常安全级别:
安全级别 | 保证 | 示例 |
---|---|---|
基本保证 | 出错时不会泄漏资源,保持对象处于有效状态,可能是部分更新 | 向 std::vector push_back 失败后,不会泄漏内存,但元素可能少一个 |
强烈保证 | 要么成功,要么状态不变(事务性) | std::swap 通常提供强烈保证 |
不抛保证 | 绝不抛出异常 | 标注为 noexcept 的函数 |
设计时应努力提供至少 基本保证,在关键操作或简单辅助函数上争取 强烈保证。
8. 常见误区与注意事项
- 不要滥用异常做流程控制
异常开销高且会破坏控制流,应仅用于真正的“异常”情况,而非普通分支逻辑。 - 避免在析构函数中抛出异常
在栈展开过程中如果析构再抛异常,会导致std::terminate()
。若有可能,析构中捕获并处理所有异常。 - 捕获对象时宜按常量引用
catch (const MyException& e)
避免切片,并且不改变原异常对象。 - 保证基类有虚析构
对于需要通过基类指针删除的对象,基类应声明virtual ~Base()
.
小结
- 抛出:
throw
一个符合接口的异常对象; - 捕获:用
try
/catch
对分支捕获不同类型的异常,并按需处理或转抛; - RAII:结合构造/析构自动管理资源,确保在异常下资源不泄漏;
noexcept
:在不会抛出的函数上标注,提升性能和安全性;- 异常安全:设计时至少提供基本保证,关键操作争取强烈保证。
掌握并遵循上述原则,能让你的 C++ 异常处理既安全可靠,又不会让代码变得臃肿或难以维护。