学懂C++(七): C++错误处理机制 -- 异常

目录

前言

一、C 语言传统的处理错误的方式

二、C++ 异常的概念

三、异常的使用

3.1 异常的抛出和匹配原则

3.2 在函数调用链中异常栈展开匹配原则

3.3 异常的重新抛出

3.4 异常规范

四、自定义异常体系

五、异常的优缺点

优点

缺点

结论


前言

        C++ 提供了一种优雅的错误处理机制——异常(Exceptions),这是 C++11 以来的重要特性之一。异常处理使得程序员能够有效地管理运行时错误,集中处理异常逻辑,从而提高代码的可读性和可维护性。本文将详细介绍 C++ 的异常处理机制,包括概念、使用、原则、自定义异常体系,以及它的优缺点。

一、C 语言传统的处理错误的方式

C 语言的错误处理相对简单,主要有以下几种方式:

  1. 终止程序通过 assert 等函数直接终止程序。这种方式的缺陷在于用户体验较差,尤其是对于非致命错误(如 I/O 错误)时,程序强制终止给用户带来困扰。

  2. 返回错误码:许多 C 库通过返回错误码(如 errno)来指示操作是否成功。这种方式要求程序员在每次函数调用后都手动检查错误码,容易导致错误被忽略。

以下是一个示例,展示了通过返回错误码的方式处理错误:

#include <iostream>
#include <cstdio>
#include <cerrno>

int main() {
    FILE* fout = fopen("file.txt", "r");
    
    if (!fout) {
        std::cout << "Error opening file: " << errno << std::endl;
        perror("fopen fail");
    }

    return 0;
}

二、C++ 异常的概念

C++ 的异常处理通过 throwcatchtry 关键字实现。异常提供了一种机制,当函数遇到它无法处理的错误时,可以抛出异常,让调用者处理。

  • throw:用于抛出异常的关键字。
  • catch:用于捕获异常并进行处理。
  • try:标记可能抛出异常的代码块,其后通常跟随一个或多个 catch 块。
    try {
        // 可能抛出异常的代码
    } catch (const std::exception& e) {
        // 处理异常
    }
    

三、异常的使用

3.1 异常的抛出和匹配原则

  • 类型匹配:抛出的异常类型和捕获的类型必须严格一致。否则,捕获将失败,程序将继续寻找其他匹配的 catch 块。

    try {
        throw "Error occurred"; // 抛出字符串
    } catch (int e) { // 类型不匹配
        // 不会被执行
    } catch (const char* e) {
        std::cout << e << std::endl; // 会被执行
    }
    
  • 就近原则:调用链中最接近 throw 语句的 catch 块会被优先匹配。

    void Func() {
        try {
            throw "Error"; // 抛出异常
        } catch (const char* e) {
            std::cout << e << std::endl; // 最近的catch
        }
    }
    
  • 通配符捕获:使用 catch(...) 可以捕获任何类型的异常,通常作为最后的手段。

    try {
        throw 10; // 抛出整型异常
    } catch (...) { // 捕获所有异常
        std::cout << "Unknown exception" << std::endl;
    }
    
  • 拷贝对象:抛出的异常对象会自动生成拷贝,原对象的生命周期结束后,拷贝仍然存在于 catch 块中。

  • 基类捕获子类:可以通过基类捕获派生类对象,这在实际应用中非常方便且常见。

    class BaseException {};
    class DerivedException : public BaseException {};
    
    try {
        throw DerivedException(); // 抛出派生类异常
    } catch (BaseException& e) { // 捕获基类异常
        std::cout << "Caught a base exception!" << std::endl;
    }
    

3.2 在函数调用链中异常栈展开匹配原则

  1. throw 开始查找:检查 throw 是否在 try 块内,若是则查找对应的 catch

  2. 沿调用链查找:若没有匹配的 catch,则退回到调用当前函数的函数中继续查找。

  3. 终止程序:若到达 main 函数仍未找到匹配的 catch,程序将终止,并可能触发终止处理程序。

  4. 继续执行:找到匹配的 catch 后,处理完异常后会继续执行 catch 块后面的代码。

3.3 异常的重新抛出

在异常处理后,可能需要重新抛出异常以便上层处理。这通常用于清理资源的操作,有效避免内存泄漏。

void Func() {
    int* array = new int[10]; // 动态分配内存
    try {
        // 可能抛出异常的代码
    } catch (...) {
        // 处理异常
        delete[] array; // 释放资源
        throw; // 重新抛出异常
    }
}

 

3.4 异常规范

  • 异常说明:函数后面可以使用 throw 指定可能抛出的异常类型,这在函数文档中非常有用。

  • 不抛异常:通过 throw() 声明,表示函数不抛出任何异常。

  • C++11 noexcept 关键字:表示该函数不会抛出异常,使用时可以提高性能。

    void func() throw(A, B); // 可能抛出 A 或 B
    void func() noexcept; // 不抛出异常
    

四、自定义异常体系

在大型项目中,通常会建立一套自定义的异常体系以规范异常管理。通过继承异常基类,可以方便地捕获和处理异常,增强代码的可读性与可维护性。

class MyException : public std::exception {
public:
    const char* what() const noexcept override {
        return "My custom exception occurred";
    }
};

使用自定义异常类可以提供更丰富的错误信息,并且有助于明确区分不同类型的错误。

五、异常的优缺点

优点

  1. 清晰的错误处理:异常对象可以包含丰富的错误信息,帮助定位和解决问题。

  2. 集中处理:通过 try-catch 块,可以在一个位置集中处理所有相关的错误,而不需要在每个函数中检查返回值。

  3. 资源安全:通过 RAII(资源获取即初始化)结合异常处理,可以有效避免资源泄漏和内存管理问题。

缺点

  1. 控制流复杂:异常会导致程序的执行流跳转,这可能使得代码逻辑难以跟踪,调试变得困难。

  2. 性能开销:异常处理可能带来一定的性能开销,尤其是在异常频繁发生时。

  3. 资源管理问题:C++ 不会自动回收资源,必须使用 RAII 原则以避免内存泄漏等问题,增加了学习成本。

结论

        C++ 的异常处理机制为程序员提供了一种灵活、强大的方式来处理错误。合理利用异常处理,可以显著提高代码的可维护性和可靠性。尽管使用不当可能引入复杂性,但通过良好的设计和实践,可以有效管理异常,确保程序的稳定性。在实际开发中,建议结合自定义异常体系和 RAII 原则,以确保资源的正确管理和错误的及时处理。通过掌握 C++ 的异常处理,你将能够编写出更加健壮的代码,提升软件的质量。

  • 14
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值