73、深入探究 C++ 异常处理

深入探究 C++ 异常处理

1. 引言

在程序执行过程中,异常是问题出现的一种指示。异常处理能够让我们创建可以解决(或处理)异常的应用程序。在很多情况下,这使得程序可以像没有遇到问题一样继续执行。通过异常处理,我们能够编写健壮且容错的程序,这些程序可以处理问题并继续执行,或者优雅地终止。

下面是学习异常处理的一些目标:
- 使用 try catch throw 分别检测、处理和指示异常。
- 声明新的异常类。
- 了解栈展开如何使在一个作用域中未捕获的异常能在另一个作用域中被捕获。
- 处理新的失败情况。
- 使用 unique_ptr 防止内存泄漏。
- 理解标准异常层次结构。

2. 示例:处理除零异常

我们通过一个简单的示例来理解异常处理,该示例处理函数尝试除零时可能发生的异常。

2.1 商函数的实现

定义了一个名为 quotient 的函数,它接收用户输入的两个整数,并将第一个 int 参数除以第二个 int 参数。在进行除法之前,函数将第一个 int 参数的值转换为 double 类型,第二个 int 参数的值会隐式提升为 double 类型进行计算,最终返回一个 double 结果。

// perform division and throw DivideByZeroException object if 
// divide-by-zero exception occurs
double quotient( int numerator, int denominator )
{
    // throw DivideByZeroException if trying to divide by zero
    if ( denominator == 0 )
        throw DivideByZeroException(); // terminate function
    // return division result
    return static_cast< double >( numerator ) / denominator;
} // end function quotient
2.2 定义异常类

为了表示除零异常,我们定义了 DivideByZeroException 类,它是标准库类 runtime_error 的派生类。 runtime_error exception 的派生类,而 exception 是 C++ 标准库中异常的标准基类。

// Fig. 22.1: DivideByZeroException.h
// Class DivideByZeroException definition.
#include <stdexcept> // stdexcept header contains runtime_error 

// DivideByZeroException objects should be thrown by functions
// upon detecting division-by-zero exceptions
class DivideByZeroException : public std::runtime_error
{
public:
    // constructor specifies default error message
    DivideByZeroException()
        : std::runtime_error( "attempted to divide by zero" ) {}
}; // end class DivideByZeroException
2.3 异常处理的演示

main 函数中,我们使用异常处理来包裹可能抛出 DivideByZeroException 的代码,并处理该异常。

// Fig. 22.2: fig22_02.cpp
// Example that throws exceptions on 
// attempts to divide by zero.
#include <iostream>
#include "DivideByZeroException.h" // DivideByZeroException class
using namespace std;

int main()
{
    int number1; // user-specified numerator
    int number2; // user-specified denominator
    cout << "Enter two integers (end-of-file to end): ";

    // enable user to enter two integers to divide
    while ( cin >> number1 >> number2 ) 
    {
        // try block contains code that might throw exception     
        // and code that will not execute if an exception occurs
        try                                                       
        {                                                         
            double result = quotient( number1, number2 );
            cout << "The quotient is: " << result << endl;         
        } // end try                                              
        catch ( DivideByZeroException &divideByZeroException )    
        {                                                         
            cout << "Exception occurred: "                         
                 << divideByZeroException.what() << endl;            
        } // end catch
        cout << "\nEnter two integers (end-of-file to end): ";
    } // end while
    cout << endl;
} // end main
2.4 代码结构分析
  • try try 块(第 32 - 36 行)包裹了对 quotient 函数的调用和显示除法结果的语句。如果 quotient 函数抛出异常, try 块内后续的语句将不会执行。
  • catch 处理程序 catch 处理程序(第 37 - 41 行)必须紧跟在 try 块之后。异常参数应声明为对 catch 处理程序能处理的异常类型的引用,这样可以避免复制异常对象,并能正确捕获派生类异常。当 try 块中发生异常时,程序会寻找第一个类型匹配的 catch 处理程序。

以下是程序执行流程的 mermaid 流程图:

graph TD;
    A[开始] --> B[提示用户输入两个整数];
    B --> C{输入是否有效};
    C -- 是 --> D[调用 quotient 函数];
    D -- 分母不为 0 --> E[输出商];
    D -- 分母为 0 --> F[抛出 DivideByZeroException];
    F --> G[进入 catch 处理程序];
    G --> H[输出错误信息];
    E --> I[提示用户再次输入];
    H --> I;
    C -- 否 --> J[结束];
    I --> C;
3. 异常处理的终止模型

如果 try 块中的语句导致异常发生, try 块会立即终止。然后,程序会搜索第一个能处理该异常类型的 catch 处理程序。当找到匹配的 catch 处理程序时,其中的代码会执行。当 catch 处理程序处理完异常后,异常被视为已处理, catch 处理程序内定义的局部变量(包括 catch 参数)将超出作用域。程序控制不会返回到异常发生的点(即抛出点),而是从 try 块后面最后一个 catch 处理程序之后的第一条语句继续执行,这就是异常处理的终止模型。

4. 重新抛出异常

函数可能会使用资源(如文件),并且在发生异常时希望释放该资源。异常处理程序在接收到异常后,可以释放资源,然后通过 throw; 语句重新抛出异常,通知调用者发生了异常。

以下是重新抛出异常的示例代码:

// Fig. 22.3: fig22_03.cpp
// Rethrowing an exception.
#include <iostream>
#include <exception>
using namespace std;

// throw an exception and catch it in the same function
void throwException()
{
    // throw exception
    try 
    {
        cout << "Function throwException throws an exception\n";
        throw exception(); // generate exception
    } 
    catch ( exception & ) 
    {
        cout << "Exception handled in function throwException"
             << "\nFunction throwException rethrows exception";
        throw; // rethrow exception for further processing
    } 

    cout << "This also should not print\n";
} 

int main()
{
    // execute try block
    try 
    {
        cout << "\nmain invokes function throwException\n";
        throwException();
        cout << "This should not print\n";
    } 
    catch ( exception & ) 
    {
        cout << "\n\nException handled in main\n";
    } 

    cout << "Program control continues after catch in main\n";
} 

在这个示例中, throwException 函数中的 try 块抛出一个 exception 实例,其 catch 处理程序捕获该异常,打印错误信息并重新抛出异常。 main 函数中的 catch 处理程序捕获重新抛出的异常并打印错误信息。

表格:常见编程错误和预防提示
| 错误类型 | 描述 | 预防提示 |
| ---- | ---- | ---- |
| 语法错误 | 在 try 块和其对应的 catch 处理程序之间或 catch 处理程序之间放置代码 | 确保代码结构正确, catch 处理程序紧跟 try 块 |
| 语法错误 | 每个 catch 处理程序只能有一个参数,指定逗号分隔的异常参数列表是语法错误 | 只声明一个异常参数 |
| 编译错误 | 在单个 try 块后面的多个 catch 处理程序中捕获相同类型的异常 | 避免重复捕获相同类型的异常 |
| 逻辑错误 | 假设异常处理后控制会返回到抛出点之后的第一条语句 | 理解异常处理的终止模型 |
| 程序终止 | 在 catch 处理程序外执行空 throw 语句会放弃异常处理并立即终止程序 | 仅在 catch 处理程序内使用空 throw 语句 |

5. 栈展开

如果 try 块中发生的异常没有匹配的 catch 处理程序,或者异常发生在不在 try 块中的语句中,包含该语句的函数将立即终止,程序会尝试在调用函数中找到一个封闭的 try 块,这个过程称为栈展开。

下面通过一个简单的调用栈示例来说明栈展开的过程:
| 调用栈层级 | 函数 | 操作 |
| ---- | ---- | ---- |
| 1 | main | 调用 funcA |
| 2 | funcA | 调用 funcB |
| 3 | funcB | 抛出异常 |

funcB 抛出异常且自身没有合适的 catch 处理程序时, funcB 终止,控制返回 funcA 。如果 funcA 也没有合适的 catch 处理程序, funcA 终止,控制返回 main 。直到找到匹配的 catch 处理程序或者程序终止。

以下是栈展开过程的 mermaid 流程图:

graph TD;
    A[funcB 抛出异常] --> B{funcB 有匹配的 catch 处理程序?};
    B -- 否 --> C{funcA 有匹配的 catch 处理程序?};
    C -- 否 --> D{main 有匹配的 catch 处理程序?};
    D -- 否 --> E[程序终止];
    B -- 是 --> F[在 funcB 处理异常];
    C -- 是 --> G[在 funcA 处理异常];
    D -- 是 --> H[在 main 处理异常];
    F --> I[继续执行];
    G --> I;
    H --> I;
6. 何时使用异常处理
  • 提高代码健壮性 :异常处理可以让程序在遇到问题时继续执行,而不是崩溃。例如在文件操作中,如果文件打开失败,程序可以捕获异常并进行相应处理,而不是直接终止。
  • 统一错误处理 :在大型项目中,使用异常处理可以提供统一的错误处理机制,方便团队协作。
  • 分离错误处理逻辑 :将错误处理逻辑从正常业务逻辑中分离出来,使代码更清晰易读。
7. 构造函数、析构函数与异常处理

在构造函数中,如果发生异常,对象的构造过程会被中断,已经构造的成员对象和基类子对象会被正确析构。在析构函数中,通常不应该抛出异常,因为如果析构函数抛出异常且没有被捕获,程序会调用 std::terminate 终止。

以下是构造函数中异常处理的示例代码:

#include <iostream>
#include <stdexcept>

class MyClass {
public:
    MyClass(int value) {
        if (value < 0) {
            throw std::invalid_argument("Value must be non-negative");
        }
        // 正常构造逻辑
        std::cout << "Object constructed successfully" << std::endl;
    }
    ~MyClass() {
        std::cout << "Object destroyed" << std::endl;
    }
};

int main() {
    try {
        MyClass obj(-1);
    } catch (const std::invalid_argument& e) {
        std::cout << "Exception caught: " << e.what() << std::endl;
    }
    return 0;
}
8. 异常与继承

异常类可以通过继承形成层次结构。派生类异常可以被基类异常的 catch 处理程序捕获。在设计异常类时,合理使用继承可以提高代码的可维护性和扩展性。

以下是异常继承的示例代码:

#include <iostream>
#include <stdexcept>

class BaseException : public std::runtime_error {
public:
    BaseException(const std::string& message) : std::runtime_error(message) {}
};

class DerivedException : public BaseException {
public:
    DerivedException(const std::string& message) : BaseException(message) {}
};

void throwDerivedException() {
    throw DerivedException("Derived exception occurred");
}

int main() {
    try {
        throwDerivedException();
    } catch (const BaseException& e) {
        std::cout << "Caught base exception: " << e.what() << std::endl;
    }
    return 0;
}
9. 处理 new 失败

operator new 无法为对象分配内存时,会抛出 std::bad_alloc 异常。我们可以捕获这个异常并进行相应处理。

以下是处理 new 失败的示例代码:

#include <iostream>
#include <new>

int main() {
    try {
        while (true) {
            char* ptr = new char[1024 * 1024]; // 尝试分配 1MB 内存
        }
    } catch (const std::bad_alloc& e) {
        std::cout << "Memory allocation failed: " << e.what() << std::endl;
    }
    return 0;
}
10. unique_ptr 与动态内存分配

unique_ptr 是 C++ 标准库中的智能指针,它可以自动管理动态分配的内存,避免内存泄漏。当 unique_ptr 对象超出作用域时,它会自动释放所指向的内存。

以下是使用 unique_ptr 的示例代码:

#include <iostream>
#include <memory>

void useUniquePtr() {
    std::unique_ptr<int> ptr(new int(42));
    std::cout << "Value: " << *ptr << std::endl;
    // 当 ptr 超出作用域时,内存会自动释放
}

int main() {
    useUniquePtr();
    return 0;
}
11. 标准库异常层次结构

C++ 标准库定义了一个异常类的层次结构, std::exception 是所有标准异常类的基类。常见的派生类包括 std::runtime_error std::logic_error 等。

以下是标准库异常层次结构的简单表格:
| 基类 | 派生类 | 描述 |
| ---- | ---- | ---- |
| std::exception | std::runtime_error | 表示运行时错误 |
| std::exception | std::logic_error | 表示逻辑错误 |
| std::runtime_error | std::overflow_error | 表示溢出错误 |
| std::runtime_error | std::underflow_error | 表示下溢错误 |

12. 总结

通过本文,我们深入学习了 C++ 异常处理的各个方面,包括异常的检测、处理、重新抛出,以及栈展开、构造函数和析构函数中的异常处理等。同时,我们还了解了标准库异常层次结构和如何使用 unique_ptr 防止内存泄漏。合理使用异常处理可以让我们编写更健壮、容错的 C++ 程序。

在实际编程中,我们应该根据具体情况选择合适的异常处理策略,将异常处理融入到系统设计中,提高代码的质量和可维护性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值