18 C++ 异常和捕获

C++ 异常和捕获

1. 传统方式的错误处理和C++的异常处理

传统方式:
逐层返回:对象能够被正确析构,流程控制复杂
远程跳转:流程控制简单,对象无法被正确析构
C++方式:
流程控制简单,对象能够被正确析构

2. 异常语法

  1. 抛出异常

    throw 异常对象;

  2. 捕获异常

try { // try块
      可能抛出异常的语句块;
      try {
      }
      catch (...) {
      }
}
catch (异常类型1 异常对象) { // catch块
      异常处理语句;
      try {
      }
      catch (...) {
      }
}
catch (异常类型2 异常对象) { // catch块
      异常处理语句;
}
...
catch (...) {
      异常处理语句;
}

3. 异常处理流程

  1. 抛出异常(throw)

    • 当代码检测到无法处理的错误时,使用throw抛出异常对象:

      if (error_condition) {
            throw std::runtime_error("Error message");  // 抛出标准异常类对象 
      }
      
      • 异常对象可以是基本类型、字符串或自定义类对象。
      • 抛出异常后,当前函数立即停止执行,进入异常处理流程。
  2. 栈展开(Stack Unwinding)

    • 程序从抛出异常的位置开始,沿函数调用链反向逐层退出栈帧,寻找匹配的catch块:
      • 若当前函数有try块包裹异常代码,优先检查其关联的catch块是否匹配异常类型。
      • 若未找到匹配的catch,则释放当前函数栈帧,继续向上层调用函数回溯查找。
      • 若回溯到main函数仍未找到匹配的catch,调用std::terminate()终止程序。
  3. 捕获异常(catch)

    • catch块按声明顺序检查类型匹配,仅第一个匹配的catch会被执行:

      try {
          // 可能抛出异常的代码 
      } catch (const std::exception& e) {  // 捕获标准异常 
          std::cerr << e.what();
      } catch (...) {  // 捕获所有未被处理的异常 
          std::cerr << "Unknown exception";
      }
      
    • 匹配规则:

      • 允许派生类异常被基类catch捕获(如std::runtime_error可被std::exception捕获)。
      • 严格类型匹配,不支持隐式类型转换(如int异常无法被double类型catch捕获)。
    • catch(...)用于捕获所有未明确处理的异常,通常作为最后一个catch块。

  4. 处理异常

    • 执行匹配的catch块内代码,处理错误(如日志记录、资源释放等)。
    • 若需重新抛出异常(如仅记录日志后继续传递),可用throw;语句:
    catch (const MyException& e) {
        log_error(e.what());
        throw;  // 重新抛出当前异常 
    }
    
  5. 恢复执行

    • 异常处理后,程序从最后一个catch块之后继续执行,而非回到抛出异常的位置。
    • 若异常未被处理(无匹配catch),程序终止。

[示例流程]

void func3() { throw 42; }          // 步骤1:抛出int异常 
void func2() { func3(); }          // 步骤2:栈展开,退出func2 
void func1() { func2(); }          // 步骤2:栈展开,退出func1 
 
int main() {
    try {
        func1();                    // 步骤2:栈展开到main的try块 
    } catch (int e) {              // 步骤3:匹配int类型异常 
        std::cout << "Caught: " << e;
    }                              // 步骤5:继续执行后续代码 
    return 0;
}

4. 标准异常类

C++标准库提供了一系列异常类(定义在<stdexcept>中),例如:

  • 逻辑错误:std::logic_errorstd::invalid_argument
  • 运行时错误:std::runtime_errorstd::out_of_range
  • 内存错误:std::bad_alloc(由new抛出)

自定义异常类通常继承自std::exception以保持一致性。

5. 异常说明符noexcept

函数标记为noexcept表示该函数保证不抛出任何异常(违反时程序直接终止)

1. 核心作用

  1. 优化代码生成
    编译器会为 noexcept 函数生成更简洁的异常处理代码,甚至完全省略异常栈展开逻辑:

    int add(int a, int b) noexcept {
        return a + b; // 基础运算不会抛异常
    }
    
  2. 影响容器行为(关键场景)
    当容器(如 vector)需要扩容时,若元素的移动构造函数标记为 noexcept,则会优先使用移动而非拷贝。

2. 语法形式

  1. 无条件声明
    void func() noexcept;
    表示该函数保证不抛出任何异常

  2. 条件声明
    void func() noexcept(expression);
    expressiontrue 时不抛异常(常用于模板)

  3. 运算符用法
    noexcept(expression) 返回布尔值,判断表达式是否可能抛异常

3. 基础用法示例

示例1:基本函数声明

int add(int a, int b) noexcept {
    return a + b; // 明确不抛出异常
}

示例2:移动构造函数

class MyData {
public:
    // 声明移动构造函数为noexcept
    MyData(MyData&& other) noexcept 
        : data_(std::move(other.data_)) {}
    
private:
    std::vector<int> data_;
};

// 当vector扩容时,会优先调用noexcept的移动构造函数
std::vector<MyData> vec;
vec.push_back(MyData()); // 触发移动而非复制

4. 高级用法

示例3:条件性noexcept

template<typename T>
void swap(T& a, T& b) noexcept(noexcept(T(std::move(a))) && 
                              noexcept(a.operator=(std::move(b)))) {
    T tmp = std::move(a);
    a = std::move(b);
    b = std::move(tmp);
}
  • 内层noexcept运算符检查表达式是否不会抛出异常。
  • 外层noexcept(...)根据内层结果动态决定函数是否为noexcept

示例4:虚函数中的限制

class Base {
public:
    virtual void foo() noexcept {} // 基类声明noexcept
};

class Derived : public Base {
public:
    void foo() override {} // 错误!派生类不能放宽异常规范
    // void foo() noexcept override {} // 正确
};

5. 关键注意事项

  1. 谨慎使用:若noexcept函数内部抛出异常,程序直接终止(如调用throw或调用可能抛出的函数)。
  2. 与STL协同:标准库算法(如std::sort)和容器对noexcept有特殊优化逻辑。
  3. 类型系统影响:函数指针的noexcept属性是类型的一部分(C++17起)。
  4. 如果一个函数的异常说明如下:
    void foo (void) throw () {...}
    表示该函数不抛出任何异常,属于已弃用标准。
    • void foo() throw(); 表示函数承诺不抛出任何异常。若违反(函数实际抛出异常),会调用std::unexpected(),默认终止程序,但允许通过set_unexpected()自定义处理。

    • C++11引入的noexcept 替代方案,更高效且明确。

    • void foo() noexcept;void foo() noexcept(true)(等价)。若违反(函数实际抛出异常),直接调用std::terminate()终止程序,无自定义处理机会。

    • throw() 旧规范无法触发优化,且因运行时检查(调用unexpected)可能引入额外开销。

    • throw() 仅支持无条件声明,灵活性差。noexcept支持条件表达式,可根据编译时条件声明是否抛出异常。

    • C++17后throw()完全移除,noexcept是唯一标准方式。throw()的遗留问题,新项目应避免。

5. 使用的注意事项

  1. 性能开销
    异常处理比普通返回码慢,应避免在频繁执行的代码路径中使用。

  2. 设计原则

    • 仅用于处理不可恢复的错误(如文件不存在、内存不足)。
    • 避免滥用异常控制正常流程。
  3. 如果是类类型的异常,catch时最好用引用。

    class MyException {...};
    throw MyException (...);
    catch (MyException& ex) {
          ...
    }
    
  4. 不要抛出局部变量(对象)的指针。

    MyException ex (...);
    ...
    if (...)
          throw &ex;
    ...
    catch (MyException* ex) {
          ...
    }
    
  5. 资源管理(RAII)
    栈展开时,局部对象的析构函数会被自动调用,确保资源(如内存、文件句柄)释放。

  6. 异常对象生命周期

    • throw会生成匿名临时对象,catch通过值、引用或指针接收该对象。
    • 若抛出局部对象,其拷贝可能被优化(如通过引用捕获)。
  7. 异常说明
    如果一个函数不带任何异常说明,表示它可以抛出任何异常。
    void foo (void) {...}
    如果一个函数被说明为只抛出有限的异常,那么它一旦抛出该范围之外的异常,此异常无法被调用者捕获。
    void foo (void) throw (int, MyException, string) {...}

  8. 子类中的覆盖版本不能比基类中的虚函数抛出更多的异常。

    class A {
          virtual void foo (void) throw (int, double) {...}
    };
    class B : public A {
          void foo (void) throw (int) {...}
    //    void foo (void) throw (string) {...} // errror
    //    void foo (void) {...} // error
    };
    
  9. 在类的构造函数中可以抛出异常,但是在抛出之前应该手动释放已分配的资源,因为这种对象是非完整对象,其析构函数不会执行。

    class A {
    public:
        A (void) {
        m_p = new char[1024];
        ...
        if (! fp) {
                delete[] m_p;
                throw 1;
        }
        }
        ~A (void) {
        delete[] m_p;
        }
    private:
        char* m_p;
        char m_data[1024];
    };
    try {
        A a;
        a.foo ();
    }
    catch (int ex) {
        ....
    }
    
  10. 永远不要在析构函数中抛出异常,也不要让异常从析构函数中被抛出。

    A::~A {
        try {
        ...
        }
        catch (...) {
        }
    }
    

6.异常的使用示例

// 通过异常机制表示出错
#include <iostream>
#include <cstdio>
using namespace std;
class A {
public:
    A (void) {
        cout << "A构造" << endl;
    }
    ~A (void) {
        cout << "A析构" << endl;
    }
};
void func3 (void) {
    A a;
    FILE* fp = fopen ("none.dat", "r");
    if (! fp)
        throw 1;
    // ...
    fclose (fp);
}
void func2 (void) {
    A a;
    func3 ();
    // ...
}
void func1 (void) {
    A a;
    func2 ();
    // ...
}
int main (void) {
    try {
        func1 ();
        // ...
    }
    catch (int ex) {
        if (ex == 1) {
            cout << "打开文件失败!" << endl;
            return -1;
        }
    }
    // ...
    return 0;
}

结果:
A构造
A构造
A构造
A析构
A析构
A析构
打开文件失败!
C++中,异常处理机制通过 `try` `catch` 语句实现,用于捕获处理运行时错误或异常情况。异常处理的核心思想是将正常代码逻辑与错误处理代码分离,从而提高代码的可读性可维护性。 ### 异常处理的基本语法 C++异常处理的基本结构如下: ```cpp try { // 可能抛出异常的代码 throw exception_type(); // 抛出异常 } catch (exception_type e) { // 处理异常的代码 } ``` - `try` 块用于包裹可能抛出异常的代码。 - `catch` 块用于捕获并处理异常。可以有多个 `catch` 块来处理不同类型的异常。 - `throw` 用于抛出异常。 ### 使用场景 1. **资源管理**:在需要确保资源(如文件句柄、网络连接等)正确释放的情况下,可以使用异常处理来捕获异常,并在 `catch` 块中执行清理操作。 2. **输入验证**:在处理用户输入或外部数据时,若输入不符合预期,可以抛出异常以通知调用者。 3. **系统错误处理**:例如文件无法打开、内存分配失败等情况下,抛出异常以便调用者处理。 4. **逻辑错误处理**:在程序逻辑出现错误时(如除以零),可以通过异常处理机制捕获并处理这些错误。 ### 示例代码 以下是一个简单的示例,展示了如何在C++中使用 `try` `catch` 来处理异常: ```cpp #include <iostream> #include <stdexcept> void divide(int a, int b) { if (b == 0) { throw std::runtime_error("Division by zero error"); } std::cout << "Result: " << a / b << std::endl; } int main() { try { divide(10, 0); } catch (const std::exception& e) { std::cerr << "Exception caught: " << e.what() << std::endl; } return 0; } ``` 在这个示例中,`divide` 函数会检查除数是否为零,如果是,则抛出一个 `std::runtime_error` 异常。在 `main` 函数中,`try` 块包裹了可能抛出异常的代码,`catch` 块捕获并处理该异常。 ### 异常处理的注意事项 1. **避免捕获所有异常**:虽然可以使用 `catch (...)` 来捕获所有异常,但这通常不是一个好做法,因为它会使调试变得更加困难。 2. **异常安全性**:在抛出异常时,确保程序的状态是安全的,资源已被正确释放。 3. **性能考虑**:异常处理机制在正常情况下不会影响性能,但在抛出异常时会有一定的性能开销。因此,异常应仅用于异常情况,而不是用于控制流。 4. **异常传播**:如果当前函数无法处理异常,可以通过 `throw;` 语句将异常重新抛出,让上层调用者处理。 ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值