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));
小结
本章学习了:
- 异常处理基础:throw、try-catch 语法
- 标准异常类:exception 层次结构和使用
- 自定义异常:继承标准异常类设计自定义异常
- 异常安全:RAII、基本保证、强保证
- 异常规范:noexcept 关键字的使用
- 最佳实践:何时使用异常、如何正确处理
下一步
接下来让我们学习 C++ I/O 流,了解文件和流的操作。