C++函数异常处理是管理程序运行时错误的重要机制,异常安全代码则要求程序在抛出异常后,不会出现资源泄漏、数据损坏或者状态不一致的问题。合理运用函数异常处理的相关特性,能够有效提升代码的异常安全性。

异常安全代码的三个基本保证
在讨论函数异常处理的应用前,需要先明确异常安全代码的三个核心保证,这是编写异常安全代码的目标:
- 基本保证:异常抛出后,程序的所有对象仍处于有效状态,没有资源泄漏,不会出现未定义行为。
- 强保证:异常抛出后,程序的状态会回滚到函数调用前的状态,就像函数从未被调用过一样。
- 不抛异常保证:函数本身不会抛出任何异常,调用该函数时不需要考虑异常处理逻辑。
函数异常处理的核心机制
C++的函数异常处理主要包含try、catch块,以及函数异常规范两部分,这两部分是支撑异常安全代码的基础。
try-catch块的使用
try块中放置可能抛出异常的代码,catch块用于捕获并处理对应类型的异常。在异常安全代码中,try-catch块通常用于释放已经获取的资源,或者回滚已经执行的状态修改。
#include <iostream>
#include <stdexcept>
void process_data(int* data, int size) {
// 先分配临时内存
int* temp = new int[size];
try {
for (int i = 0; i < size; ++i) {
if (data[i] < 0) {
// 遇到非法数据抛出异常
throw std::invalid_argument("数据包含负数");
}
temp[i] = data[i] * 2;
}
// 处理完成后拷贝回原数组
for (int i = 0; i < size; ++i) {
data[i] = temp[i];
}
} catch (...) {
// 捕获所有异常,先释放临时内存再重新抛出
delete[] temp;
throw;
}
// 正常流程下释放临时内存
delete[] temp;
}
int main() {
int arr[] = {1, 2, -3, 4};
try {
process_data(arr, 4);
} catch (const std::exception& e) {
std::cout << "捕获到异常: " << e.what() << std::endl;
}
return 0;
}
上面的代码中,try块内如果抛出异常,会先进入catch块释放临时申请的temp内存,再重新抛出异常,避免了内存泄漏,满足了异常安全的基本保证。
函数异常规范
C++11之后引入了noexcept关键字作为函数异常规范,用来标记函数是否可能抛出异常。如果函数承诺不会抛出异常,就标记为noexcept,这能帮助编译器做更多优化,同时也能明确函数的异常安全级别。
// 该函数不会抛出任何异常,满足不抛异常保证
void swap_int(int& a, int& b) noexcept {
int temp = a;
a = b;
b = temp;
}
// 该函数可能抛出std::bad_alloc异常
int* create_array(int size) {
return new int[size]; // new操作可能抛出std::bad_alloc
}
RAII对异常安全代码的辅助作用
RAII(资源获取即初始化)是C++中实现异常安全的重要技术,其核心是将资源的管理和对象的生命周期绑定。当对象离开作用域时,析构函数会自动被调用,释放对应的资源,即使中途抛出异常也不会遗漏资源释放。
结合函数异常处理,RAII能大幅简化异常安全代码的编写,不需要手动在catch块中释放资源。
#include <memory>
#include <stdexcept>
#include <vector>
class DataProcessor {
private:
std::vector<int> buffer;
public:
DataProcessor(int size) : buffer(size) {}
void process() {
for (int i = 0; i < buffer.size(); ++i) {
if (i == 5) {
throw std::runtime_error("处理到第5个元素时出错");
}
buffer[i] = i * 3;
}
}
};
void run_process() {
// std::unique_ptr管理的资源会在离开作用域时自动释放,即使process抛出异常也没问题
std::unique_ptr<DataProcessor> processor = std::make_unique<DataProcessor>(10);
processor->process();
}
int main() {
try {
run_process();
} catch (const std::exception& e) {
std::cout << "异常信息: " << e.what() << std::endl;
}
return 0;
}
上面的代码中,即使process函数抛出异常,processor指向的对象也会在离开run_process函数作用域时被析构,内部的buffer资源会自动释放,不需要手动写catch块处理资源释放逻辑。
函数异常处理实现强保证的示例
强保证要求函数抛出异常后,程序状态回滚到调用前的状态。结合copy-and-swap惯用法和函数异常处理,可以实现强保证。
#include <iostream>
#include <stdexcept>
#include <algorithm>
class SafeArray {
private:
int* data;
int size;
public:
SafeArray(int sz) : size(sz), data(new int[sz]) {}
// 拷贝构造函数
SafeArray(const SafeArray& other) : size(other.size), data(new int[other.size]) {
std::copy(other.data, other.data + other.size, data);
}
// 拷贝赋值运算符,使用copy-and-swap实现强保证
SafeArray& operator=(SafeArray other) noexcept {
swap(*this, other);
return *this;
}
// swap函数,标记为noexcept,满足不抛异常保证
friend void swap(SafeArray& first, SafeArray& second) noexcept {
using std::swap;
swap(first.size, second.size);
swap(first.data, second.data);
}
void modify(int index, int value) {
if (index < 0 || index >= size) {
throw std::out_of_range("索引越界");
}
// 先修改临时副本,再交换,确保修改失败时不改变原对象状态
SafeArray temp(*this);
temp.data[index] = value;
swap(*this, temp); // swap是noexcept的,不会抛出异常
}
~SafeArray() {
delete[] data;
}
};
int main() {
SafeArray arr(5);
try {
arr.modify(10, 100); // 索引越界,抛出异常
} catch (const std::exception& e) {
std::cout << "修改失败: " << e.what() << std::endl;
}
return 0;
}
上面的modify函数先创建当前对象的副本,修改副本后再通过noexcept的swap函数交换,即使修改过程中抛出异常,原对象的状态也不会被改变,满足了异常安全的强保证。
函数异常处理的注意事项
- 不要在析构函数中抛出异常,否则如果前一个异常还在处理中,会导致程序直接调用
std::terminate终止。 - 优先使用RAII管理资源,减少手动在catch块中释放资源的逻辑,降低出错概率。
- 对明确不会抛出异常的函数标记
noexcept,提升代码可读性,也方便编译器优化。 - catch块捕获异常后,如果无法处理,应该重新抛出异常,让上层调用者决定如何处理。