在前面 12 节中,我们学习了变量、函数、类、指针、文件操作、模板和 STL。这些都是「怎么写代码」的知识。今天,我们来学习一个同样重要但经常被初学者忽略的主题——异常处理。它解决的是「代码出错了怎么办」的问题。

1. 为什么需要异常处理?

程序运行时,总会遇到各种意外情况:文件找不到、内存不够用、数组越界、除以零……这些情况如果不处理,程序就会直接崩溃。

你可能会说:用 if 判断一下不就行了?确实可以,但 if 有一个局限——它只能处理「当前函数」里的错误。如果错误发生在深层嵌套的函数调用中,你需要一层一层地把错误信息往上传,非常麻烦。

异常处理提供了一种更优雅的方式:在出错的地方「抛出」异常,在合适的地方「捕获」它。 错误信息会自动沿着调用链向上传递,直到被处理。

2. 基本语法:try、catch、throw

C++ 的异常处理由三个关键字组成:

  • try:包裹可能出错的代码
  • throw:当检测到错误时,「抛出」一个异常
  • catch:「捕获」异常并处理

2.1 最简单的例子

#include <iostream>
using namespace std;

int divide(int a, int b) {
    if (b == 0) {
        throw "除数不能为零!";  // 抛出异常
    }
    return a / b;
}

int main() {
    try {
        cout << divide(10, 2) << endl;   // 正常执行
        cout << divide(10, 0) << endl;   // 会触发异常
        cout << "这行不会被执行" << endl; // 异常后跳过
    } catch (const char* msg) {
        cout << "捕获到异常:" << msg << endl;
    }

    cout << "程序继续正常运行" << endl;
    return 0;
}

输出:

5
捕获到异常:除数不能为零!
程序继续正常运行

几个关键点:

  • throw 抛出异常后,try 块中剩余的代码不会被执行,程序直接跳转到对应的 catch 块。
  • catch 处理完异常后,程序继续往下执行,不会崩溃。
  • throw 后面可以跟任何类型的值:字符串、整数、自定义对象等。

2.2 多个 catch 块

你可以针对不同类型的异常写不同的处理逻辑:

#include <iostream>
using namespace std;

void process(int type) {
    if (type == 1) throw 42;
    if (type == 2) throw "出错了";
    if (type == 3) throw 3.14;
}

int main() {
    for (int i = 1; i <= 3; i++) {
        try {
            process(i);
        } catch (int e) {
            cout << "整数异常:" << e << endl;
        } catch (const char* e) {
            cout << "字符串异常:" << e << endl;
        } catch (double e) {
            cout << "浮点异常:" << e << endl;
        }
    }
    return 0;
}

输出:

整数异常:42
字符串异常:出错了
浮点异常:3.14

catch 块会按照书写的顺序匹配异常类型,只会执行第一个匹配的 catch

3. 标准异常类

在实际开发中,我们通常不会 throw 裸字符串或整数,而是使用 C++ 标准库提供的异常类。它们都在 <stdexcept> 头文件中。

3.1 常见的标准异常

异常类 用途
std::runtime_error 运行时错误(如文件不存在)
std::invalid_argument 无效参数
std::out_of_range 越界访问
std::overflow_error 算术溢出
std::logic_error 逻辑错误

3.2 使用标准异常

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

int getElement(const vector<int>& vec, int index) {
    if (index < 0 || index >= vec.size()) {
        throw out_of_range("下标 " + to_string(index) + " 越界!");
    }
    return vec[index];
}

int main() {
    vector<int> nums = {10, 20, 30};

    try {
        cout << getElement(nums, 1) << endl;   // 20
        cout << getElement(nums, 5) << endl;   // 越界!
    } catch (const out_of_range& e) {
        cout << "异常:" << e.what() << endl;
    }

    return 0;
}

输出:

20
异常:下标 5 越界!

标准异常类都有一个 what() 方法,返回错误描述信息。用 const 引用const exception&)来捕获是一个好习惯,可以避免不必要的拷贝。

3.3 统一捕获所有异常

如果不确定会抛出什么类型的异常,可以用 catch (...) 捕获所有异常:

try {
    // 可能出错的代码
} catch (const exception& e) {
    cout << "标准异常:" << e.what() << endl;
} catch (...) {
    cout << "未知异常" << endl;
}

建议把 catch (const exception&) 放在前面,catch (...) 放在最后作为兜底。

4. 自定义异常类

当标准异常类不能满足需求时,你可以定义自己的异常类。通常的做法是继承 std::exception

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

class MyException : public exception {
private:
    string message;
public:
    MyException(const string& msg) : message(msg) {}
    const char* what() const noexcept override {
        return message.c_str();
    }
};

void riskyOperation() {
    throw MyException("自定义异常:操作失败!");
}

int main() {
    try {
        riskyOperation();
    } catch (const MyException& e) {
        cout << e.what() << endl;
    }
    return 0;
}

输出:

自定义异常:操作失败!

自定义异常的好处是你可以携带更多的上下文信息(比如错误代码、发生位置等),方便调试和日志记录。

5. 异常的传播机制

理解异常是怎么「传递」的,对于正确使用异常处理至关重要。

5.1 调用链中的异常传播

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

void funcC() {
    throw runtime_error("funcC 中出错了");
}

void funcB() {
    funcC();  // 不处理,继续向上传
}

void funcA() {
    try {
        funcB();  // 不处理,继续向上传
    } catch (const runtime_error& e) {
        cout << "funcA 捕获:" << e.what() << endl;
    }
}

int main() {
    funcA();
    return 0;
}

输出:

funcA 捕获:funcC 中出错了

异常从 funcC 抛出,经过 funcB(没有 catch),最终在 funcA 中被捕获。异常会沿着调用链自动向上传递,直到找到匹配的 catch 块。

5.2 没有被捕获的异常

如果异常一路传到 main 函数都没有被捕获,程序会调用 std::terminate() 直接终止,并可能弹出系统级的错误提示。所以一定要确保所有可能抛出异常的地方都有对应的 catch 处理。

6. RAII:C++ 资源管理的黄金法则

异常处理有一个容易被忽视的问题:资源泄漏。如果在 try 块中申请了内存或打开了文件,异常发生后这些资源可能不会被释放。

C++ 的解决方案是 RAII(Resource Acquisition Is Initialization)——把资源的生命周期绑定到对象的生命周期上。当对象离开作用域时,析构函数会自动释放资源,即使是因为异常导致的离开。

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

void processFile(const string& filename) {
    ifstream file(filename);  // RAII:文件在构造时打开
    if (!file.is_open()) {
        throw runtime_error("无法打开文件:" + filename);
    }

    string line;
    while (getline(file, line)) {
        cout << line << endl;
    }

    // 函数结束时,file 的析构函数自动关闭文件
    // 即使中途抛出异常,析构函数也会被调用
}

int main() {
    try {
        processFile("test.txt");
    } catch (const runtime_error& e) {
        cout << "错误:" << e.what() << endl;
    }
    return 0;
}

STL 容器(vectorstring 等)和智能指针(unique_ptrshared_ptr)都遵循 RAII 原则。在 C++ 中,优先使用 RAII 管理资源,而不是手动 new/deleteopen/close

7. 使用异常的最佳实践

7.1 什么时候该用异常

  • 真正的异常情况:文件不存在、网络断开、内存不足等不可预期的错误
  • 不适合正常流程控制:不要用异常来代替 if-else 判断

7.2 异常安全的三个级别

级别 保证 说明
基本保证 程序不会泄漏资源 最低要求
强保证 操作要么完全成功,要么回到操作前的状态 事务性
不抛出保证 函数保证不抛出异常 noexcept 声明

7.3 使用 noexcept 标记不抛出异常的函数

如果你确定某个函数不会抛出异常,可以用 noexcept 声明,帮助编译器做更好的优化:

int add(int a, int b) noexcept {
    return a + b;
}

析构函数默认就是 noexcept 的,你不应该在析构函数中抛出异常。

8. 总结

这一节我们学习了 C++ 的异常处理机制:

  • try 包裹可能出错的代码,throw 抛出异常,catch 捕获并处理。
  • 标准异常类(runtime_errorout_of_range 等)提供了统一的错误描述接口。
  • 异常沿着调用链自动向上传递,直到被 catch 捕获。
  • RAII 是 C++ 资源管理的核心原则,确保异常发生时资源不会泄漏。
  • 异常只用于处理真正的异常情况,不要用来做流程控制。

异常处理是编写健壮程序的重要保障。掌握了它,你的代码就能在面对意外情况时「优雅地失败」,而不是直接崩溃。下一节我们将继续探索 C++ 的更多高级特性。加油!

更多推荐