跳到主要内容

C++ 异常处理

异常处理是 C++ 提供的一种处理程序运行时错误的机制。通过异常,我们可以将错误检测和错误处理分离,使代码更加清晰和健壮。

为什么需要异常处理?

传统错误处理的局限性

在异常机制出现之前,C 程序通常使用以下方式处理错误:

// 方式1:返回错误码
int divide(int a, int b, int* result) {
if (b == 0) {
return -1; // 返回错误码
}
*result = a / b;
return 0; // 成功
}

// 方式2:全局错误变量
errno = 0;
double result = sqrt(-1);
if (errno != 0) {
// 处理错误
}

// 方式3:断言(仅用于调试)
assert(b != 0); // 生产环境中不应依赖断言

这些方式存在明显的问题:

返回错误码的问题: 调用者必须检查每个函数的返回值,容易遗漏。当函数需要返回实际结果时,错误码和结果混在一起,设计变得复杂。

全局变量的问题: 多线程环境下不安全,容易产生竞态条件。

断言的问题: 仅在调试时生效,发布版本中会被禁用,不能用于真正的错误处理。

异常处理的优势

异常机制提供了更优雅的错误处理方式:

#include <stdexcept>
#include <iostream>

double divide(double a, double b) {
if (b == 0) {
throw std::runtime_error("除数不能为零");
}
return a / b;
}

int main() {
try {
double result = divide(10.0, 0.0);
std::cout << "结果: " << result << std::endl;
} catch (const std::runtime_error& e) {
std::cerr << "错误: " << e.what() << std::endl;
}
return 0;
}

异常处理的优势:

  • 分离关注点:错误检测和错误处理代码分离,各自职责清晰
  • 自动传播:异常会自动沿调用栈向上传播,直到被捕获
  • 强制处理:未捕获的异常会导致程序终止,强迫开发者处理错误
  • 携带信息:异常对象可以携带详细的错误信息

异常的基本语法

throw:抛出异常

throw 语句用于抛出一个异常:

#include <stdexcept>

void checkAge(int age) {
if (age < 0) {
// 抛出一个异常对象
throw std::invalid_argument("年龄不能为负数");
}
if (age > 150) {
throw std::out_of_range("年龄超出合理范围");
}
}

// 可以抛出任何类型的对象
void process(int value) {
if (value == 0) {
throw 0; // 抛出整数
}
if (value < 0) {
throw "负数无效"; // 抛出 C 风格字符串
}
if (value > 100) {
throw std::string("值过大"); // 抛出 std::string
}
}

// 重新抛出当前异常
void wrapper() {
try {
process(-1);
} catch (...) {
// 做一些清理工作
throw; // 重新抛出捕获的异常
}
}

try-catch:捕获异常

try 块包含可能抛出异常的代码,catch 块处理捕获的异常:

#include <iostream>
#include <stdexcept>

int main() {
try {
// 可能抛出异常的代码
int* ptr = new int[100000000000]; // 可能抛出 std::bad_alloc

// ... 其他代码

} catch (const std::bad_alloc& e) {
// 捕获特定类型的异常
std::cerr << "内存分配失败: " << e.what() << std::endl;

} catch (const std::exception& e) {
// 捕获标准异常基类
std::cerr << "标准异常: " << e.what() << std::endl;

} catch (...) {
// 捕获所有其他类型的异常
std::cerr << "未知异常" << std::endl;
}

return 0;
}

catch 块的匹配规则

异常匹配遵循以下规则:

#include <stdexcept>
#include <iostream>

void throwException(int type) {
switch (type) {
case 1: throw std::runtime_error("运行时错误");
case 2: throw std::logic_error("逻辑错误");
case 3: throw 42;
case 4: throw "C 字符串";
}
}

int main() {
try {
throwException(1);

} catch (const std::runtime_error& e) {
// 优先匹配最具体的类型
std::cout << "捕获 runtime_error: " << e.what() << std::endl;

} catch (const std::logic_error& e) {
std::cout << "捕获 logic_error: " << e.what() << std::endl;

} catch (const std::exception& e) {
// 匹配所有标准异常
std::cout << "捕获 exception: " << e.what() << std::endl;

} catch (int n) {
// 匹配整数异常
std::cout << "捕获整数: " << n << std::endl;

} catch (const char* msg) {
// 匹配 C 字符串
std::cout << "捕获字符串: " << msg << std::endl;

} catch (...) {
// 匹配任何异常(必须放在最后)
std::cout << "捕获未知异常" << std::endl;
}

return 0;
}

重要规则:

  • catch 块按顺序匹配,找到第一个匹配的就处理
  • 应该从最具体的异常类型到最通用的类型排列
  • catch (...) 必须放在最后,它会捕获任何异常
  • 异常按引用捕获可以避免对象切片

标准异常类层次

C++ 标准库提供了一组异常类,形成以下继承层次:

std::exception
├── std::logic_error // 逻辑错误(可在编码阶段检测)
│ ├── std::domain_error // 定义域错误
│ ├── std::invalid_argument // 无效参数
│ ├── std::length_error // 长度错误
│ └── std::out_of_range // 超出范围

├── std::runtime_error // 运行时错误(运行时才能检测)
│ ├── std::range_error // 范围错误
│ ├── std::overflow_error // 上溢
│ └── std::underflow_error // 下溢

├── std::bad_alloc // 内存分配失败
├── std::bad_cast // dynamic_cast 失败
├── std::bad_typeid // typeid 失败
└── std::bad_exception // 异常规范失败

使用标准异常

#include <stdexcept>
#include <vector>
#include <iostream>

// 使用 invalid_argument
double sqrt_value(double x) {
if (x < 0) {
throw std::invalid_argument("不能对负数求平方根");
}
return std::sqrt(x);
}

// 使用 out_of_range
int getElement(const std::vector<int>& vec, size_t index) {
if (index >= vec.size()) {
throw std::out_of_range("索引超出范围");
}
return vec[index];
}

// 使用 runtime_error
void openFile(const std::string& filename) {
std::ifstream file(filename);
if (!file.is_open()) {
throw std::runtime_error("无法打开文件: " + filename);
}
}

// 使用 length_error
void appendToBuffer(std::vector<int>& buffer, int value) {
if (buffer.size() >= buffer.max_size()) {
throw std::length_error("缓冲区已满");
}
buffer.push_back(value);
}

int main() {
try {
sqrt_value(-1);
} catch (const std::invalid_argument& e) {
std::cerr << "参数错误: " << e.what() << std::endl;
}

try {
std::vector<int> v = {1, 2, 3};
getElement(v, 10);
} catch (const std::out_of_range& e) {
std::cerr << "范围错误: " << e.what() << std::endl;
}

return 0;
}

自定义异常类

继承标准异常类

最佳实践是继承 std::exception 或其子类,这样可以利用标准异常处理机制:

#include <exception>
#include <string>
#include <iostream>

// 自定义异常类:文件错误
class FileError : public std::runtime_error {
public:
explicit FileError(const std::string& filename, const std::string& message)
: std::runtime_error("文件错误: " + filename + " - " + message),
filename_(filename) {}

const std::string& getFilename() const { return filename_; }

private:
std::string filename_;
};

// 自定义异常类:网络错误
class NetworkError : public std::runtime_error {
public:
enum class ErrorType {
ConnectionFailed,
Timeout,
InvalidResponse
};

NetworkError(ErrorType type, const std::string& message)
: std::runtime_error(message), type_(type) {}

ErrorType getType() const { return type_; }

private:
ErrorType type_;
};

// 使用自定义异常
void readFile(const std::string& filename) {
std::ifstream file(filename);
if (!file.is_open()) {
throw FileError(filename, "无法打开文件");
}
// ... 读取文件
}

int main() {
try {
readFile("nonexistent.txt");
} catch (const FileError& e) {
std::cerr << "文件错误: " << e.what() << std::endl;
std::cerr << "问题文件: " << e.getFilename() << std::endl;
} catch (const NetworkError& e) {
std::cerr << "网络错误: " << e.what() << std::endl;
// 根据错误类型采取不同措施
switch (e.getType()) {
case NetworkError::ErrorType::ConnectionFailed:
// 重试连接
break;
case NetworkError::ErrorType::Timeout:
// 增加超时时间
break;
case NetworkError::ErrorType::InvalidResponse:
// 记录并跳过
break;
}
}

return 0;
}

异常类设计原则

设计自定义异常类时,应遵循以下原则:

#include <exception>
#include <string>
#include <ctime>

// 良好的异常类设计
class ApplicationException : public std::exception {
public:
// 构造函数
ApplicationException(const std::string& message,
const std::string& context = "",
const char* file = nullptr,
int line = 0)
: message_(message),
context_(context),
file_(file ? file : ""),
line_(line),
timestamp_(std::time(nullptr)) {}

// 重写 what() 方法
const char* what() const noexcept override {
return message_.c_str();
}

// 提供额外信息
const std::string& context() const { return context_; }
const std::string& file() const { return file_; }
int line() const { return line_; }
std::time_t timestamp() const { return timestamp_; }

// 可复制和可移动
ApplicationException(const ApplicationException&) = default;
ApplicationException& operator=(const ApplicationException&) = default;
ApplicationException(ApplicationException&&) = default;
ApplicationException& operator=(ApplicationException&&) = default;

private:
std::string message_;
std::string context_;
std::string file_;
int line_;
std::time_t timestamp_;
};

// 辅助宏:自动记录文件和行号
#define THROW_EXCEPTION(msg) \
throw ApplicationException(msg, "", __FILE__, __LINE__)

异常安全

什么是异常安全?

异常安全指代码在异常发生时的行为保证。有四个等级:

等级名称保证
1无保证异常可能导致资源泄漏或数据损坏
2基本保证异常发生时,对象处于有效状态,资源不泄漏
3强保证异常发生时,程序状态回滚到操作之前(事务语义)
4不抛异常保证操作保证不抛出异常

异常安全代码示例

#include <vector>
#include <algorithm>
#include <memory>

// 不安全版本:可能泄漏内存
void unsafe_function() {
int* p = new int[100]; // 如果下面抛出异常,内存泄漏!
process_that_may_throw();
delete[] p;
}

// 安全版本1:使用智能指针
void safe_version1() {
std::unique_ptr<int[]> p(new int[100]); // RAII
process_that_may_throw(); // 即使抛出异常,p 也会自动释放
}

// 安全版本2:使用标准容器
void safe_version2() {
std::vector<int> v(100); // 容器自动管理内存
process_that_may_throw();
}

// 基本保证示例
class Stack {
std::vector<int> data_;

public:
void push(int value) {
data_.push_back(value); // vector::push_back 提供强异常保证
}

int pop() {
if (data_.empty()) {
throw std::out_of_range("栈为空");
}
int value = data_.back();
data_.pop_back(); // 不会抛出异常
return value;
}
};

// 强保证示例:copy-and-swap 惯用法
class Container {
std::vector<int> data_;

public:
void updateAll(const std::vector<int>& newData) {
std::vector<int> temp = newData; // 先复制
// 如果复制失败,原数据不受影响

process(temp); // 处理临时数据
// 如果处理失败,原数据不受影响

using std::swap;
swap(data_, temp); // 交换(不抛异常)
// 成功后原数据被替换
}
};

RAII 与异常安全

RAII(Resource Acquisition Is Initialization)是实现异常安全的关键:

#include <fstream>
#include <mutex>

// 文件处理的 RAII
void processFile(const std::string& filename) {
std::ifstream file(filename); // RAII:自动关闭文件
if (!file) {
throw std::runtime_error("无法打开文件");
}

std::string line;
while (std::getline(file, line)) {
processLine(line); // 可能抛出异常
}
// 离开作用域时,file 自动关闭
}

// 锁的 RAII
class ThreadSafeCounter {
int count_ = 0;
mutable std::mutex mutex_;

public:
void increment() {
std::lock_guard<std::mutex> lock(mutex_); // RAII:自动释放锁
count_++;
// 即使后续操作抛出异常,锁也会被释放
}

int get() const {
std::lock_guard<std::mutex> lock(mutex_);
return count_;
}
};

// 动态内存的 RAII
void processData() {
auto data = std::make_unique<Data[]>(1000); // RAII
process(data.get()); // 可能抛出异常
// 离开作用域时,data 自动释放
}

异常规范

noexcept 关键字

C++11 引入 noexcept 关键字,用于指定函数不会抛出异常:

// 声明函数不抛出异常
int safe_add(int a, int b) noexcept {
return a + b;
}

// noexcept 函数中抛出异常会调用 std::terminate
void unsafe_noexcept() noexcept {
throw std::runtime_error("错误"); // 程序终止!
}

// 条件 noexcept
template<typename T>
void swap_wrapper(T& a, T& b) noexcept(noexcept(std::swap(a, b))) {
std::swap(a, b);
}

// noexcept 运算符:检查表达式是否不抛出异常
static_assert(noexcept(safe_add(1, 2)), "safe_add should be noexcept");

noexcept 的作用

#include <vector>
#include <utility>

// 1. 优化:编译器可以生成更高效的代码
int simple_function(int x) noexcept {
return x * 2; // 编译器不需要生成异常处理代码
}

// 2. 标准库容器优化
class MyClass {
public:
// 移动构造函数标记为 noexcept,vector 重分配时会优先使用移动
MyClass(MyClass&& other) noexcept
: data_(std::move(other.data_)) {}

private:
std::vector<int> data_;
};

// 3. 析构函数默认是 noexcept
class Resource {
public:
~Resource() /* noexcept = default */ {
// 析构函数默认不会抛出异常
// 如果需要抛出,必须显式声明 noexcept(false)
}
};

何时使用 noexcept

// 应该使用 noexcept 的情况:

// 1. 简单的算术运算
int add(int a, int b) noexcept { return a + b; }

// 2. 移动构造和移动赋值
class Buffer {
public:
Buffer(Buffer&& other) noexcept;
Buffer& operator=(Buffer&& other) noexcept;
};

// 3. swap 函数
template<typename T>
void my_swap(T& a, T& b) noexcept {
using std::swap;
swap(a, b);
}

// 4. 析构函数(默认就是 noexcept)

// 5. 空函数
void do_nothing() noexcept {}

// 不应该使用 noexcept 的情况:

// 1. 可能失败的 I/O 操作
std::string read_file(const std::string& path); // 文件可能不存在

// 2. 内存分配
void* allocate(size_t size); // 可能内存不足

// 3. 可能失败的业务逻辑
bool validate(const Data& data); // 数据可能无效

异常处理最佳实践

1. 用异常表示异常情况

// 好的做法:用异常表示真正的异常情况
std::string readFile(const std::string& filename) {
std::ifstream file(filename);
if (!file) {
throw std::runtime_error("无法打开文件: " + filename);
}
// ... 读取文件
}

// 不好的做法:用异常控制正常流程
int findElement(const std::vector<int>& v, int value) {
for (size_t i = 0; i < v.size(); i++) {
if (v[i] == value) {
return i;
}
}
throw std::runtime_error("未找到"); // 不应该这样!
}

// 更好的做法:返回 optional
std::optional<size_t> findElement(const std::vector<int>& v, int value) {
for (size_t i = 0; i < v.size(); i++) {
if (v[i] == value) {
return i;
}
}
return std::nullopt; // 正常情况:未找到
}

2. 按引用捕获异常

try {
// ...
} catch (const std::exception& e) { // 正确:按引用捕获
std::cout << e.what() << std::endl;
} catch (std::exception e) { // 错误:按值捕获,可能导致对象切片
std::cout << e.what() << std::endl;
}

3. 在适当的地方捕获异常

// 低层函数:抛出异常
int divide(int a, int b) {
if (b == 0) throw std::runtime_error("除零错误");
return a / b;
}

// 中层函数:可以不捕获,让异常传播
int calculate(int x, int y) {
return divide(x, y) * 2; // 异常自动向上传播
}

// 高层函数:捕获并处理异常
void processUserInput() {
try {
int result = calculate(getX(), getY());
std::cout << "结果: " << result << std::endl;
} catch (const std::exception& e) {
// 用户友好的错误消息
std::cerr << "计算错误: " << e.what() << std::endl;
// 恢复或记录日志
}
}

4. 释放资源后再重新抛出

void process() {
Resource* resource = acquireResource();

try {
useResource(resource);
} catch (...) {
releaseResource(resource); // 先释放资源
throw; // 再重新抛出异常
}

releaseResource(resource);
}

// 更好的做法:使用 RAII
void process() {
std::unique_ptr<Resource> resource(acquireResource()); // 自动管理
useResource(resource.get());
// 离开作用域时自动释放,即使抛出异常
}

5. 异常消息应该有意义

// 不好的做法
throw std::runtime_error("错误");

// 好的做法
throw std::runtime_error("无法连接数据库 '" + dbName +
"',主机: " + host +
",端口: " + std::to_string(port));

小结

本章学习了:

  1. 异常处理基础:throw、try-catch 语法
  2. 标准异常类:exception 层次结构和使用
  3. 自定义异常:继承标准异常类设计自定义异常
  4. 异常安全:RAII、基本保证、强保证
  5. 异常规范:noexcept 关键字的使用
  6. 最佳实践:何时使用异常、如何正确处理

下一步

接下来让我们学习 C++ I/O 流,了解文件和流的操作。