75、C++ 异常处理深度剖析

C++ 异常处理深度剖析

1. 异常处理概述

异常是程序执行过程中出现问题的一种指示。异常处理能够让我们创建出可以解决运行时问题的程序,在很多情况下,程序能继续执行,就像没有遇到问题一样。对于更严重的问题,程序可能需要在有控制地终止前通知用户。

异常处理的一些关键概念如下:
- 异常(Exception) :程序执行时出现的问题指示。
- 异常处理(Exception Handling) :解决运行时问题,使程序能继续执行或有控制地终止。
- 标准基类(Standard Base Class) exception 类是异常类的标准基类,它提供了虚函数 what ,派生类可以重写该函数以发出适当的错误消息。
- 运行时错误基类(Runtime Error Base Class) runtime_error 类定义在 <stdexcept> 头文件中,是表示运行时错误的 C++ 标准基类。
- 终止模型(Termination Model) :C++ 使用终止模型进行异常处理。

2. 处理除零异常示例

以处理除零异常为例,介绍异常处理的基本流程:

#include <iostream>
#include <stdexcept>

double divide(double numerator, double denominator) {
    if (denominator == 0) {
        throw std::runtime_error("Division by zero!");
    }
    return numerator / denominator;
}

int main() {
    try {
        double result = divide(10, 0);
        std::cout << "Result: " << result << std::endl;
    } catch (const std::runtime_error& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }
    return 0;
}

上述代码的执行流程如下:
1. divide 函数检查分母是否为零,如果为零则抛出 std::runtime_error 异常。
2. main 函数中的 try 块调用 divide 函数。
3. 如果抛出异常, try 块终止,程序控制转移到匹配的 catch 处理程序。
4. catch 处理程序捕获异常并输出错误消息。

3. 异常处理的关键组件
  • try 块 :由 try 关键字后跟花括号 {} 组成,包含可能抛出异常的代码。如果异常发生, try 块终止,程序控制转移到匹配的 catch 处理程序。
  • catch 处理程序 :必须紧跟在 try 块之后,每个 catch 处理程序指定一个异常参数,表示该处理程序可以处理的异常类型。如果异常参数包含可选的参数名, catch 处理程序可以使用该参数名与捕获的异常对象进行交互。
  • 抛出点(Throw Point) :程序中抛出异常的点。
  • 栈展开(Stack Unwinding) :如果 try 块中发生的异常没有匹配的 catch 处理程序,或者异常发生在没有 try 块的语句中,包含该语句的函数立即终止,程序会尝试在调用函数中找到一个包含的 try 块。这个过程称为栈展开。
  • 抛出异常 :使用 throw 关键字后跟一个表示要抛出的异常类型的操作数。 throw 的操作数可以是任何类型。
4. 异常的重抛

异常处理程序可以将异常处理(或部分处理)推迟到另一个异常处理程序。可以通过重抛异常来实现这一点。例如:

#include <iostream>
#include <stdexcept>

void func() {
    try {
        throw std::runtime_error("Something went wrong!");
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught in func: " << e.what() << std::endl;
        throw; // 重抛异常
    }
}

int main() {
    try {
        func();
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught in main: " << e.what() << std::endl;
    }
    return 0;
}

在上述代码中, func 函数捕获异常后重抛, main 函数再次捕获该异常。

5. 栈展开的详细解释

栈展开意味着在未捕获异常的函数中,该函数终止,该函数中的所有局部变量被销毁,控制返回到最初调用该函数的语句。例如:

#include <iostream>
#include <stdexcept>

void nestedFunction() {
    throw std::runtime_error("Exception in nestedFunction");
}

void outerFunction() {
    try {
        nestedFunction();
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught in outerFunction: " << e.what() << std::endl;
    }
}

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

在这个例子中, nestedFunction 抛出异常, outerFunction 中的 try 块捕获该异常, nestedFunction 终止,其局部变量被销毁。

6. 何时使用异常处理
  • 同步错误(Synchronous Errors) :异常处理适用于同步错误,即语句执行时发生的错误。
  • 异步事件(Asynchronous Events) :异常处理不适合处理与异步事件相关的错误,异步事件与程序的控制流并行且独立发生。
  • noexcept 声明 :从 C++11 开始,如果一个函数不抛出任何异常且不调用任何抛出异常的函数,应该显式声明该函数为 noexcept
7. 构造函数、析构函数与异常处理
  • 构造函数抛出异常 :构造函数抛出的异常会导致在异常抛出之前作为正在构造的对象的一部分构建的任何对象的析构函数被调用。
  • 自动对象析构 try 块中构造的每个自动对象在异常抛出之前被析构。
  • 栈展开完成 :在异常处理程序开始执行之前,栈展开完成。
  • 析构函数抛出异常 :如果由于栈展开而调用的析构函数抛出异常,程序将终止。
  • 成员对象析构 :如果对象有成员对象,并且在外部对象完全构造之前抛出异常,则会为在异常发生之前已经构造的成员对象执行析构函数。
  • 数组对象析构 :如果数组对象在部分构造时发生异常,只会调用已构造的数组元素对象的析构函数。
  • 动态内存释放 :当从 new 表达式创建的对象的构造函数中抛出异常时,为该对象动态分配的内存会被释放。
8. 异常与继承

如果 catch 处理程序捕获基类类型的异常对象的引用,它也可以捕获从该基类公开派生的所有类的对象的引用,这允许对相关错误进行多态处理。例如:

#include <iostream>
#include <stdexcept>

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

int main() {
    try {
        throw DerivedError("Derived error occurred");
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught base class exception: " << e.what() << std::endl;
    }
    return 0;
}

在上述代码中, catch 处理程序捕获 DerivedError 异常,因为 DerivedError std::runtime_error 的派生类。

9. 处理 new 失败

new 运算符失败时,它会抛出 bad_alloc 异常,该异常定义在 <new> 头文件中。可以使用 set_new_handler 函数注册一个新的处理程序,当 new 失败时调用该处理程序。例如:

#include <iostream>
#include <new>

void outOfMemory() {
    std::cerr << "Out of memory!" << std::endl;
    std::abort();
}

int main() {
    std::set_new_handler(outOfMemory);
    try {
        while (true) {
            new int[1000000];
        }
    } catch (const std::bad_alloc& e) {
        std::cerr << "Caught bad_alloc: " << e.what() << std::endl;
    }
    return 0;
}

上述代码的执行流程如下:
1. outOfMemory 函数是 new 失败时调用的处理程序。
2. std::set_new_handler(outOfMemory) 注册 outOfMemory 函数为新的处理程序。
3. try 块中不断分配内存,直到 new 失败。
4. 如果 new 失败, outOfMemory 函数被调用。

10. 类 unique_ptr 与动态内存分配

如果在成功分配内存后但在 delete 语句执行之前发生异常,可能会发生内存泄漏。C++ 标准库提供了 unique_ptr 类模板来处理内存泄漏。 unique_ptr 对象维护一个指向动态分配内存的指针,其析构函数会对指针数据成员执行 delete 操作。例如:

#include <iostream>
#include <memory>

int main() {
    std::unique_ptr<int> ptr(new int(42));
    std::cout << *ptr << std::endl;
    return 0;
}

上述代码中, std::unique_ptr<int> ptr(new int(42)) 创建了一个 unique_ptr 对象,指向动态分配的整数。当 ptr 超出作用域时,其析构函数会自动删除该整数,避免内存泄漏。

11. 标准库异常层次结构

C++ 标准库包含一个异常类的层次结构,以 exception 类为基类。直接派生类包括 runtime_error logic_error ,每个派生类又有几个派生类。一些 C++ 运算符也会抛出标准异常,例如 new 抛出 bad_alloc dynamic_cast 抛出 bad_cast typeid 抛出 bad_typeid

以下是标准库异常层次结构的部分示例:

graph TD;
    exception --> logic_error;
    exception --> runtime_error;
    exception --> bad_type_id;
    exception --> bad_alloc;
    exception --> bad_cast;
    exception --> bad_exception;
    runtime_error --> underflow_error;
    runtime_error --> overflow_error;
    logic_error --> invalid_argument;
    logic_error --> length_error;
    logic_error --> out_of_range;

标准库异常类的部分描述如下表所示:
| 异常类 | 描述 |
| ---- | ---- |
| logic_error | 表示程序逻辑错误的标准异常类的基类,例如 invalid_argument 表示函数接收到无效参数, length_error 表示使用的长度超过了被操作对象允许的最大大小, out_of_range 表示值超出了允许的范围。 |
| runtime_error | 表示执行时错误的标准异常类的基类,例如 overflow_error 描述算术溢出错误, underflow_error 描述算术下溢错误。 |

12. 常见编程错误与预防提示
  • 常见编程错误 22.6 :将捕获基类对象的 catch 处理程序放在捕获从该基类派生的类的对象的 catch 处理程序之前是逻辑错误。基类的 catch 会捕获从该基类派生的所有类的对象,因此派生类的 catch 永远不会执行。
  • 常见编程错误 22.7 :异常类不必从 exception 类派生,因此捕获 exception 类型不能保证捕获程序可能遇到的所有异常。
  • 错误预防提示 22.9 :使用 catch(...) 可以捕获 try 块中抛出的任何类型的异常。优点是可以捕获所有可能的异常,缺点是 catch 没有参数,无法引用抛出对象中的信息,也不知道异常的原因。
13. 软件工程观察
  • 软件工程观察 22.8 :标准异常层次结构是创建异常的良好起点。可以构建抛出标准异常、抛出从标准异常派生的异常或抛出不派生自标准异常的自定义异常的程序。
  • 软件工程观察 22.9 :使用 catch(...) 执行不依赖于异常类型的恢复操作(例如释放公共资源)。可以重新抛出异常以提醒更具体的外部 catch 处理程序。
14. 自我复习练习解答
  • 22.1 :五个常见的异常示例包括:满足新请求的内存不足、数组下标越界、算术溢出、除零、无效的函数参数。
  • 22.2 :不应该将异常处理技术用于常规程序控制的原因如下:
    • 异常处理旨在处理不常发生的情况,通常会导致程序终止,因此编译器编写者不需要实现最优的异常处理。
    • 传统控制结构的控制流通常比异常更清晰、更高效。
    • 异常发生时栈会展开,可能导致在异常发生之前分配的资源无法释放。
    • “额外”的异常使处理更多的异常情况变得更加困难。
  • 22.3 :库函数不太可能执行满足所有用户独特需求的错误处理。
  • 22.4 :“资源泄漏”指程序突然终止可能使资源处于其他程序无法获取的状态,或者程序本身可能无法重新获取“泄漏”的资源。
  • 22.5 :如果 try 块中没有抛出异常,该块的 catch 处理程序将被跳过,程序将在最后一个 catch 处理程序之后继续执行。
  • 22.6 :在 try 块之外抛出的异常会导致调用 terminate 函数。
  • 22.7 catch(...) 的优点是可以捕获 try 块中抛出的任何类型的异常,缺点是没有参数,无法引用抛出对象中的信息,也不知道异常的原因。
  • 22.8 :如果没有匹配的 catch 处理程序,程序会继续在包含的下一个 try 块中搜索匹配项。如果最终确定程序中没有匹配抛出对象类型的处理程序,程序将终止。
  • 22.9 try 块之后的第一个匹配的异常处理程序将被执行。
  • 22.10 :指定基类类型作为 catch 处理程序的类型,然后抛出派生类类型的对象,是捕获相关类型异常的好方法。
  • 22.11 :基类处理程序会捕获所有派生类类型的对象。
  • 22.12 :抛出异常不一定会导致程序终止,但会终止抛出异常的块。
  • 22.13 :异常将由与包含导致异常的 catch 处理程序的 try 块相关联的 catch 处理程序(如果存在)处理。
  • 22.14 :如果 throw; 语句出现在 catch 处理程序中,它会重新抛出异常;否则,程序将终止。
15. 练习

以下是一些相关练习及简要说明:
- 22.15 :列出各种异常情况,并描述程序通常如何使用异常处理技术处理这些异常。
- 22.16 :讨论在定义捕获处理程序要捕获的对象类型时,不提供参数名的情况。
- 22.17 :说明 throw; 语句通常出现的位置,以及该语句出现在程序不同部分的情况。
- 22.18 :比较和对比异常处理与其他错误处理方案。
- 22.19 :解释为什么不应该将异常用作程序控制的替代形式。
- 22.20 :描述处理相关异常的技术。
- 22.21 :编写程序检查异常处理程序本身抛出相同异常是否会导致无限递归。
- 22.22 :使用继承创建 runtime_error 的各种派生类,并展示指定基类的 catch 处理程序可以捕获派生类异常。
- 22.23 :抛出条件表达式的结果,该表达式返回 double int ,提供 int double catch 处理程序,展示无论返回 int 还是 double ,只有 double catch 处理程序会执行。
- 22.24 :编写程序说明在块中构造的所有对象的析构函数在该块中抛出异常之前被调用。
- 22.25 :编写程序说明只有在异常发生之前已经构造的成员对象的析构函数会被调用。
- 22.26 :编写程序演示使用 catch(...) 异常处理程序捕获多种异常类型。
- 22.27 :编写程序说明异常处理程序的顺序很重要,第一个匹配的处理程序将被执行,并尝试以两种不同的方式编译和运行程序,展示不同的处理程序执行产生不同的效果。
- 22.28 :编写程序展示构造函数将构造失败的信息传递给 try 块后的异常处理程序。
- 22.29 :编写程序说明重新抛出异常。
- 22.30 :编写程序说明具有自己的 try 块的函数不必捕获 try 块内产生的所有可能的错误,一些异常可以传递到外部作用域进行处理。
- 22.31 :编写程序从深度嵌套的函数中抛出异常,并使 main 中包含初始调用的 try 块后面的 catch 处理程序捕获该异常。

C++ 异常处理深度剖析

16. 异常处理的操作步骤总结

为了更清晰地理解异常处理的流程,下面总结异常处理的关键操作步骤:
1. 定义可能抛出异常的代码 :将可能出现异常的代码放在 try 块中。
- 例如:

try {
    // 可能抛出异常的代码
    int result = divide(10, 0);
} catch (const std::runtime_error& e) {
    // 异常处理代码
    std::cerr << "Exception caught: " << e.what() << std::endl;
}
  1. 抛出异常 :在代码中使用 throw 关键字抛出异常。
    • 例如:
if (denominator == 0) {
    throw std::runtime_error("Division by zero!");
}
  1. 捕获异常 :在 try 块后面紧跟一个或多个 catch 处理程序,用于捕获和处理抛出的异常。
    • 例如:
catch (const std::runtime_error& e) {
    std::cerr << "Exception caught: " << e.what() << std::endl;
}
  1. 栈展开 :如果 try 块中抛出的异常没有匹配的 catch 处理程序,程序会进行栈展开,查找外层的 try 块和匹配的 catch 处理程序。
  2. 重抛异常 :在 catch 处理程序中可以使用 throw; 重抛异常,将异常处理推迟到外层的 catch 处理程序。
    • 例如:
catch (const std::runtime_error& e) {
    std::cerr << "Caught in func: " << e.what() << std::endl;
    throw; // 重抛异常
}
17. 异常处理的注意事项

在使用异常处理时,还需要注意以下几点:
- 异常处理程序的顺序 :异常处理程序的顺序非常重要,应该将捕获派生类异常的处理程序放在捕获基类异常的处理程序之前。否则,基类的 catch 处理程序会捕获所有派生类的异常,导致派生类的 catch 处理程序无法执行。
- 异常类的选择 :根据异常的类型选择合适的异常类进行抛出和捕获,这样可以更准确地处理不同类型的异常。
- 资源管理 :在异常处理过程中,要注意资源的正确释放,避免资源泄漏。可以使用 unique_ptr 等智能指针来管理动态分配的内存。

18. 异常处理的流程图

下面是一个 mermaid 格式的流程图,展示了异常处理的基本流程:

graph TD;
    A[开始] --> B[执行 try 块];
    B --> C{是否抛出异常};
    C -- 是 --> D[栈展开查找匹配的 catch 处理程序];
    D --> E{找到匹配的 catch 处理程序};
    E -- 是 --> F[执行 catch 处理程序];
    F --> G[继续执行后续代码];
    E -- 否 --> H[程序终止];
    C -- 否 --> I[跳过 catch 处理程序,继续执行后续代码];
19. 异常处理在实际项目中的应用

在实际项目中,异常处理可以应用于多个方面,以下是一些常见的应用场景:
- 文件操作 :在进行文件打开、读取、写入等操作时,可能会出现文件不存在、权限不足等异常情况。可以使用异常处理来捕获这些异常并进行相应的处理。
- 例如:

#include <iostream>
#include <fstream>
#include <stdexcept>

void readFile(const std::string& filename) {
    std::ifstream file(filename);
    if (!file.is_open()) {
        throw std::runtime_error("Failed to open file: " + filename);
    }
    // 读取文件内容
    std::string line;
    while (std::getline(file, line)) {
        std::cout << line << std::endl;
    }
    file.close();
}

int main() {
    try {
        readFile("example.txt");
    } catch (const std::runtime_error& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }
    return 0;
}
  • 网络编程 :在进行网络连接、数据传输等操作时,可能会出现网络中断、连接超时等异常情况。可以使用异常处理来捕获这些异常并进行相应的处理。
  • 数据库操作 :在进行数据库查询、插入、更新等操作时,可能会出现数据库连接失败、查询语句错误等异常情况。可以使用异常处理来捕获这些异常并进行相应的处理。
20. 总结

异常处理是 C++ 中处理运行时错误的重要机制。通过使用 try 块、 catch 处理程序和 throw 关键字,可以有效地捕获和处理程序中出现的异常,提高程序的健壮性和可靠性。同时,要注意异常处理程序的顺序、异常类的选择和资源管理等问题。在实际项目中,异常处理可以应用于文件操作、网络编程、数据库操作等多个方面。

以下是异常处理相关知识点的总结表格:
| 知识点 | 描述 |
| ---- | ---- |
| 异常处理概述 | 解决程序运行时的问题,使程序能继续执行或有控制地终止 |
| try 块 | 包含可能抛出异常的代码 |
| catch 处理程序 | 捕获和处理抛出的异常 |
| 抛出异常 | 使用 throw 关键字抛出异常 |
| 栈展开 | 查找外层的 try 块和匹配的 catch 处理程序 |
| 重抛异常 | 在 catch 处理程序中使用 throw; 重抛异常 |
| unique_ptr | 处理动态内存分配,避免内存泄漏 |
| 标准库异常层次结构 | 以 exception 为基类的异常类层次结构 |

通过深入理解和掌握异常处理的相关知识,可以更好地编写高质量的 C++ 程序。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值