Skip to content

Ch 11: 错误处理与异常

  • 掌握异常处理机制
  • 理解异常安全级别
  • 学会使用 std::optional 错误处理
  • 设计良好的异常层次
  • 理解 noexcept 的作用和性能影响

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 等。

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;
}
特性PythonC++
异常基类BaseException / Exceptionstd::exception
捕获语法except Type as ecatch (const Type& e)
重新抛出raise 或 raise ethrow;
强制清理finallyRAII + 析构函数
异常规格无noexcept 标记

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_;
};

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

在实际项目中,建议创建自定义异常层次:

#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;
}

异常安全是 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_;
};

异常发生后,对象仍然处于有效状态,没有资源泄漏。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"; // 这行可能不执行
}
};

最实用的安全级别。异常发生后,程序状态与调用前完全相同,即”事务性语义”。

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 的异常安全主要依靠 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(); // 可能抛出
} // 析构函数自动调用,即使异常发生

对于可能”没有值”的情况,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 None
#include <optional>
#include <unordered_map>
#include <iostream>
// 函数返回 optional
std::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;
}
#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; });
场景std::optional异常
预期失败(正常流程)适用不推荐
意外错误(bug)不推荐适用
性能敏感路径更快(无栈展开)有开销
需要错误信息不适合适合
预期”无值”最佳选择可以
// 场景 1: 预期失败 - 用 optional
std::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;
}

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;
}

在嵌入式系统或性能关键代码中,有时会避免异常,使用错误码:

#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;
}

异常处理的性能是一个经常被讨论的话题:

// 正常路径:几乎没有开销
try {
int x = divide(10, 2); // 正常执行,几乎零开销
}
catch (...) {
// 只有异常时才执行这段
}
// 异常路径:显著开销
// - 栈展开(stack unwinding)
// - 销毁局部对象(调用析构函数)
// - 查找catch子句
// 可能有 1000x 以上的性能差异

使用异常:

  • 预期失败是异常情况(不是正常流程的一部分)
  • 错误信息需要传递到上层
  • 需要完整的错误上下文
  • 代码库已经大量使用异常

使用错误码 / std::optional:

  • 失败是预期结果(如查找失败)
  • 性能关键路径
  • 嵌入式系统
  • 需要明确控制错误传播

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;
}
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 内存管理。