深度解析C++异常处理机制:分类、处理方式、常见错误及11新增功能

C++ 基础知识 八 异常处理 上篇

  • 一、基础
    • 1. 异常的概念
    • 2. 异常的分类
      • 2.1 内置异常
      • 2.2 自定义异常
    • 3. 异常的处理方式
      • 3.1 try-catch 语句
      • 3.2 throw 语句
      • 3.3 noexcept 修饰符
      • 3.4 finally 语句块
  • 二、 异常处理机制
    • 1 try-catch 语句块
    • 2 异常处理流程
    • 3 标准异常类
  • 三、 抛出异常
    • 1 throw语句
    • 2 异常类型
    • 3 异常传递
  • 四、 捕获异常
    • 1 catch语句
    • 2 多个catch语句的顺序与匹配规则
    • 3 异常处理机制的使用示例
  • 五、 C++11 新增异常功能
    • 1 noexcept操作符
    • 2 异常列表(function try-block)
  • 六、常见错误和异常处理
    • 1 空指针异常
    • 2 内存泄漏异常
    • 3 数组越界异常
    • 4 死锁异常

一、基础

1. 异常的概念

异常是程序在运行过程中出现非正常情况的处理机制。当出现异常时程序会停止运行并调用异常处理程序。

2. 异常的分类

异常可以分为内置异常和自定义异常

2.1 内置异常

C++ 标准库提供了许多预定义的异常类,称为内置异常,包括以下几种:

  • std::exception:所有标准异常类的基类。
  • std::logic_error:表示程序逻辑错误。
  • std::runtime_error:表示运行时错误。

2.2 自定义异常

除了使用内置异常,我们还可以创建自定义异常类来处理特定错误。自定义异常类可以继承自内置异常类以实现异常的精细控制。

3. 异常的处理方式

C++ 提供了以下几种处理异常的方式:

3.1 try-catch 语句

try {
  // 可能会抛出异常的代码块
} catch (exception1_type exception1_var) {
  // 处理异常类型1
} catch (exception2_type exception2_var) {
  // 处理异常类型2
}

使用 try 代码块来包含可能抛出异常的代码,紧跟 catch 块来处理捕获到的异常。当抛出异常后,程序会自动匹配 catch 块中与对应异常相同类型的代码块执行,保证程序正常执行。

3.2 throw 语句

throw exception_object;

使用 throw 语句抛出一个异常对象,使程序进入异常处理模式。 exception_object 可以是基本类型或对象,甚至可以是自定义类型的实例。

3.3 noexcept 修饰符

void function_name() noexcept {
  // 不会抛出异常的函数
}

noexcept 修饰符指示函数不抛出异常。使用 noexcept 可以优化程序性能。当程序遇到一个没有 noexcept 修饰符的函数,会假设这个函数可能抛出异常,导致额外的代码执行。

3.4 finally 语句块

try{
  // 可能抛出异常的代码块
} catch(...) {
  // 处理异常
} finally {
  // 无论有无异常都执行
}

finally 语句在 try-catch 语句的末尾执行,无论异常是否抛出。通常用于释放资源等程序收尾工作。

综上所述异常处理是程序设计中重要而必不可少的一部分。了解常见的异常处理方式,可以让我们更加高效地处理程序中的错误,并确保程序的正常运行。

二、 异常处理机制

C++中的异常处理机制用于处理程序运行过程中的异常情况。异常可以是程序中的一种错误或者突发事件,可能会导致程序崩溃,但是正常情况下我们希望程序可以平稳地运行下去,去处理这些异常情况。在这样的情况下, C++的异常处理机制发挥了巨大的作用。

1 try-catch 语句块

try-catch 是处理 C++ 异常的关键工具。它的主要作用是将异常处理分离出代码,将异常在运行时捕获并处理。

try {
  // 可能引发异常的代码块
} catch (Type1 arg1) {
  // 处理Type1类型的异常
} catch (Type2 arg2) {
  // 处理Type2类型的异常
} catch (Type3 arg3) {
  // 处理Type3类型的异常
}

当一个异常被抛出的时候(通常是使用 throw 语句),与抛出异常的类型相匹配的 catch 块会被调用,它会处理异常并从这个异常中恢复程序执行。

2 异常处理流程

异常处理的整个过程是一种顺序执行模型,以下是它的基本流程:

  1. 程序执行到可能抛出异常的代码时,这段代码必须嵌入到 try 块中。
  2. 如果在 try 块中的代码引发了异常,程序会跳转到与抛出异常类型匹配的 catch 块中,catch 块负责处理异常。
  3. 最后程序会从 catch 块中退出,向下执行任何后续代码。

简单来说,我们可以将异常处理机制的处理过程看作是程序运行时一条流程线,它沿着 try-catch 块执行,遇到异常时跳转 catch 块那里处理异常,最后退出 catch 块。

3 标准异常类

C++ 标准库中定义了一些异常类,这些异常类是所有 C++ 应用程序都可以使用的。由于这些异常类是预定义的,因此它们都是放在 std 命名空间之下。

C++ 标准库提供的异常类:

  1. std::exception —— 所有标准异常类的基类。
  2. std::bad_alloc —— 在申请内存失败时抛出。
  3. std::bad_cast —— 将一个指向派生类的基类指针强制转换为派生类指针时,如果类不是目标类型,则抛出。
  4. std::ios_base::failure —— I/O 操作失败时抛出。
  5. std::out_of_range —— 数组访问越界时抛出。
  6. std::invalid_argument —— 提供无效参数时抛出。
  7. std::length_error —— 尝试创建一个超出可分配内存大小的vector、string等时发生。
  8. std::logic_error —— 人为 bug,如出现未在程序中考虑到的条件等,通常还有一些 derived class 总结不同的情况(此类异常可以在编译环境中静态检查出来)。
  9. std::runtime_error —— 运行时错误,一般情况下是在单个文件运行时出错。(常常由文件、网络等外部原因引起)

以上这些异常类是为了解决常见的情况而设计的,它们能够支持很多常见的内部错误,如果我们想要精细控制异常,我们还可以创建自定义异常。

通过使用异常处理机制,程序员可以保证程序的正常运行,在发生异常时捕获并处理它们,从而防止程序崩溃。同时使用标准异常类,也可以避免一些常见的错误和异常。

三、 抛出异常

在C++中可以使用异常处理机制来处理程序中的异常情况。当程序出现错误或突发事件时,异常处理机制可以让程序正常运行下去,而不会导致程序崩溃。那么如何抛出异常呢,我们一起来看看

1 throw语句

使用throw语句可以抛出异常,throw语句必须跟上一个表达式该表达式是抛出的异常对象,如下所示:

throw someException; // 抛出异常

我们可以用任何类型的值做为异常对象,不过通常我们会把异常对象设置为标准库的异常类之一

2 异常类型

异常类型是某种类型的值,它可以用于标识需要通知程序时发生了什么错误。异常类型可以是任意类型,但为了能够与 catch 块中的异常参数类型匹配,通常使用异常类的对象或指针。

下面是一个简单的示例,在函数中抛出一个异常:

// 声明一个自定义的异常类
class MyException : public exception {
public:
    // 重写 what() 函数,返回异常信息
    const char* what() const noexcept override {
        return "MyException occurred";
    }
};

void myFunction() {
    throw MyException(); // 抛出自定义异常
}

在抛出异常时可以使用任何类型的值,但是为了能够被 catch 块所匹配,一般使用异常类的对象或指针,也可以自定义异常类

3 异常传递

如果在函数中抛出了异常,那么异常会被抛到调用该函数的代码中。如果这个函数也没有捕获这个异常,那么异常就会继续传递到更高的层次,直到被捕获为止。

下面是一个示例代码,展示了异常是如何传递的:

void functionC() {
    cout << "Starting function C" << endl;
    throw MyException(); // 抛出自定义异常
    cout << "Ending function C" << endl;
}

void functionB() {
    cout << "Starting function B" << endl;
    functionC(); // 调用functionC
    cout << "Ending function B" << endl;
}

void functionA() {
    cout << "Starting function A" << endl;
    try {
        functionB(); // 调用functionB
    } catch (const exception& e) {
        cerr << e.what() << endl; // 捕获并处理异常
    }
    cout << "Ending function A" << endl;
}

int main() {
    cout << "Starting main function" << endl;
    functionA(); // 调用functionA
    cout << "Ending main function" << endl;
    return 0;
}

输出结果为:

Starting main function
Starting function A
Starting function B
Starting function C
MyException occurred
Ending function A
Ending main function

首先main函数调用 functionA。functionA 内部调用 functionB,并且包含一个 try 块,用来捕获异常。functionB 内部调用 functionC并抛出了一个异常,这个异常传递到了 functionA 中被 catch 块捕获并处理。

这里需要注意的是一旦抛出了一个异常,函数就会终止。因此functionC 中的 cout 语句不会被执行到。

总结起来异常处理机制是 C++ 中重要的一部分,它可以帮助程序处理错误和突发事件,并保证程序的正常运行。在抛出异常时需要考虑使用什么类型的异常对象,并养成良好的习惯在适当的地方捕获异常。

四、 捕获异常

在C++中捕获异常是指捕获并处理由 throw 语句抛出的异常。在异常处理机制中,try 块用于捕获异常并处理它们,而 catch 块则用于处理 try 块中抛出的异常。下面我们来看看如何使用 catch 块来捕获和处理异常

1 catch语句

catch 块用于捕获和处理由 try 块抛出的异常。catch 块必须跟上一个括号,括号中是一个参数,它是 catch 块的异常参数,用来接收抛出的异常对象
如下所示:

try {
    // 可能会发生异常的代码
} catch (exceptionType& e) {
    // 处理异常的代码
}

这里的 exceptionType 是异常类型的名字,使用与 throw 语句中抛出的一致的类型或基类类型。&e 表示将异常对象的引用传递给 catch 块中的代码,从而使得 catch 块能够访问并处理该异常对象。需要注意的是,&e 表示对异常对象的只读访问不能对传递过来的异常对象进行修改。

我们可以使用多个 catch 块来处理不同类型的异常,这样可以将异常处理代码与常规代码分离,更好地管理程序错误和异常。下面我们来看看多个 catch 块的顺序和匹配规则

2 多个catch语句的顺序与匹配规则

当由 try 块抛出一个异常时,程序会按照由上到下的顺序遍历 catch 块,直到找到一个与抛出的异常匹配的 catch 块为止。这里的匹配指的是异常参数类型与抛出的异常对象类型一致,或者是该异常的基类类型。如果找不到合适的 catch 块,则程序将异常传递到更高层次的代码中。

需要注意的是如果有多个 catch 块可以匹配抛出的异常,C++ 编译器将使用第一个合适的 catch 块进行处理,而不是使用最具体的 catch 块。因此在编写多个 catch 块时,应该将最具体的 catch 块放在最前面

下面是一个示例,它展示了多个 catch 块的顺序和匹配规则:

try {
    // 可能会发生异常的代码
} catch (const Exception1& e) {
    // 处理异常1的代码
} catch (const Exception2& e) {
    // 处理异常2的代码
} catch (const Exception3& e) {
    // 处理异常3的代码
} catch (const exception& e) {
    // 处理其他异常的代码
}

在这个示例中当由 try 块抛出 Exception1 类型的异常时,程序会执行第一个 catch 块。当抛出 Exception2 类型的异常时,程序会执行第二个 catch 块。当抛出 Exception3 类型的异常时,程序会执行第三个 catch 块。当抛出其他异常时程序会执行最后一个 catch 块。

3 异常处理机制的使用示例

下面是一个简单的示例,它展示了异常处理机制的使用。我们自定义一个异常类 DivideByZeroException用于抛出在除法运算中除数为零的异常。在该程序中,我们使用 try 块和 catch 块来捕获和处理异常,并且为了更好地管理程序错误和异常,将业务逻辑和异常处理代码分离开来。

#include <iostream>
#include <exception>
using namespace std;

// 自定义一个异常类
class DivideByZeroException : public exception {
public:
    // 重写 what() 函数,返回异常信息
    const char* what() const noexcept override {
        return "Attempt to divide by zero";
    }
};

// 除法函数,在该函数中可能会抛出一个 DivideByZeroException 异常
double divide(double a, double b) {
    if (b == 0) {
        throw DivideByZeroException();
    }
    return a / b;
}

int main() {
    double a = 42, b = 0, result;
    try {
        result = divide(a, b);
        cout << "Result is: " << result << endl;
    } catch (const DivideByZeroException& e) {
        cerr << "Exception caught: " << e.what() << endl;
    }
    cout << "Program continues to run" << endl;
    return 0;
}

在该示例中定义了一个 除法函数 divide,该函数接收两个 double 类型的参数 a 和 b,如果除数 b 为零,则抛出一个 DivideByZeroException 异常。在程序中我们在 try 块中调用 divide 函数,并通过 catch 块来捕获并处理抛出的异常。我们在 catch 块中打印出异常信息并继续执行程序。最后输出的结果应该是:

Exception caught: Attempt to divide by zero
Program continues to run

以上就是关于 catch 块的使用和异常处理机制的一个简单示例,希望可以帮助您更好地掌握 C++ 中的异常处理机制。

五、 C++11 新增异常功能

在C++11中增加了一些异常的功能,让我们来看看

1 noexcept操作符

noexcept是一个C++11新增的操作符,它用于指明一个函数是否可能抛出异常。当一个函数有可能抛出异常时,我们可以在其声明或定义前使用 noexcept 关键字告知编译器这一信息。这个操作符的作用是帮助编译器进行优化,提高代码的效率。

一个使用 noexcept 的函数可以被认为是“抛出异常不影响程序正确性”的,这意味着该函数不会抛出异常,或者抛出异常后程序可以优雅地进行终止。

下面是一个使用 noexcept 关键字的示例:

#include <iostream>
using namespace std;

void func1() noexcept {
    cout << "func1: No exceptions here!" << endl;
}

void func2() {
    throw runtime_error("Exception in func2");
}

int main() {
    cout << boolalpha;
    cout << "func1 is noexcept: " << noexcept(func1()) << endl; // 输出 true
    cout << "func2 is noexcept: " << noexcept(func2()) << endl; // 输出 false
}

在这个示例中定义了两个函数 func1 和 func2,其中 func1 使用了 noexcept,并且没有抛出任何异常。而 func2 则可能会抛出一个 runtime_error 异常。在主函数中,我们使用 noexcept 来检查 func1 和 func2 是否使用了 noexcept 进行声明或定义,从而判断它们是否可能会抛出异常。

需要注意的是noexcept 操作符只能用于函数声明或定义的末尾,这个末尾必须是分号或花括号。对于类的成员函数,可以在函数名后的括号内使用它,如下所示:

class MyClass {
public:
    void func1() noexcept;
    void func2() noexcept(true) { // 或者把true换成false
        throw runtime_error("Exception in func2");
    }
};

在类的成员函数声明或定义中也可以使用 noexcept 进行限定,但有一个细微的差别,可以将其放在括号内来表示类作为一个新环境。括号内的布尔值指定了该函数是否可能抛出异常。

2 异常列表(function try-block)

在 C++11 中还新增了异常列表,也叫函数 try 块。这个功能允许我们在函数定义中捕获异常并处理,与在函数体中进行异常处理不同,这让我们可以将所有异常处理代码放在同一个位置,提高代码的可读性和可维护性。

在函数体开始之前,在函数参数列表后立即使用 try 关键字并紧跟着花括号,在花括号中包含函数体,需要同时声明和捕获异常。需要注意的是,如果一个函数既有函数 try 块,又有普通的 try-catch 块,它们的处理顺序是不一样的,函数 try 块中的异常处理将先于普通的 try-catch 块执行。

下面是一个函数 try 块的示例代码:

#include <iostream>
#include <stdexcept>
using namespace std;

class MyClass {
public:
    MyClass() : num(42) {
        cout << "Constructing MyClass" << endl;
    }

    ~MyClass() noexcept(false) {
        cout << "Destructing MyClass" << endl;
        throw runtime_error("Exception in destructor");
    }

    void DoSomething() noexcept(false) {
        cout << "Doing something" << endl;
        throw runtime_error("Exception in DoSomething");
    }

private:
    int num;
};

void f(MyClass& mc) try {
    mc.DoSomething();
} catch (const exception& e) {
    cerr << "Caught exception in f(): " << e.what() << endl;
}

int main() {
    MyClass mc;
    f(mc);
    return 0;
}

在这个示例中定义了一个 MyClass 类,该类包含了一个有副作用的默认构造函数和一个有副作用的析构函数,它们都可能会抛出异常。MyClass 类还包含一个成员函数 DoSomething,这个函数也可能会抛出异常。我们使用了函数 try 块来定义了一个函数 f,该函数调用了成员函数 DoSomething,并在函数名后面使用了 try 来声明和捕获异常。在 catch 块中打印出异常信息。

在主函数中创建了一个 MyClass 对象,然后调用函数 f 来演示函数 try 块的使用。

以上就是 C++11 中新增的两个异常功能:noexcept 操作符和异常列表(function try-block)。这些新功能使我们可以更好地处理异常,写出更健壮、高效的代码。

六、常见错误和异常处理

在编写代码时,我们难免会遇到一些错误与异常。这些错误与异常可能会导致我们的程序崩溃或者出现一些意想不到的行为。因此在编写代码的过程中,我们应该注意一些常见的错误和异常,以便及时解决它们或者避免它们的发生。

1 空指针异常

指针是我们编程过程中经常使用的一个概念,空指针异常指当我们使用一个空指针时,会出现意想不到的行为或意外的程序崩溃。

下面是一个关于空指针异常的代码示例:

int* p = nullptr; // 定义一个空指针
int a = *p; // 这里会发生空指针异常

在代码示例中定义了一个空指针 p,然后试图去访问 p 指向的内存空间中的值,并将其赋值给变量 a,这里就会发生空指针异常。

在C++ 中我们可以通过以下方式来避免空指针异常的发生:

  1. 在使用指针之前要进行判断,确保指针不为空。
  2. 在给指针分配内存空间时,要使用 new 操作符,并进行异常处理。

2 内存泄漏异常

内存泄漏指程序在分配了内存空间后,没有合适的方式来释放它。内存泄漏会导致程序内存空间的消耗过大,从而影响程序性能。

下面是一个内存泄漏的代码示例:

int* p = new int; // 分配了一个动态内存空间
p = nullptr; // 将指针指向空

在代码示例中通过 new 操作符动态地分配了一段内存空间,但是在之后将指针置为空时,我们并没有使用 delete 来释放所分配的内存空间,从而导致了内存泄漏。

在C++ 中应该避免内存泄漏的发生。一种常见的做法是,在分配内存空间时使用智能指针(smart pointer),它们会在指针不再需要时自动释放所分配的内存空间。

3 数组越界异常

数组越界是指访问数组的时候,访问一个超出了数组范围的索引,其结果是未定义的行为。访问越界的错误有时可能看起来没有任何影响,但是在某些情况下,它们可能会对程序的运行时行为产生严重的影响。

下面是一个数组越界的代码示例:

int arr[5] = {1, 2, 3, 4, 5};
int n = 6;
int a = arr[n]; // 这里会发生数组越界

在代码示例中定义了一个长度为 5 的数组 arr,然后试图访问数组中不存在的索引 n,这里就会发生数组越界异常。

在C++ 中我们可以通过以下方式来避免数组越界异常的发生:

  1. 在访问数组元素时,要确保访问的索引在有效范围内。
  2. 在使用数组之前要进行初始化。

4 死锁异常

死锁是指两个或多个进程(线程)相互等待对方释放共享资源,从而导致进程(线程)阻塞的情况。死锁是多线程编程中比较常见的一个问题,一旦发生,会导致程序的挂起,从而影响程序的性能。

下面是一个死锁的代码示例:

#include <mutex>
#include <thread>

void func(std::mutex& m1, std::mutex& m2) {
    m1.lock();
    m2.lock(); // 这里会导致死锁
    // ...
    m2.unlock();
    m1.unlock();
}

int main() {
    std::mutex m1, m2;
    std::thread t1(func, std::ref(m1), std::ref(m2));
    std::thread t2(func, std::ref(m2), std::ref(m1));
    t1.join();
    t2.join();
    return 0;
}

在代码示例中定义了两个线程 t1t2,它们分别执行函数 func。在函数中,我们使用了两个互斥量 m1m2 来保证在访问共享资源前线程的同步,但是在执行线程 t1t2 的时候,如果它们同时试图获取 m1m2 的互斥锁,就会导致死锁异常的发生。

在 C++ 中可以通过以下方式来避免死锁异常的发生:

  1. 避免使用多个互斥量进行同步。
  2. 在使用互斥量时,谨慎使用 lock 和 unlock
  3. 使用 RAII 实现自动加锁和解锁,在 C++11 中,我们可以使用 std::lock_guardstd::unique_lock 来实现在构造函数中加锁,在析构函数中解锁的操作

版权声明:本文为博主作者:格林希尔原创文章,版权归属原作者,如果侵权,请联系我们删除!

原文链接:https://blog.csdn.net/u010349629/article/details/130569698

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
心中带点小风骚的头像心中带点小风骚普通用户
上一篇 2024年4月1日
下一篇 2024年4月1日

相关推荐