2023-5-22-C++异常处理机制学习

简介: 2023-5-22-C++异常处理机制学习

😉一、C++异常处理机制介绍

C++ 异常处理机制是一种用于在程序运行时检测错误并进行相应处理的技术。当程序执行过程中遇到异常情况,比如计算错误、内存分配失败等,就会抛出一个异常对象。该对象包含有关异常的信息,包括异常类型和位置。

异常处理机制是由 try-catch 块组成的。try 块中包含可能引发异常的代码,而 catch 块则用于处理特定类型的异常。可以为每个 try 块指定多个 catch 块,以便处理不同类型的异常。

当 try 块中的代码引发异常时,控制流将转移到与之匹配的 catch 块。如果没有匹配的 catch 块,则程序将终止,并显示错误消息。

除了 try-catch 块外,还有一些其他的异常处理机制,例如 throw 操作符和 noexcept 关键字。throw 操作符用于抛出异常,而 noexcept 关键字用于指示函数不会抛出异常。这些机制可以更精细地控制异常处理过程。

需要注意的是,在使用异常处理机制时,需要小心确保资源的正确释放,以避免内存泄漏等问题。

try {
    // 可能抛出异常的代码
} catch (exception& e) {
    // 处理异常
}

🎉二、抛出和捕获异常的方式

在 C++ 中,抛出和捕获异常的方式主要有以下两种:

  1. 使用 try-catch 块:try 块中包含可能引发异常的代码,而 catch 块则用于处理特定类型的异常。可以为每个 try 块指定多个 catch 块,以便处理不同类型的异常。

示例代码:

try {
    // 可能会引发异常的代码
    throw MyException();  // 抛出自定义异常
}
catch (MyException& e) {  // 处理自定义异常
    // 处理异常的代码
}
catch (std::exception& e) {  // 处理标准异常
    // 处理异常的代码
}
catch (...) {  // 处理所有其他异常
    // 处理异常的代码
}
  1. 使用 throw 操作符:throw 操作符用于抛出异常。通过 throw 抛出的异常可以被上层调用者捕获并处理。

示例代码:

if (someCondition) {
    // 抛出 std::runtime_error 异常
    throw std::runtime_error("An error occurred.");
}

🎉三、C++异常有几种

C++ 异常主要可以分为三种类型:

  1. 标准异常(std::exception):是所有标准 C++ 异常的基类,包括 std::runtime_error、std::logic_error 等。
  2. 自定义异常:程序员可以根据自己的需求创建异常类来实现特定的异常处理逻辑。
  3. 系统异常:由操作系统或底层库抛出的异常,例如访问非法内存地址、除零等。这些异常不能被捕获并处理,只能通过遵循编程最佳实践避免它们的发生。

🐱‍🚀四、标准C++异常有几种

标准C++异常有15种,其中13种是派生自std::exception的标准异常类,另外两种是std::bad_exception和std::nested_exception。

这些标准异常类的区别在于它们所表示的错误类型不同。例如,std::invalid_argument异常表示对一个函数传递了无效的参数,而std::out_of_range异常则表示访问了一个超出范围的元素。std::bad_alloc异常表示内存分配失败,而std::logic_error异常表示程序逻辑错误。以下是15中标准C++异常:

以下是 C++ 标准库中 15 种标准异常及其含义:

  1. std::exception:所有标准异常的基类,可以被用来捕获任意一个标准异常。
  2. std::bad_alloc:在动态内存分配失败时抛出。
  3. std::bad_cast:在使用动态类型转换(dynamic_cast)时,如果转换目标与对象类型不兼容,则抛出。
  4. std::bad_exception:当函数抛出了一个未捕获的异常时,会调用 std::unexpected() 函数,如果 std::unexpected() 函数也抛出了异常,则会进一步调用 std::terminate() 函数。std::bad_exception 就是 std::unexpected() 函数抛出的异常。
  5. std::bad_typeid:在使用 typeid 运算符获取对象类型信息时,如果对象是空指针或者不是多态类型,则会抛出此异常。
  6. std::logic_error:逻辑错误,通常在程序运行期间检测到。从 std::exception 继承而来,有两个子类:std::domain_error 和 std::invalid_argument。
  7. std::domain_error:某些操作的参数超出了定义域范围。
  8. std::invalid_argument:提供给函数的参数无效。
  9. std::length_error:当试图创建大于 max_size 的对象时抛出。
  10. std::out_of_range:访问数组、字符串或容器等序列类型时,下标越界时抛出。
  11. std::runtime_error:运行时错误,通常在程序运行期间检测到。从 std::exception 继承而来,有四个子类:std::overflow_error、std::range_error、std::underflow_error 和 std::system_error。
  12. std::overflow_error:算术上溢出(如整数相加结果超过了类型的最大值)时抛出。
  13. std::range_error:尝试存储超过类型所能表示的数据(如负数开根号)时抛出。
  14. std::underflow_error:算术下溢(如整数相减结果超过了类型的最小值)时抛出。
  15. std::system_error:涉及底层操作系统或者 C++ 标准库的错误,比如文件打开失败等。含有一个 error_code 类型的成员变量表示错误码。

std::bad_exception和std::nested_exception是除标准异常以外的两个特殊异常类。std::bad_exception用于表示抛出了一个未知的异常类型,而std::nested_exception用于捕获、保存并再次抛出当前异常的信息。


🎂五、怎么实现自定义异常

在 C++ 中,可以通过继承标准异常类 std::exception 来实现自定义异常。具体步骤如下:

  1. 定义一个新的异常类,并从 std::exception 继承。
class MyException : public std::exception {
public:
    // 构造函数可以接受一个字符串作为异常信息
    MyException(const std::string& msg) : message(msg) {}
    // 继承自 std::exception 的虚函数 what(),用于返回异常信息
    const char* what() const noexcept override {
        return message.c_str();
    }
private:
    std::string message;
};
  1. 抛出自定义异常对象。
void foo() {
    throw MyException("Something went wrong!");
}
  1. 在 catch 块中捕获自定义异常。
try {
    foo();
} catch (const MyException& e) {
    std::cerr << "Exception caught: " << e.what() << '\n';
}

在这个例子中,我们定义了一个名为 MyException 的异常类,它包含一个字符串类型的成员变量 message 用于存储异常信息。该类同时重载了 what() 函数,以便在抛出异常时提供异常信息。

在函数 foo() 中,我们使用 throw 语句抛出一个 MyException 类型的对象并传递一个字符串作为异常信息。

try-catch 块中,我们编写了一个 catch 块来捕获 MyException 类型的异常,打印出异常信息。


🥩六、注意事项

以下是在C++中处理异常时需要注意的事项:

  1. 在程序中使用try-catch块捕获异常,避免程序因未处理的异常而崩溃。
  2. 异常应该被抛出和捕获在适当的位置,即在能够处理它们的地方抛出异常,然后在合适的位置进行捕获。
  3. 不要在析构函数中抛出异常,因为这可能导致未定义的行为。
  4. 使用特定的异常类型来组织代码,并且仅在必要的情况下派生新的异常类型。
  5. 将异常信息记录到日志文件中,以便更好地跟踪异常发生的原因。
  6. 不要在循环中捕获异常,因为这会影响程序的性能。
  7. 避免在同一try块中嵌套多个catch块,因为这可能会使代码变得混乱难懂。
  8. 在尝试修复异常之前,请确保已经详细了解了异常的根本原因。
  9. 不要忽略异常,即使它们似乎可以安全地被忽略。这可能导致程序在后续执行中出现问题。
  10. 小心处理内存分配的异常,如果没有正确的处理,这可能导致内存泄漏或其他严重的问题。

这些是处理C++异常时需要注意的一些重要事项,但并不是全部。处理异常时最重要的原则是谨慎和小心,以确保程序的安全性和稳定性。


🍚七、什么是异常安全性?如何保证程序具有异常安全性?

在C++中,异常安全性是指一个程序在发生异常后仍能保持正确和健壮的行为。具有良好的异常安全性意味着即使出现异常,程序也不会崩溃或导致资源泄漏等问题。

要确保程序具有异常安全性,可以采取以下措施:

  1. 使用RAII(Resource Acquisition Is Initialization)技术来管理资源,例如使用智能指针、容器和文件句柄等。这些资源对象在其析构函数中释放资源,无论是否发生异常。
  2. 避免手动管理内存,使用标准库容器、字符串和算法等高级语言特性进行操作。
  3. 在抛出异常时不要改变对象状态,因为这可能导致未定义的行为。可使用copy-and-swap技术避免状态的改变。
  4. 将资源分配和使用的代码块封装在单独的函数中,并将其声明为noexcept,这样可以确保该函数不会引发异常
  5. 在处理异常时,必须对所有可能引发异常的代码进行评估,并在恢复正常执行之前确保任何资源都已正确释放。
  6. 在编写代码时,考虑到异常情况并进行适当的测试。

通过以上措施可以提高程序的异常安全性,在开发大型项目时,保持良好的异常安全性对于代码的可维护性和稳定性至关重要。


🥠八、RAII技术在异常处理中的应用

RAII(Resource Acquisition Is Initialization)技术是一种C++编程技术,它通过在对象的构造函数中获取资源并在析构函数中释放资源来管理资源。

在异常处理中,RAII技术可以确保资源在出现异常时被正确地释放。当抛出异常时,程序会自动执行已经创建的对象的析构函数,从而释放对象占用的资源,无需手动处理异常。这种自动化的行为避免了可能发生的资源泄漏和内存泄漏问题。通常情况下,RAII技术与C++标准库中的智能指针结合使用,以确保动态分配的内存得到正确地释放。

例如,在操作文件时,我们可以使用std::ofstream来打开文件,并将其封装在一个类中:

class File {
public:
    File(const std::string& filename) : _file(filename) {
        if (!_file.is_open()) {
            throw std::runtime_error("failed to open file");
        }
    }
    ~File() {
        if (_file.is_open()) {
            _file.close();
        }
    }
    void write(const std::string& s) {
        _file << s;
    }
private:
    std::ofstream _file;
};
void foo() {
    File f("test.txt");
    f.write("hello, world");
}
int main() {
    try {
        foo();
    } catch (std::exception& e) {
        std::cout << e.what() << std::endl;
    }
    return 0;
}

在上面的示例中,我们定义了一个File类来封装文件操作,构造函数中打开文件并检查是否成功打开,析构函数中关闭文件。

在foo函数中,我们创建了一个File对象来进行文件写操作。无论在写入数据时是否发生异常,File对象都会被正确地销毁,并自动调用析构函数来关闭文件。这保证了程序具有异常安全性。

总之,RAII技术能够有效地提高代码的可靠性和可读性,使得程序的异常处理更加简单和安全。


🍚九、std::exception_ptr的作用和使用方法

std::exception_ptr是一个指向异常的指针,它允许将异常在程序的不同部分传递和保存,从而实现跨越不同层次的异常处理。

使用方法如下:

  1. 在抛出异常时使用 std::make_exception_ptr()std::current_exception() 函数创建 std::exception_ptr 对象。
  2. std::exception_ptr 作为参数传递给其他函数或对象以传递异常信息。
  3. 在需要重新抛出异常时,可以使用 std::rethrow_exception() 函数将 std::exception_ptr 转换回原始的异常对象。
  4. 在捕获异常时,可以使用 std::exception_ptr 指示是否发生了异常,并使用 std::rethrow_if_nested() 函数检查是否存在嵌套异常。

例如,以下代码演示了如何使用 std::exception_ptr 来传递异常信息:

void foo() {
    try {
        // some code that may throw an exception
    } catch (...) {
        std::exception_ptr eptr = std::current_exception();
        bar(eptr); // pass the exception to another function
    }
}
void bar(std::exception_ptr eptr) {
    if (eptr) {
        // rethrow the exception
        std::rethrow_exception(eptr);
    } else {
        // handle the case where no exception was thrown
    }
}

🍳参考文献

🧊文章总结

提示:这里对文章进行总结:

  本文讲了关于C++异常处理机制的一些内容,希望大家有所收获。


目录
相关文章
|
2月前
|
C++
C++ 语言异常处理实战:在编程潮流中坚守稳定,开启代码可靠之旅
【8月更文挑战第22天】C++的异常处理机制是确保程序稳定的关键特性。它允许程序在遇到错误时优雅地响应而非直接崩溃。通过`throw`抛出异常,并用`catch`捕获处理,可使程序控制流跳转至错误处理代码。例如,在进行除法运算或文件读取时,若发生除数为零或文件无法打开等错误,则可通过抛出异常并在调用处捕获来妥善处理这些情况。恰当使用异常处理能显著提升程序的健壮性和维护性。
51 2
|
2月前
|
算法 C语言 C++
C++语言学习指南:从新手到高手,一文带你领略系统编程的巅峰技艺!
【8月更文挑战第22天】C++由Bjarne Stroustrup于1985年创立,凭借卓越性能与灵活性,在系统编程、游戏开发等领域占据重要地位。它继承了C语言的高效性,并引入面向对象编程,使代码更模块化易管理。C++支持基本语法如变量声明与控制结构;通过`iostream`库实现输入输出;利用类与对象实现面向对象编程;提供模板增强代码复用性;具备异常处理机制确保程序健壮性;C++11引入现代化特性简化编程;标准模板库(STL)支持高效编程;多线程支持利用多核优势。虽然学习曲线陡峭,但掌握后可开启高性能编程大门。随着新标准如C++20的发展,C++持续演进,提供更多开发可能性。
49 0
|
3月前
|
存储 安全 编译器
【C++入门 四】学习C++内联函数 | auto关键字 | 基于范围的for循环(C++11) | 指针空值nullptr(C++11)
【C++入门 四】学习C++内联函数 | auto关键字 | 基于范围的for循环(C++11) | 指针空值nullptr(C++11)
|
3月前
|
人工智能 分布式计算 Java
【C++入门 一 】学习C++背景、开启C++奇妙之旅
【C++入门 一 】学习C++背景、开启C++奇妙之旅
|
3月前
|
存储 自然语言处理 编译器
【C++入门 三】学习C++缺省参数 | 函数重载 | 引用
【C++入门 三】学习C++缺省参数 | 函数重载 | 引用
|
3月前
|
小程序 C++
【C++入门 二 】学习使用C++命名空间及其展开
【C++入门 二 】学习使用C++命名空间及其展开
|
3月前
|
存储 C++ 索引
|
3月前
|
存储 C++ 容器
|
3月前
|
C++
C++基础知识(四:类的学习)
类指的就是对同一类对象,把所有的属性都封装起来,你也可以把类看成一个高级版的结构体。
|
3月前
|
算法 C++ 容器