文章

C++异常处理

C++异常处理

C++异常处理

C++ 的异常处理机制是通过 trythrowcatch 三个关键字来实现的,其设计目的是在程序发生错误时提供一种清晰的处理流程,而不是像 C 一样靠返回值或错误码。

基本语法结构

1
2
3
4
5
6
7
8
9
10
11
try {
    // 可能抛出异常的代码
    ...
    throw 异常对象; // 抛出异常
} catch (异常类型1 变量名) {
    // 处理异常类型1
} catch (异常类型2 变量名) {
    // 处理异常类型2
} catch (...) {
    // 捕获所有异常
}

关键字解释

1. try

用于包裹可能发生异常的代码块。

2. throw

用于抛出异常,可以抛出任意类型(如整型、字符串、对象等)。例如:

1
2
3
throw 1;                      // 抛出 int 类型
throw "Error occurred";       // 抛出 const char* 类型
throw std::runtime_error("xx"); // 抛出异常类对象

3. catch

用于捕获异常。参数的类型决定了它能捕获哪类异常(匹配类型或其子类)。catch(...) 可用于捕获所有异常。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <stdexcept>

void mightFail(bool shouldThrow) {
    if (shouldThrow) {
        throw std::runtime_error("Something went wrong!");
    }
}

int main() {
    try {
        mightFail(true);
    } catch (const std::runtime_error& e) {
        std::cout << "Caught a runtime error: " << e.what() << std::endl;
    } catch (...) {
        std::cout << "Caught some other exception." << std::endl;
    }
    return 0;
}

异常类体系

C++ 标准库提供了许多内置的异常类,它们都继承自 std::exception,常见的有:

异常类描述
std::exception所有标准异常的基类
std::runtime_error表示运行时错误
std::logic_error表示逻辑错误(如非法参数等)
std::bad_alloc内存分配失败
std::out_of_range越界访问
std::invalid_argument非法参数

这些类都支持 what() 方法返回描述信息。

注意事项

1. 异常匹配机制

C++ 使用“从上到下”按类型匹配 catch,一旦匹配成功则不再继续向下匹配。

2. 异常对象的拷贝

异常对象是被复制到 catch 块中的,通常使用引用(如 const std::exception&)避免拷贝开销。

3. 析构函数抛异常的问题

如果析构函数抛异常,可能会导致程序在异常传播时终止(特别是在栈展开过程中已抛出异常的情况下),建议析构函数不抛异常

C++11/17 的补充说明

1. noexcept

用于声明函数不会抛异常,编译器可进行优化:

1
void func() noexcept;

2.throw()(已弃用)

C++98 的异常说明符(如 void f() throw(int);)在 C++11 后已弃用。

自定义异常类

自定义异常类并重写 what() 方法:

1
2
3
4
5
6
7
8
class MyError : public std::exception {
    std::string msg;
public:
    MyError(const std::string& m) : msg(m) {}
    const char* what() const noexcept override {
        return msg.c_str();
    }
};
1
2
3
4
5
try {
    throw MyError("网络连接失败");
} catch (const std::exception& e) {
    std::cout << e.what() << std::endl;
}

异常与资源管理

推荐使用 RAII(资源获取即初始化)来管理资源,这样即使发生异常,资源也能自动释放。例如使用 std::vectorstd::unique_ptr 管理内存资源。

RAII 是一种 C++ 中非常重要的资源管理策略,它利用对象生命周期(构造/析构)自动处理资源,避免手动 new/delete,从而防止内存泄漏、文件未关闭、死锁等问题。

RAII + 异常机制 = 安全 + 简洁 + 高效 是 C++ 最强大的组合之一。

C++析构函数可以抛出异常吗

C++ 中,不建议也几乎不允许析构函数抛出异常。虽然语言标准并没有完全禁止析构函数中抛出异常,但这么做会非常危险,主要原因如下:

1. 析构函数抛异常可能导致程序崩溃

如果一个对象在栈展开(stack unwinding)过程中被销毁(即已经在处理另一个异常),又有析构函数抛出异常,那么 C++ 会调用 std::terminate(),导致程序崩溃

栈展开就是异常抛出时,C++ 自动调用所有已经构造的对象的析构函数,从而逐层清理函数调用栈。

展开的过程 = 自动清理资源(通过调用析构函数)

  • 谁先构造,谁后析构(后进先出,LIFO)
  • 每层函数退出时,所有局部对象都会被销毁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <stdexcept>

class A {
public:
    ~A() {
        std::cout << "A::~A()" << std::endl;
        throw std::runtime_error("Error in destructor");
    }
};

void test() {
    A a;
    throw std::runtime_error("Original exception");
}

int main() {
    try {
        test();
    } catch (const std::exception& e) {
        std::cout << "Caught: " << e.what() << std::endl;
    }
}

输出:

A::~A()
terminate called after throwing an instance of 'std::runtime_error'

程序终止,catch 根本没来得及处理。

2. 正确的做法:捕获并处理析构函数中的异常

如果析构函数中确实可能发生异常,必须捕获并在内部处理绝不能让异常传播出析构函数

1
2
3
4
5
6
7
8
9
10
class A {
public:
    ~A() {
        try {
            // 可能抛异常的代码
        } catch (const std::exception& e) {
            // 记录日志或采取补救措施
        }
    }
};
本文由作者按照 CC BY 4.0 进行授权