第3章:异常(Exceptions)
一、导读
1.1 本章学习目标
第3章讲解 C++ 异常处理的最佳实践。Meyers 在此给出了关于异常安全、异常保证、异常层次等方面的重要建议。
本章核心学习目标:
- 掌握异常安全的三个层次
- 理解异常保证:noexcept 的使用
- 掌握异常层次的设计
- 理解异常与构造函数的关系
1.2 为什么本章如此重要
异常处理是 C++ 中最容易出错的部分。错误的异常处理会导致资源泄漏、程序崩溃、难以调试的问题。
1.3 核心问题引导
为什么析构函数不应该抛出异常?
异常安全的强保证如何实现?
什么时候应该用 noexcept?
二、核心概念详解
2.1 析构函数不应抛出异常
cpp
// 错误: 析构函数抛出异常
class BadConnection {
int fd;
public:
~BadConnection() {
if (close(fd) != 0)
throw std::runtime_error("close failed"); // 危险!
}
};
// 如果 vector<BadConnection> 在扩容时析构旧元素抛出异常
// → std::terminate 被调用 → 程序崩溃
// 正确: 在析构函数中处理异常
class GoodConnection {
int fd;
public:
~GoodConnection() noexcept {
try {
if (close(fd) != 0) {
// 记录日志,但不抛出
logError("close failed");
}
} catch (...) {
// 吞掉异常,保证析构不抛出
}
}
void close() { // 提供显式的关闭方法
if (close(fd) != 0)
throw std::runtime_error("close failed");
}
};2.2 异常安全保证
cpp
// 基本保证: 异常后对象处于有效状态
void basicGuarantee(std::vector<int>& v, int value) {
v.push_back(value); // 如果抛出 bad_alloc,v 仍然有效
}
// 强保证: 异常后状态不变(如同操作未发生)
void strongGuarantee(std::vector<int>& v, int value) {
auto backup = v; // 先备份
try {
v.push_back(value);
} catch (...) {
v = std::move(backup); // 回滚
throw;
}
}
// 更好的实现: copy-and-swap
void strongGuaranteeBetter(std::vector<int>& v, int value) {
auto copy = v;
copy.push_back(value);
v.swap(copy); // swap 不抛出异常
}
// noexcept 的使用
void swap(Widget& other) noexcept {
std::swap(data, other.data);
}
// noexcept 的好处:
// 1. 编译器可以生成更高效的代码
// 2. STL 容器在移动时优先使用 noexcept 的移动操作2.3 异常层次设计
cpp
// 设计异常层次
class AppException : public std::exception {
protected:
std::string message;
public:
explicit AppException(std::string msg) : message(std::move(msg)) {}
const char* what() const noexcept override { return message.c_str(); }
};
class DatabaseException : public AppException {
public:
using AppException::AppException;
};
class ConnectionException : public DatabaseException {
public:
using DatabaseException::DatabaseException;
};
class QueryException : public DatabaseException {
public:
using DatabaseException::DatabaseException;
};
// 使用:
try {
db.execute("SELECT ...");
} catch (const ConnectionException& e) {
// 处理连接错误
} catch (const QueryException& e) {
// 处理查询错误
} catch (const DatabaseException& e) {
// 处理其他数据库错误
} catch (const AppException& e) {
// 处理其他应用错误
} catch (const std::exception& e) {
// 处理标准库异常
}三、实战案例
3.1 案例一:异常安全的资源管理
cpp
class Transaction {
Database& db;
bool committed = false;
public:
explicit Transaction(Database& d) : db(d) {
db.execute("BEGIN");
}
void execute(const std::string& sql) {
db.execute(sql); // 如果抛出异常,析构函数回滚
}
void commit() {
db.execute("COMMIT");
committed = true;
}
~Transaction() noexcept {
if (!committed) {
try {
db.execute("ROLLBACK");
} catch (...) {
// 析构函数不抛出
}
}
}
};
// 使用:
void transferMoney(Database& db, int from, int to, int amount) {
Transaction txn(db);
txn.execute("UPDATE accounts SET balance = balance - " +
std::to_string(amount) + " WHERE id = " + std::to_string(from));
txn.execute("UPDATE accounts SET balance = balance + " +
std::to_string(amount) + " WHERE id = " + std::to_string(to));
txn.commit();
} // 如果任何操作抛出异常,txn 析构时自动回滚四、本章小结
析构函数不应抛出异常,否则可能导致程序崩溃
异常安全分三个层次:基本保证、强保证、不抛保证
noexcept 标记不抛出的函数,帮助编译器优化
异常层次应该反映错误的语义分类
五、思考与练习
思考题
为什么 STL 容器要求元素的移动操作是 noexcept 的?
如何实现异常安全的容器操作?
编程练习
实现一个异常安全的数据库事务类。
设计一个异常层次,覆盖文件操作、网络操作、数据库操作。