C++ 异常处理

异常(exception)是 C++ 处理运行时错误的机制。当函数遇到无法处理的错误(文件打不开、网络断、除零),它可以"抛出"异常,由调用链上某处"捕获"处理。这一章我们看 C++ 异常的全部要点。

一、为什么需要异常?

不用异常,处理错误是这种风格:

异常的优点:

异常的缺点:

二、基本用法:throw / try / catch

#include <iostream>
#include <stdexcept>
using namespace std;

double divide(int a, int b) {
    if (b == 0) {
        // throw 抛出一个异常对象
        throw runtime_error("除零错误!");
    }
    return static_cast<double>(a) / b;
}

int main() {
    try {
        // 可能抛异常的代码
        cout << divide(10, 2) << endl;   // 5
        cout << divide(10, 0) << endl;   // 抛异常
        cout << "这里不会执行" << endl;
    }
    catch (const runtime_error& e) {
        // 捕获并处理
        cout << "捕获异常: " << e.what() << endl;
    }
    cout << "程序继续..." << endl;
    return 0;
}
// 输出:
// 5
// 捕获异常: 除零错误!
// 程序继续...

关键点:

三、多重 catch 与通配

#include <iostream>
#include <stdexcept>
using namespace std;

void risky(int x) {
    if (x == 0) throw runtime_error("zero");
    if (x < 0) throw invalid_argument("negative");
    if (x > 100) throw out_of_range("too big");
    cout << "OK: " << x << endl;
}

int main() {
    int arr[] = {1, 0, -5, 200};
    for (int x : arr) {
        try {
            risky(x);
        }
        catch (const runtime_error& e) {
            cout << "runtime_error: " << e.what() << endl;
        }
        catch (const invalid_argument& e) {
            cout << "invalid_argument: " << e.what() << endl;
        }
        catch (const out_of_range& e) {
            cout << "out_of_range: " << e.what() << endl;
        }
        catch (...) {        // 捕获所有其他类型(通配)
            cout << "未知异常" << endl;
        }
    }
    return 0;
}

四、标准异常体系

C++ 标准库提供了一组异常类,全在 <stdexcept> 头文件:

#include <iostream>
#include <stdexcept>
using namespace std;

// C++ 标准异常体系(<stdexcept>):
// std::exception                   所有标准异常的基类
//  ├── logic_error                 程序逻辑错误(可避免)
//  │    ├── invalid_argument       参数非法
//  │    ├── domain_error           域错误
//  │    ├── length_error           长度超限
//  │    └── out_of_range           下标越界
//  ├── runtime_error               运行时错误(难以避免)
//  │    ├── overflow_error         算术上溢
//  │    ├── underflow_error        算术下溢
//  │    └── range_error            范围错误
//  └── bad_alloc                   new 分配失败
//       bad_cast                   dynamic_cast 失败
//       bad_typeid                 typeid 失败

int main() {
    try {
        // 各种抛标准异常的操作
        throw out_of_range("越界");
    }
    // 子类异常必须在前,父类在后(否则永远捕到父类)
    catch (const out_of_range& e) {
        cout << "out_of_range: " << e.what() << endl;
    }
    catch (const logic_error& e) {        // 父类
        cout << "logic_error: " << e.what() << endl;
    }
    catch (const exception& e) {          // 顶层基类
        cout << "exception: " << e.what() << endl;
    }
    return 0;
}

常用标准异常:

推荐用 runtime_error 作为自定义异常的基类——它有 what() 方法返回错误信息,够用。

五、自定义异常

继承 std::runtime_errorstd::exception,加自己的字段:

#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;

// 自定义异常:继承 std::runtime_error 或 std::exception
class DatabaseError : public runtime_error {
    int errorCode;
public:
    DatabaseError(const string& msg, int code)
        : runtime_error(msg), errorCode(code) {}

    int code() const { return errorCode; }
};

class FileNotFoundError : public runtime_error {
    string path;
public:
    FileNotFoundError(const string& p)
        : runtime_error("File not found: " + p), path(p) {}

    const string& getPath() const { return path; }
};

int main() {
    try {
        throw DatabaseError("连接超时", 500);
    }
    catch (const DatabaseError& e) {
        cout << "DB 错误 [" << e.code() << "]: " << e.what() << endl;
    }

    try {
        throw FileNotFoundError("/etc/config");
    }
    catch (const FileNotFoundError& e) {
        cout << e.what() << " (path: " << e.getPath() << ")" << endl;
    }
    return 0;
}

自定义异常的关键点:

六、noexcept:承诺不抛异常(C++11)

#include <iostream>
using namespace std;

// noexcept(C++11):承诺函数不会抛异常
void safeFunction() noexcept {
    cout << "我不会抛异常" << endl;
    // throw runtime_error("oops");   // 调 terminate,程序直接挂
}

// noexcept(false):可能抛(默认)
void riskyFunction() {
    throw runtime_error("oops");
}

// noexcept(condition):根据条件决定
template <typename T>
void swapValues(T& a, T& b) noexcept(noexcept(a = b)) {
    T tmp = a;
    a = b;
    b = tmp;
}

// 为什么用 noexcept?
// 1. 编译器优化:知道不抛异常,可以做更多优化
// 2. 标准库要求:vector 在重新分配时,如果元素类型是 noexcept 移动,
//    就用移动(快),否则用拷贝(慢但安全)

int main() {
    static_assert(noexcept(safeFunction()), "should be noexcept");
    cout << "noexcept check passed" << endl;
    return 0;
}

noexcept 的意义:

七、重新抛出与异常包装

有时你想"先处理一下,再让上层继续处理":

#include <iostream>
#include <stdexcept>
using namespace std;

void inner() {
    throw runtime_error("原始异常");
}

void middle() {
    try {
        inner();
    }
    catch (const runtime_error& e) {
        cout << "middle 捕获: " << e.what() << endl;
        // 处理后可以重新抛出,让外层继续处理
        throw;                  // 不带参数:重新抛出当前异常
        // throw runtime_error("包装: "s + e.what());  // 也可以包装后抛新异常
    }
}

int main() {
    try {
        middle();
    }
    catch (const exception& e) {
        cout << "outer 捕获: " << e.what() << endl;
    }
    return 0;
}
// 输出:
// middle 捕获: 原始异常
// outer 捕获: 原始异常

八、异常安全(Exception Safety)

"异常安全"指代码在抛异常时的资源/数据完整性。有四个等级:

#include <iostream>
#include <memory>
using namespace std;

// 异常安全(Exception Safety)的四个等级:
// 1. 无异常保证(No guarantee):出异常可能资源泄漏、数据破坏
// 2. 基本保证(Basic):出异常不会泄漏、不会破坏,但程序状态可能改变
// 3. 强保证(Strong):出异常时程序状态回滚到调用前(事务性)
// 4. 不抛保证(No-throw):绝不抛异常(noexcept)

// 反面教材:不安全
class Bad {
    int* a;
    int* b;
public:
    Bad() : a(new int(1)), b(new int(2)) {}
    ~Bad() { delete a; delete b; }

    void reset() {
        delete a;
        a = new int(0);       // 若 new 抛异常,a 已 delete 但未置 nullptr
        delete b;             // 析构时再 delete a 是 double free!
        b = new int(0);
    }
};

// 正面教材:强异常安全(copy-and-swap)
class Good {
    unique_ptr<int> a;
    unique_ptr<int> b;
public:
    Good() : a(make_unique<int>(1)), b(make_unique<int>(2)) {}

    void reset() {
        auto newA = make_unique<int>(0);   // 先在临时变量上构造
        auto newB = make_unique<int>(0);   // 若这里抛异常,原 a/b 未受影响
        a = move(newA);                    // 不抛(移动)
        b = move(newB);                    // 不抛
    }
};

四个异常安全等级:

实现强保证的常用手法:

九、何时用异常,何时用错误码?

这是 C++ 社区长期争论的话题。经验法则:

// 异常 vs 错误码:何时用异常?

// 用异常的情况:
// 1. 真正"异常"的情况(失败的预期很低):文件打不开、网络断、内存不足
// 2. 错误会传播多层:用异常可以避免每层都 if (err) return;
// 3. 构造函数失败:构造函数没有返回值,只能用异常
// 4. 不能继续执行的错误:解析 JSON 失败、配置格式错误

// 用错误码/返回值的情况:
// 1. 预期会发生的情况:用户输入错误、文件已存在
// 2. 性能关键路径:异常有性能开销(尽管现代 C++ 已经优化很多)
// 3. 嵌入式/游戏引擎:很多团队禁用异常(关 -fno-exceptions 提速)
// 4. 跨语言边界:extern "C" API 不能抛异常

// 经验法则:
// - 业务应用、脚本、CLI:用异常
// - 高频交易、游戏引擎、内核:用错误码
// - 库的对外 API:看目标用户(谷歌 C++ 风格禁用异常)

// C++23 引入了 std::expected<T, E>(类似 Rust 的 Result)
// 提供了"错误码 + 类型安全"的折中方案,无需用异常。
#include <expected>
std::expected<int, std::string> parse(const std::string& s);

十、常见陷阱

小结

这一章你掌握了 C++ 异常处理:throw/try/catch 基本用法、多重 catch、标准异常体系、自定义异常、noexcept、异常安全四等级、何时用异常何时用错误码。下一篇我们看 C++ 的文件 IO——怎么读写文件。

← 上一篇 C++ 内存管理

下一篇 C++ 文件 IO

✈️💬