Ch 11: 错误处理与异常
- 掌握异常处理机制
- 理解异常安全级别
- 学会使用 std::optional 错误处理
- 设计良好的异常层次
- 理解 noexcept 的作用和性能影响
11.1 Python 异常机制回顾
Section titled “11.1 Python 异常机制回顾”Python 的异常处理简洁直观,使用 try/except/finally 语法即可捕获和处理异常:
try: result = risky_operation()except ValueError as e: print(f"Value error: {e}")except (KeyError, TypeError) as e: print(f"Key or type error: {e}")except Exception as e: print(f"Unknown: {e}") raise # 重新抛出finally: cleanup() # 始终执行Python 异常的显著特点是:
- 所有异常都是类,继承自
BaseException - 异常可以在任何层级捕获
finally块保证清理代码始终执行- 异常携带错误信息(
e对象)
Python 内置常见异常类型:ValueError、TypeError、KeyError、IndexError、AttributeError、RuntimeError 等。
11.2 C++ 异常机制
Section titled “11.2 C++ 异常机制”C++ 异常机制与 Python 类似,但有重要区别:
#include <stdexcept>#include <iostream>#include <string>
int divide(int a, int b) { if (b == 0) { throw std::invalid_argument("Division by zero"); } return a / b;}
int main() { try { int result = divide(10, 0); std::cout << "Result: " << result << std::endl; } catch (const std::invalid_argument& e) { // 捕获特定异常类型 std::cerr << "Invalid argument: " << e.what() << std::endl; } catch (const std::runtime_error& e) { // 捕获运行时错误 std::cerr << "Runtime error: " << e.what() << std::endl; } catch (const std::exception& e) { // 捕获所有标准异常(所有异常的基类) std::cerr << "Exception: " << e.what() << std::endl; } catch (...) { // 捕获所有异常(包括非 std::exception) std::cerr << "Unknown exception" << std::endl; throw; // 重新抛出 }
// 没有 finally,但 RAII 代替其作用 return 0;}C++ 异常的与 Python 的关键差异
Section titled “C++ 异常的与 Python 的关键差异”| 特性 | Python | C++ |
|---|---|---|
| 异常基类 | BaseException / Exception | std::exception |
| 捕获语法 | except Type as e | catch (const Type& e) |
| 重新抛出 | raise 或 raise e | throw; |
| 强制清理 | finally | RAII + 析构函数 |
| 异常规格 | 无 | noexcept 标记 |
异常抛出原则
Section titled “异常抛出原则”C++ 中抛出异常应遵循以下原则:
// 1. 抛出临时对象,避免抛出指针throw std::runtime_error("error message"); // OK// throw new std::runtime_error("..."); // 错误:不要抛指针
// 2. 使用标准异常或自定义异常类throw std::out_of_range("index out of range");throw std::invalid_argument("invalid parameter");
// 3. 异常对象被拷贝(所以抛出的应该是可拷贝的)class MyException : public std::exception {public: MyException(const std::string& msg) : message_(msg) {} const char* what() const noexcept override { return message_.c_str(); }private: std::string message_;};11.3 标准异常类层次
Section titled “11.3 标准异常类层次”C++ 标准库定义了完整的异常层次结构:
// 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 // 正则表达式错误// | +-- system_error (C++11) // 系统错误// |// +-- std::bad_alloc // 内存分配失败// +-- std::bad_cast // 动态类型转换失败// +-- std::bad_typeid // typeid(nullptr)// +-- std::bad_exception // 意外异常// +-- std::nested_exception (C++11) // 嵌套异常// +-- std::terminate_handler// +-- std::unexpected_handler自定义异常类
Section titled “自定义异常类”在实际项目中,建议创建自定义异常层次:
#include <stdexcept>#include <string>#include <string_view>
// 基础异常类class AppException : public std::runtime_error {public: AppException(std::string_view message, int error_code = 0) : std::runtime_error(std::string(message)), error_code_(error_code) {}
int error_code() const noexcept { return error_code_; }
private: int error_code_;};
// 文件相关异常class FileException : public AppException {public: using AppException::AppException;};
// 网络相关异常class NetworkException : public AppException {public: NetworkException(std::string_view message, int error_code = 0) : AppException(message, error_code) {}};
// 使用try { throw FileException("Cannot open file", 404);}catch (const AppException& e) { std::cerr << "App error [" << e.error_code() << "]: " << e.what() << std::endl;}11.4 异常安全级别
Section titled “11.4 异常安全级别”异常安全是 C++ 中非常重要的概念,指的是当异常发生时,程序的状态保证。分为三个级别:
11.4.1 无抛出保证(Noexcept Guarantee)
Section titled “11.4.1 无抛出保证(Noexcept Guarantee)”最严格的安全级别。函数保证不会抛出异常。如果抛出,程序会调用 std::terminate() 终止。
// 使用 noexcept 标记void critical_function() noexcept { // 不会抛出异常的代码}
// C++17 允许更精确的说明void maybe_throw() noexcept(false) { // 可能抛出异常}
void never_throws() noexcept(true) { // 永远不会抛出}noexcept 的实际作用:
- 编译器可以生成更高效的代码(不需要异常展开栈)
- 移动构造函数通常是 noexcept 的(否则 STL 容器会使用拷贝)
noexcept运算符可以查询函数是否承诺不抛异常
// noexcept 影响移动语义class Buffer {public: Buffer(size_t size) : data_(new char[size]) {}
// 移动构造函数 - 应该标记 noexcept // 否则 std::vector::push_back 可能选择拷贝而不是移动 Buffer(Buffer&& other) noexcept : data_(std::move(other.data_)) {}
// 移动赋值 Buffer& operator=(Buffer&& other) noexcept { if (this != &other) { data_ = std::move(other.data_); } return *this; }
private: std::unique_ptr<char[]> data_;};11.4.2 基本保证(Basic Guarantee)
Section titled “11.4.2 基本保证(Basic Guarantee)”异常发生后,对象仍然处于有效状态,没有资源泄漏。Python 的 try/except 默认提供这种级别。
class Process {private: std::vector<int> data_; std::string name_;
public: void append(int value) { // 基本保证:即使异常发生,data_ 仍有效 // 可能状态改变,但不会是无效状态 data_.push_back(value); // 可能抛出 bad_alloc name_ = "processed"; // 这行可能不执行 }};11.4.3 强保证(Strong Guarantee)
Section titled “11.4.3 强保证(Strong Guarantee)”最实用的安全级别。异常发生后,程序状态与调用前完全相同,即”事务性语义”。
class Account {private: double balance_; std::string name_;
public: // 强保证:要么完全成功,要么状态不变 void transfer_from(Account& from, double amount) { if (this == &from) return;
// 全部操作在同一语句中,异常时状态不变 from.withdraw(amount); this->deposit(amount); // 如果这里异常,from 的钱已经取出 // 但这里实现了"撤销"逻辑 }
void withdraw(double amount) { if (balance_ < amount) { throw std::runtime_error("Insufficient funds"); } balance_ -= amount; }
void deposit(double amount) { if (amount <= 0) { throw std::invalid_argument("Deposit must be positive"); } balance_ += amount; }};Python 异常安全对比
Section titled “Python 异常安全对比”Python 的异常安全主要依靠 try/finally 和上下文管理器:
# Python - 使用 finally 保证清理def process(): resource = acquire_resource() # 可能抛出 try: return resource.process() # 可能抛出 finally: resource.release() # 始终执行
# Python - 上下文管理器def process(): with acquire_resource() as resource: return resource.process() # with 块退出时自动调用 __exit__C++ 的 RAII 模式提供了类似的自动清理功能,但更加类型安全和自动化:
// C++ - RAII 自动清理void process() { Resource resource = acquire_resource(); // 构造函数可能抛出 resource.process(); // 可能抛出} // 析构函数自动调用,即使异常发生11.5 std::optional 错误处理
Section titled “11.5 std::optional 错误处理”对于可能”没有值”的情况,std::optional(C++17)提供了一种无异常的错误处理方式。这与 Python 中返回 None 或 None 配合类型标注类似:
# Python - Optional 返回值from typing import Optional
def find_user(users: dict, name: str) -> Optional[int]: if name in users: return users[name] return Nonestd::optional 基础用法
Section titled “std::optional 基础用法”#include <optional>#include <unordered_map>#include <iostream>
// 函数返回 optionalstd::optional<int> find_user_age( const std::unordered_map<std::string, int>& users, const std::string& name) {
auto it = users.find(name); if (it != users.end()) { return it->second; // 返回值 } return std::nullopt; // 返回空}
int main() { std::unordered_map<std::string, int> users = { {"Alice", 30}, {"Bob", 25} };
// 使用方式 1: 检查后再访问 auto age = find_user_age(users, "Alice"); if (age) { std::cout << "Alice's age: " << *age << std::endl; }
// 使用方式 2: value_or 提供默认值 int unknown_age = find_user_age(users, "Charlie").value_or(0); std::cout << "Charlie's age: " << unknown_age << std::endl; // 0
// 使用方式 3: and_then 链式调用 auto result = find_user_age(users, "Alice") .transform([](int age) { return age * 2; });
return 0;}optional 详细操作
Section titled “optional 详细操作”#include <optional>
std::optional<int> opt = 42;
// 解引用(需确保有值)int val = *opt; // UB 如果 opt 为空!
// 检查if (opt.has_value()) { /* ... */ }if (opt) { /* ... */ }
// 获取值或默认值int x = opt.value_or(-1); // opt 为空时返回 -1
// 变换std::optional<double> dbl = opt.transform([](int x) { return x * 2.0; });
// 链式操作(C++23)std::optional<int> result = opt .and_then([](int x) { return x > 0 ? std::optional<int>(x * 2) : std::nullopt; });Optional vs 异常的对比
Section titled “Optional vs 异常的对比”| 场景 | std::optional | 异常 |
|---|---|---|
| 预期失败(正常流程) | 适用 | 不推荐 |
| 意外错误(bug) | 不推荐 | 适用 |
| 性能敏感路径 | 更快(无栈展开) | 有开销 |
| 需要错误信息 | 不适合 | 适合 |
| 预期”无值” | 最佳选择 | 可以 |
// 场景 1: 预期失败 - 用 optionalstd::optional<size_t> find_index(const std::vector<int>& v, int target) { for (size_t i = 0; i < v.size(); ++i) { if (v[i] == target) return i; } return std::nullopt; // 预期失败:没找到}
// 场景 2: 意外错误 - 用异常double divide_safe(int a, int b) { if (b == 0) { throw std::invalid_argument("Division by zero"); // 编程错误 } return static_cast<double>(a) / b;}11.6 std::expected(C++23)
Section titled “11.6 std::expected(C++23)”C++23 引入了 std::expected,它结合了 std::optional 和错误值的概念,类似于 Rust 的 Result<T, E>:
#include <expected>
// 返回错误信息std::expected<int, std::string> parse_number(const std::string& s) { try { return std::stoi(s); } catch (...) { return std::unexpected("Invalid number format"); }}
int main() { auto result = parse_number("42");
if (result) { std::cout << "Number: " << *result << std::endl; } else { std::cerr << "Error: " << result.error() << std::endl; }
// 使用 map 变换 auto doubled = result.transform([](int x) { return x * 2; });
// 使用 and_then 处理链式错误 auto chained = result.and_then([](int x) { return x > 0 ? std::expected<int, std::string>(x * 3) : std::unexpected("Value must be positive"); });
return 0;}11.7 错误码(传统方式)
Section titled “11.7 错误码(传统方式)”在嵌入式系统或性能关键代码中,有时会避免异常,使用错误码:
#include <cstdint>#include <optional>
// C 风格错误码enum class ErrorCode { Success = 0, NotFound = 1, InvalidInput = 2, OutOfMemory = 3};
ErrorCode find_user( const std::unordered_map<std::string, int>& users, const std::string& name, int& result) { // 输出参数
auto it = users.find(name); if (it == users.end()) { return ErrorCode::NotFound; } result = it->second; return ErrorCode::Success;}
// 使用int age;if (find_user(users, "Alice", age) == ErrorCode::Success) { std::cout << "Found: " << age << std::endl;}11.8 异常与性能
Section titled “11.8 异常与性能”异常处理的性能是一个经常被讨论的话题:
异常抛出时的开销
Section titled “异常抛出时的开销”// 正常路径:几乎没有开销try { int x = divide(10, 2); // 正常执行,几乎零开销}catch (...) { // 只有异常时才执行这段}
// 异常路径:显著开销// - 栈展开(stack unwinding)// - 销毁局部对象(调用析构函数)// - 查找catch子句// 可能有 1000x 以上的性能差异何时使用异常 vs 错误码
Section titled “何时使用异常 vs 错误码”使用异常:
- 预期失败是异常情况(不是正常流程的一部分)
- 错误信息需要传递到上层
- 需要完整的错误上下文
- 代码库已经大量使用异常
使用错误码 / std::optional:
- 失败是预期结果(如查找失败)
- 性能关键路径
- 嵌入式系统
- 需要明确控制错误传播
11.9 noexcept 与编译优化
Section titled “11.9 noexcept 与编译优化”noexcept 不仅是承诺,也是编译器优化的依据:
#include <vector>#include <iostream>
class Widget {public: Widget(int v) : value_(v) {}
// 标记移动操作为 noexcept Widget(Widget&& other) noexcept : value_(other.value_) { other.value_ = 0; }
Widget& operator=(Widget&& other) noexcept { if (this != &other) { value_ = other.value_; other.value_ = 0; } return *this; }
int value() const { return value_; }
private: int value_;};
int main() { std::vector<Widget> vec;
// 如果移动构造函数是 noexcept // vector 扩容时会使用 move 而不是 copy // 这是关键的性能优化!
for (int i = 0; i < 100; i++) { vec.push_back(Widget(i)); }
// 查看结果 for (const auto& w : vec) { std::cout << w.value() << " "; }
return 0;}运行时检查 noexcept
Section titled “运行时检查 noexcept”void maybe_throws() {}void never_throws() noexcept {}
int main() { // noexcept 运算符查询 std::cout << std::boolalpha; std::cout << "maybe_throws is noexcept: " << noexcept(maybe_throws()) << std::endl; // false std::cout << "never_throws is noexcept: " << noexcept(never_throws()) << std::endl; // true}- C++ 异常机制类似 Python,但使用
throw/catch/try语法 std::exception是所有标准异常的基类- RAII + 析构函数取代了
finally的作用 std::optional提供无异常的错误处理- 异常安全分为三个级别:基本保证、强保证、无抛出保证
noexcept标记函数不会抛出异常,也是移动语义的关键- C++23 的
std::expected结合了 optional 和错误值 - 选择异常还是错误码取决于场景和性能需求
下章预告:ch12 内存管理。