Ch 29: 错误处理
- 理解 C++ 异常处理机制
- 掌握标准异常类层次结构
- 学会设计异常安全的函数
- 理解 noexcept 的作用和影响
- 掌握替代错误处理方式(optional、expected)
29.1 Python try/except vs C++ try/catch
Section titled “29.1 Python try/except vs C++ try/catch”Python 异常处理
Section titled “Python 异常处理”# Python 异常处理def divide(a, b): if b == 0: raise ValueError("Division by zero") return a / b
try: result = divide(10, 0)except ValueError as e: print(f"Invalid value: {e}")except (KeyError, TypeError) as e: print(f"Key or type error: {e}")except Exception as e: print(f"Unknown error: {e}") raise # 重新抛出finally: cleanup() # 始终执行Python 异常特点:
- 所有异常都是类,继承自 BaseException
- 可以捕获几乎任何异常
- 异常会传播到调用栈
- finally 块始终执行
C++ 异常处理
Section titled “C++ 异常处理”#include <stdexcept>#include <iostream>#include <string>
double divide(double a, double b) { if (b == 0.0) { throw std::invalid_argument("Division by zero"); } return a / b;}
int main() { try { double result = divide(10.0, 0.0); } catch (const std::invalid_argument& e) { std::cout << "Invalid argument: " << e.what() << "\n"; } catch (const std::exception& e) { std::cout << "Standard exception: " << e.what() << "\n"; } catch (...) { std::cout << "Unknown exception\n"; throw; // 重新抛出 } // 没有 finally,RAII 处理资源清理}C++ 异常特点:
- 异常层次结构清晰
- 按引用捕获(避免切片)
- catch(…) 捕获所有异常
- RAII 替代 finally
29.2 标准异常层次结构
Section titled “29.2 标准异常层次结构”std::exception├── std::logic_error│ ├── std::invalid_argument│ ├── std::domain_error│ ├── std::length_error│ ├── std::out_of_range│ └── std::future_error (C++11)├── std::runtime_error│ ├── std::range_error│ ├── std::overflow_error│ ├── std::underflow_error│ └── std::regex_error (C++11)├── std::bad_alloc├── std::bad_cast (C++11)├── std::bad_typeid (C++11)├── std::bad_exception (C++11)└── std::nested_exception (C++11)#include <stdexcept>#include <iostream>#include <vector>#include <string>
// std::invalid_argument - 参数无效void process_age(int age) { if (age < 0 || age > 150) { throw std::invalid_argument("Age must be between 0 and 150"); }}
// std::out_of_range - 越界访问int& at_index(std::vector<int>& v, std::size_t idx) { if (idx >= v.size()) { throw std::out_of_range("Index out of range: " + std::to_string(idx)); } return v[idx];}
// std::domain_error - 域错误(数学运算不合法)double sqrt_safe(double x) { if (x < 0) { throw std::domain_error("Square root of negative number"); } return std::sqrt(x);}
// std::length_error - 长度超出允许范围void reserve_too_much(std::vector<int>& v) { if (v.size() > 1000000) { throw std::length_error("Vector size exceeds maximum"); }}
int main() { try { process_age(-5); } catch (const std::invalid_argument& e) { std::cerr << "Invalid: " << e.what() << "\n"; }
try { std::vector<int> v{1, 2, 3}; at_index(v, 10); } catch (const std::out_of_range& e) { std::cerr << "Out of range: " << e.what() << "\n"; }
try { sqrt_safe(-1.0); } catch (const std::domain_error& e) { std::cerr << "Domain error: " << e.what() << "\n"; }}std::bad_alloc 和 std::bad_cast
Section titled “std::bad_alloc 和 std::bad_cast”#include <iostream>#include <new>#include <typeinfo>#include <string>
struct Base { virtual ~Base() = default;};
struct Derived : Base { void specific_method() {}};
int main() { // std::bad_alloc - new 失败 try { // 尝试分配巨大内存 void* p = operator new(sizeof(char) * 1000000000000000000ULL); } catch (const std::bad_alloc& e) { std::cerr << "Allocation failed: " << e.what() << "\n"; }
// std::bad_cast - 动态转换失败 try { Base* base = new Derived(); // 错误:base 实际指向 Derived,但转换类型不对 // Derived* d = static_cast<Derived*>(base); // 如果基类没有虚函数,这是 UB delete base; } catch (const std::bad_cast& e) { std::cerr << "Bad cast: " << e.what() << "\n"; }}29.3 自定义异常类
Section titled “29.3 自定义异常类”基本自定义异常
Section titled “基本自定义异常”#include <stdexcept>#include <string>#include <iostream>
// 继承 std::runtime_error 用于运行时错误class NetworkError : public std::runtime_error {public: enum class ErrorCode { ConnectionFailed, Timeout, HostNotFound, ProtocolError };
explicit NetworkError(const std::string& msg, ErrorCode code = ErrorCode::ConnectionFailed) : std::runtime_error(msg), code_(code) {}
ErrorCode code() const { return code_; }
const char* what() const noexcept override { return message_.c_str(); }
private: ErrorCode code_; mutable std::string message_;
// 缓存 what() 消息 void build_message() const { message_ = "[NetworkError:" + std::to_string(static_cast<int>(code_)) + "] "; message_ += std::runtime_error::what(); }};
// 使用void connect(const std::string& host) { if (host.empty()) { throw std::invalid_argument("Host cannot be empty"); } if (host == "unreachable") { throw NetworkError("Cannot reach host", NetworkError::ErrorCode::Timeout); }}
int main() { try { connect("unreachable"); } catch (const NetworkError& e) { std::cerr << "Network error: " << e.what() << "\n"; std::cerr << "Error code: " << static_cast<int>(e.code()) << "\n"; } catch (const std::exception& e) { std::cerr << "Exception: " << e.what() << "\n"; }}#include <stdexcept>#include <iostream>#include <string>
void low_level_operation() { try { throw std::runtime_error("Low level failure"); } catch (...) { // 捕获并重新抛出,携带当前异常作为原因 std::throw_with_nested( std::runtime_error("Operation failed")); }}
void high_level_operation() { try { low_level_operation(); } catch (...) { std::throw_with_nested( std::runtime_error("High level operation failed")); }}
int main() { try { high_level_operation(); } catch (const std::nested_exception& e) { std::cerr << "Top-level error: " << e.what() << "\n"; try { e.rethrow_if_nested(); } catch (const std::exception& inner) { std::cerr << " Caused by: " << inner.what() << "\n"; } } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << "\n"; }}29.4 noexcept 详解
Section titled “29.4 noexcept 详解”#include <exception>#include <iostream>
// noexcept - 承诺不抛出异常void safe_function() noexcept { // 如果这里抛出异常,调用 std::terminate()}
// noexcept 表达式void test() { constexpr bool can_throw = noexcept(safe_function()); // safe_function 声明为 noexcept,所以 can_throw 为 true
std::cout << "safe_function is noexcept: " << can_throw << "\n";}
// 条件 noexcepttemplate<typename T>void copy_vector(std::vector<T>& dest, const std::vector<T>& src) noexcept( std::is_nothrow_copy_constructible_v<T>) { // 只有当 T 的拷贝构造不抛异常时,这个函数才是 noexcept}noexcept 的影响
Section titled “noexcept 的影响”#include <vector>#include <iostream>
class Widget {public: Widget() { std::cout << "Construct Widget\n"; } ~Widget() { std::cout << "Destroy Widget\n"; }
// noexcept 移动构造函数 Widget(Widget&&) noexcept { std::cout << "Move Widget\n"; }
Widget& operator=(Widget&&) noexcept { std::cout << "Move assign Widget\n"; return *this; }
// 非 noexcept 移动构造函数 Widget(const Widget&) { std::cout << "Copy Widget\n"; }};
int main() { std::vector<Widget> vec;
// vector realloc 时: // - 如果移动构造函数是 noexcept,使用移动 // - 否则使用拷贝(更安全) std::cout << "=== push_back ===\n"; vec.push_back(Widget()); // 可能移动
std::cout << "\n=== reserve and push_back ===\n"; vec.reserve(10); // 预分配 vec.push_back(Widget()); // 移动}noexcept 与 STL
Section titled “noexcept 与 STL”#include <vector>#include <iostream>#include <type_traits>
// vector 在 realloc 时的行为// C++11/14: 如果移动操作 noexcept,使用移动// C++17: 移动操作总是 noexcept(除非显式标记为可能抛出)
class MayThrow {public: MayThrow() = default; MayThrow(MayThrow&&) {} // 可能抛出};
class NoThrow {public: NoThrow() = default; NoThrow(NoThrow&&) noexcept {} // 不抛出};
int main() { std::cout << "MayThrow move is noexcept: " << std::is_nothrow_move_constructible_v<MayThrow> << "\n"; std::cout << "NoThrow move is noexcept: " << std::is_nothrow_move_constructible_v<NoThrow> << "\n";
// 推荐:移动操作应该标记为 noexcept // 这样 vector 可以优化 realloc}29.5 异常安全保证
Section titled “29.5 异常安全保证”#include <vector>#include <string>#include <iostream>
// 1. 基本保证 - 异常后对象仍有效class BasicGuarantee { std::vector<int> data_;public: void push_back_safe(int value) { auto copy = data_; // 拷贝副本 copy.push_back(value); // 可能抛出(内存分配失败) data_ = std::move(copy); // 不会抛异常 } // 即使 push_back 失败,data_ 仍保持原状态};
// 2. 强保证 - 异常后状态不变class StrongGuarantee { std::vector<int> data_;public: void operation() noexcept { auto copy = data_; // 拷贝 copy.push_back(42); // 可能抛出 data_ = std::move(copy); // noexcept } // 整个操作是原子性的:要么成功,要么状态不变};
// 3. 不抛出保证 - 绝不抛出异常class NoThrow { int data_ = 0;public: void reset() noexcept { data_ = 0; // 绝不抛异常的基本操作 }};RAII 保证异常安全
Section titled “RAII 保证异常安全”#include <iostream>#include <fstream>#include <stdexcept>
class FileHandler { std::fstream file_;public: FileHandler(const std::string& filename) { file_.open(filename); if (!file_.is_open()) { throw std::runtime_error("Cannot open file"); } }
~FileHandler() noexcept { if (file_.is_open()) { file_.close(); } }
void write(const std::string& data) { if (!(file_ << data << "\n")) { throw std::runtime_error("Write failed"); } }};
void safe_operation() { FileHandler file("test.txt"); file.write("data1"); file.write("data2"); // 即使 write 抛出异常,file 也会正确关闭}
int main() { try { safe_operation(); } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << "\n"; }}29.6 错误处理策略
Section titled “29.6 错误处理策略”返回 std::optional(C++17)
Section titled “返回 std::optional(C++17)”#include <optional>#include <vector>#include <iostream>#include <string>
// 策略1:返回 std::optional// 成功:返回包含值的 optional// 失败:返回空 optional
std::optional<int> find_index(const std::vector<int>& v, int target) { for (std::size_t i = 0; i < v.size(); ++i) { if (v[i] == target) { return static_cast<int>(i); } } return std::nullopt; // 没找到}
std::optional<std::string> get_config(const std::string& key) { static std::vector<std::pair<std::string, std::string>> configs = { {"host", "localhost"}, {"port", "8080"} };
for (const auto& [k, v] : configs) { if (k == key) { return v; } } return std::nullopt;}
int main() { std::vector<int> nums = {1, 2, 3, 4, 5};
// 使用 optional auto idx = find_index(nums, 3); if (idx.has_value()) { std::cout << "Found at index: " << idx.value() << "\n"; } else { std::cout << "Not found\n"; }
// 使用 value_or auto port = get_config("port").value_or("80"); std::cout << "Port: " << port << "\n";
// 使用 and_then(C++23) // auto result = get_config("host") // .and_then([](const std::string& h) { // return validate_host(h); // });}#include <variant>#include <iostream>#include <string>
// 策略2:返回错误码enum class Error { NotFound, InvalidInput, PermissionDenied, Unknown};
std::variant<int, Error> divide(int a, int b) { if (b == 0) { return Error::InvalidInput; } return a / b;}
int main() { auto result = divide(10, 0);
if (std::holds_alternative<Error>(result)) { switch (std::get<Error>(result)) { case Error::InvalidInput: std::cerr << "Invalid input\n"; break; case Error::NotFound: std::cerr << "Not found\n"; break; default: std::cerr << "Unknown error\n"; } } else { std::cout << "Result: " << std::get<int>(result) << "\n"; }}std::expected(C++23)
Section titled “std::expected(C++23)”#include <expected>#include <iostream>#include <string>
// C++23: std::expected - 类似 optional 但携带错误信息std::expected<int, std::string> parse_int(const std::string& s) { try { return std::stoi(s); } catch (...) { return std::unexpected("Failed to parse: " + s); }}
int main() { auto result = parse_int("42");
if (result) { std::cout << "Parsed: " << result.value() << "\n"; } else { std::cerr << "Error: " << result.error() << "\n"; }
// 使用 value_or auto result2 = parse_int("abc"); std::cout << "Or default: " << result2.value_or(0) << "\n";}29.7 最佳实践
Section titled “29.7 最佳实践”何时使用异常
Section titled “何时使用异常”#include <stdexcept>#include <iostream>
// 使用异常的情况:// 1. 真正异常的情况(不应该发生的错误)// 2. 错误不能被调用者合理处理// 3. 需要传播到上层
class DatabaseConnection {public: void connect(const std::string& host) { if (host.empty()) { throw std::invalid_argument("Host cannot be empty"); } // 连接失败应该抛异常 }};
// 不使用异常的情况:// 1. 常见可预期的错误(如查找失败)// 2. 性能关键代码// 3. 需要快速路径和慢速路径
std::optional<int> find_in_cache(int key); // 缓存查找,失败是正常的
int critical_path(int key); // 性能关键,不应抛异常异常安全代码模式
Section titled “异常安全代码模式”#include <vector>#include <string>#include <iostream>
// 模式1:Copy-and-swap(强异常安全)class StringList { std::vector<std::string> data_;public: void push_back_guaranteed(const std::string& s) { auto copy = data_; // 拷贝 copy.push_back(s); // 可能抛异常 data_ = std::move(copy); // noexcept }};
// 模式2:资源获取即初始化(RAII)class RAIIResource { std::vector<int> data_;public: void modify() { // 即使修改失败,原状态保持 auto backup = data_; // ... 修改操作 }};
// 模式3:noexecpt 操作替代异常class SafeNumerics {public: static int add(int a, int b) noexcept { if (a > 0 && b > INT_MAX - a) { return INT_MAX; // 溢出处理,不抛异常 } return a + b; }};异常规范和文档
Section titled “异常规范和文档”#include <stdexcept>#include <vector>
// 文档化函数可能抛出的异常class Widget {public: // 可能抛出 std::bad_alloc(从 push_back) // 可能抛出 std::out_of_range(从 at) void process() { data_.push_back(42); data_.at(0) = 100; }
private: std::vector<int> data_;};
// noexcept 函数不应该抛出class SafeClass {public: int get_size() const noexcept { return data_.size(); }
bool is_empty() const noexcept { return data_.empty(); }
private: std::vector<int> data_;};29.8 章节总结
Section titled “29.8 章节总结”异常 vs 其他错误处理:
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 异常 | 真正异常 | 传播自动 | 可能有性能开销 |
| optional | 可能无值 | 明确 | 调用繁琐 |
| error code | 性能关键 | 零开销 | 可能被忽略 |
| expected | 有错误信息 | 完整 | C++23 |
异常安全级别:
| 级别 | 保证 | 说明 |
|---|---|---|
| 基本保证 | 对象有效 | 异常后不泄漏资源 |
| 强保证 | 状态不变 | 操作原子性 |
| 不抛异常 | 无异常 | 最低开销 |
noexcept 影响:
- 移动构造函数 noexcept → vector realloc 使用移动
- noexcept 函数抛出异常 → 调用 std::terminate()
- 用于性能优化和 API 契约声明
最佳实践:
- 用 RAII 管理资源
- 移动操作标记 noexcept
- 常用错误用 optional/error_code
- 真正异常才用异常
下章预告:ch30 综合项目,使用 C++20 构建 CLI 任务管理器,整合所有学到的知识。