【第十节】C++异常处理

目录

一、异常处理任务

二、异常及其特点

三、处理异常示例

四、C++异常处理机制

五、自定义类对象传递异常

六、用 exception 类对象传递异常

七、在异常处理中处理析构函数


一、异常处理任务

        在程序设计之初,就应预见到可能出现的各种异常情况,并为之制定相应的应对策略,这便是异常处理的核心任务。若程序未配备异常处理机制,一旦运行中遭遇异常,由于缺乏自我修复能力,程序将不得不终止运行。然而,若程序内嵌了异常处理机制,当异常发生时,程序会自动将控制流转向预设的异常处理代码段,从而允许用户执行特定的处理措施。

        异常处理的概念涵盖了对运行时错误及其它意外情况的处理,任何与预期不符的情况均可被视为异常,并应予以处理。在小规模程序中,异常处理可能相对简单。但在大型系统中,若在每个函数内都设置异常处理代码,将导致程序结构复杂且臃肿。

        为此,C++采取了一种分层处理的策略:当函数执行过程中遇到异常,它不会立即在本函数内处理,而是将异常信息向上传递给调用它的函数。若上级函数能够处理,则执行相应的异常处理代码;若不能,异常信息将继续向上传递,直至找到能够处理的层级。若异常信息传递至程序的最高层级仍未得到处理,程序将最终以异常终止的方式结束执行。

二、异常及其特点

        异常(Exceptions)是指在程序运行过程中可能出现的、可能导致程序非正常终止的错误。这些错误并非由编译系统检测到的语法错误或导致程序逻辑结果不正确的逻辑错误,而是指在程序正常运行时可能遭遇的问题。

常见的异常包括:

  • 系统资源短缺,例如内存不足,无法动态分配内存;磁盘空间不足,无法打开新的输出文件等。

  • 用户操作失误导致的运算错误,如分母为零、数学运算溢出、数组访问越界、参数类型无法转换等。

异常具有以下特性:

  • 偶然性:异常并非每次程序运行都会发生,它们的出现具有偶然性。

  • 可预见性:尽管异常的出现是偶然的,但它们的存在和发生是可以预见的。

  • 严重性:一旦异常发生,程序可能会终止,或者产生不可预知的结果。

对于程序中的异常,通常有三种处理方式:

  1. 不作处理:许多程序选择不对异常进行处理。

  2. 发布错误信息并终止程序:在C语言程序中,这通常是处理异常的方式。

  3. 适当处理异常:理想情况下,应使程序在异常发生后仍能继续运行。

        异常处理(Exception Handling)是指在程序运行时对异常进行检测和控制的过程。在C++中,异常处理采用try-throw-catch的模式,即通过try块尝试执行可能抛出异常的代码,通过throw语句抛出异常,并通过catch块捕获并处理异常。这种机制允许程序在遇到异常时,能够有条不紊地进行错误处理,而不是简单地终止程序。

三、处理异常示例

        在C++中,异常机制是一种处理错误和异常情况的机制。异常可以是任何类型的对象,但通常它们是派生自std::exception类的对象。

        在C++中,异常处理是通过以下三个关键字来实现的:trythrowcatch。下面是C++异常处理的基本语法结构:

try {
    // 可能抛出异常的代码块
    // 如果在这个代码块中发生了异常,就会执行throw语句抛出异常
}
catch (异常类型1 参数名1) {
    // 处理异常类型1的代码块
}
catch (异常类型2 参数名2) {
    // 处理异常类型2的代码块
}
// 可以有多个catch块来处理不同类型的异常
catch (异常类型N 参数名N) {
    // 处理异常类型N的代码块
}
catch (…) {
    // 捕获所有类型的异常,这是一个通用的catch块
}
  • try 块:包含可能抛出异常的代码。如果try块中的代码抛出了异常,那么程序的控制流会立即跳转到相应的catch块。

  • throw 语句:用于抛出异常。当程序在try块中遇到throw语句时,它会停止执行当前的代码,并抛出一个异常。异常可以是任何类型,但通常是std::exception或其派生类的实例。

  • catch 块:用于捕获并处理异常。每个catch块都指定了它能够处理的异常类型。当异常被抛出时,程序会按顺序检查每个catch块,直到找到一个能够处理该异常类型的catch块为止。

  • 省略号catch块:这是一个通用的catch块,可以捕获任何类型的异常,包括那些没有被前面的catch块指定的异常类型

        以下是一个简单的C++程序,它使用异常机制来处理除以零的错误:

#include <iostream>
#include <stdexcept>

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

int main() {
    double a = 10;
    double b = 0;

    try {
        double result = divide(a, b);
        std::cout << "The result is: " << result << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }

    return 0;
}

        在这个例子中,divide函数会检查除数b是否为零。如果为零,它会抛出一个std::runtime_error异常。main函数中的try块尝试调用divide函数,并捕获任何抛出的异常。如果捕获到异常,它会打印出异常的信息。

        请注意,异常机制是C++中处理错误和异常情况的一种强大工具,但它不应该被滥用。异常应该用于预期和可恢复的错误情况,而不是用于流程控制或普通的程序逻辑。

四、C++异常处理机制

        在C++中,处理异常通常遵循两种基本策略:检测异常和处理异常是两个独立且分离的步骤。具体来说,我们在“try”块中检测潜在的异常,而在紧随其后的“catch”块中处理这些异常。

        由于这两个步骤分布在不同的代码区域,因此需要一种机制来在它们之间传递异常信息。在C++中,这种机制是通过“对象”来实现的,这些对象可以是简单的数据类型(如整数),也可以是系统定义或用户自定义的类实例。

        以下是一个示例,其中在try块中调用了函数divide。尽管divide函数在try块外部定义,但只要它在try块中调用,它就被视为try块的一部分。

        如果divide函数遇到异常,例如抛出一个字符串对象作为异常,那么该异常的类型就是字符串类型。此时,我们可以设置catch块来捕获类型为char*的异常对象,进而处理字符串异常,通常是通过输出错误信息并结束程序。

在编写涉及异常处理的C++程序时,需要注意以下几点:

  1. try和catch块必须作为一个整体出现,两者之间不能插入其他语句。

  2. 一个try块后面可以有多个catch块,但不允许将多个try块与一个catch块关联。

  3. 要检测的函数必须位于try块中,否则异常处理将不会生效。

  4. catch块是try-catch结构不可或缺的一部分,必须紧跟在try块之后,不能单独使用,且二者之间不能插入其他语句。但可以在一个try-catch结构中仅包含try块而不包含catch块,以在本函数中检测异常而不处理,而将处理逻辑放在其他函数中。

  5. try和catch块必须使用花括号包围,即使只包含一个语句,花括号也不能省略。

  6. 每个try-catch结构只能有一个try块,但可以包含多个catch块,以便处理不同类型的异常。

  7. 在catch块的圆括号中,通常只指定异常的类型,例如catch(double)

  8. catch块只检查捕获的异常类型,而不检查其值。因此,如果需要处理多种不同的异常,应抛出不同类型的异常对象。

  9. 异常对象可以是C++预定义的标准类型,也可以是用户自定义的类型(如结构体或类)。只要throw抛出的对象与catch块中指定的类型或其子类型匹配,catch块就会捕获该异常。

  10. catch块还可以同时指定类型和变量名,如catch(double d)。在这种情况下,如果throw抛出的异常是一个double类型的变量a,则catch块不仅会捕获该异常,还会将a的值赋给d。

  11. 如果catch块使用省略号“…”作为参数,则它可以捕获任何类型的异常,通常作为try-catch结构中的最后一个catch块,作为“其他”情况的处理。如果将其放在第一个位置,则后续的catch块将不会起作用。

  12. try-catch结构可以与throw语句出现在同一函数中,也可以分布在不同函数中。当throw抛出异常时,程序会首先在当前函数中查找匹配的catch块,如果找不到或当前函数没有try-catch结构,则会向上层函数继续查找。

  13. 在某些情况下,throw语句可以不包含任何表达式,仅表示“我不处理这个异常,请上级处理”。

  14. 如果throw抛出的异常没有找到匹配的catch块,系统将调用terminate函数,导致程序终止运行。

        为便于阅读程序,使用户在看程序时能够知道所用的函数是否会抛出异常信息以及异常信息可能的类型,C++允许在声明函数时列出可能抛出的异常类型,例如:
int fun(int nNum)throw(int,float,char)
{
    //......
}
        则表示fun依数可以抛出int,float 或char 类型的异常信息注意:异常指定是函数声明的一部分,必须同时出现在函数声明和函数定义的首行中,否则在进行函数的另一次声明时,编译系统会报告“类型不匹配”如果想定义一个不能抛出异常的函数,可以定义为:
int fun(int nNum) throw()
{
    //......
}

五、自定义类对象传递异常

#include<iostream>
using namespace std;

//栈满异常类
class StackOverflowExp {
public:
    StackOverflowExp() {}
    ~StackOverflowExp() {}
    void getMessage() {
        cout << "异常:栈满不能入栈" << endl;
    }
};

//栈空异常类
class StackEmptyExp {
public :
    StackEmptyExp() {}
    ~StackEmptyExp() {}
    void getMessage() {
        cout << "异常:栈空不能出栈" << endl;
    }
};


template <class TYPE>
class CStack {
public:
    CStack(int nCount) {
        m_pstack = new TYPE[nCount];
        memset(m_pstack, 0, nCount);
        if (!m_pstack)throw StackEmptyExp();
        m_nLength = nCount;
        m_nTos = 0;
    }

    ~CStack() {
        delete[] m_pstack;
    }

    void push(TYPE Data) {
        if (m_nTos == m_nLength)throw StackOverflowExp();
        m_pstack[m_nTos++] = Data;
    }

    TYPE pop() {
        if (!m_nTos)throw StackEmptyExp();
        return m_pstack[--m_nTos];
    }

private:
    TYPE* m_pstack;
    int m_nTos, m_nLength;
};

int main() {
    CStack<int> obj(3);
    //cout<< obj.pop() << endl;//报错
    for (int i = 0; i < 5; i++) {
        try {
            cout << obj.pop() << endl;
        }
        catch (StackOverflowExp& e) {
            e.getMessage();
        }
        catch (StackEmptyExp& e) {
            e.getMessage();
        }
    }
    system("pause");
    return 0;

}

        通过对象传递参数,具体来说,是在throw语句中直接调用异常类的构造函数,生成一个无名对象(如:throwStackEmptyException();),来传递异常的。
        在catch语句中规定的异常类型则是异常类对象的引用,当然,也可以直接用异常类对象作为异常。
        通过异常类对象的引用,直接调用异常类的成员函数getMessage,来处理异常,在try语句块后面直接有两个catch语句来捕获异常,也就是说,要处理的异常增加时,catch语句的数目也要增加。

六、用 exception 类对象传递异常

        在C++中,除了使用标准库定义的异常类型(如std::exception及其派生类)外,还可以创建自定义异常类型以满足特定的异常处理需求。自定义异常通常通过继承自std::exception或其他异常基类来实现,这样可以利用标准异常机制提供的特性,如异常信息的字符串描述。

下面是一个创建自定义异常类型的示例:

#include <iostream>
#include <exception>
#include <string>

// 自定义异常类,继承自 std::exception
class MyCustomException : public std::exception {
private:
    std::string message; // 异常信息字符串

public:
    // 构造函数,接受一个描述异常信息的字符串
    explicit MyCustomException(const std::string& msg) : message(msg) {}

    // 重载 what() 函数,返回异常信息的字符串表示
    const char* what() const noexcept override {
        return message.c_str();
    }
};

// 可能抛出异常的函数
void someFunctionThatMightThrow() {
    // 假设这里有一些条件判断
    // 如果条件不满足,则抛出自定义异常
    throw MyCustomException("An error occurred in someFunctionThatMightThrow");
}

int main() {
    try {
        // 调用可能抛出异常的函数
        someFunctionThatMightThrow();
    } catch (const MyCustomException& e) {
        // 捕获并处理自定义异常
        std::cerr << "Caught a custom exception: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        // 捕获并处理其他类型的标准异常
        std::cerr << "Caught a standard exception: " << e.what() << std::endl;
    } catch (...) {
        // 捕获所有其他类型的异常
        std::cerr << "Caught an unknown exception" << std::endl;
    }

    return 0;
}

        在上面的示例中,我们定义了一个名为MyCustomException的自定义异常类,它继承自std::exception。我们添加了一个私有的std::string成员变量message来存储异常信息,并在构造函数中初始化它。我们还重载了what()函数以返回异常信息的字符串表示。

        在someFunctionThatMightThrow()函数中,我们模拟了一个可能抛出异常的情况,并实际抛出了MyCustomException类型的异常。

        在main()函数中,我们使用try-catch块来捕获并处理异常。我们首先尝试捕获MyCustomException类型的异常,并打印出异常信息。如果捕获不到自定义异常,我们还有一个捕获std::exceptioncatch块来处理其他标准异常。最后,我们还有一个捕获所有其他类型异常的catch块,以确保不会遗漏任何未捕获的异常。

        使用自定义异常的好处是可以更精确地描述和处理特定于应用程序的异常情况,提高代码的可读性和可维护性。

七、在异常处理中处理析构函数

        在C++中,析构函数用于在对象生命周期结束时执行清理操作,比如释放动态分配的内存或关闭打开的文件等。当异常发生时,确保所有已创建的对象都能正确执行析构函数非常重要,以防止资源泄漏或其他潜在问题。

以下是一个示例,展示了在异常处理中析构函数的作用:

#include <iostream>
#include <stdexcept> // 用于 std::runtime_error

// 假设我们有一个需要动态分配资源的类
class ResourceHolder {
public:
    ResourceHolder() {
        std::cout << "ResourceHolder::ResourceHolder() - Allocating resource\n";
        // 假设这里分配了一些资源,例如内存
    }

    &#126;ResourceHolder() {
        std::cout << "ResourceHolder::&#126;ResourceHolder() - Freeing resource\n";
        // 假设这里释放了之前分配的资源
    }

    void operationThatMightThrow() {
        // 假设这是一个可能抛出异常的操作
        throw std::runtime_error("Operation failed");
    }
};

int main() {
    try {
        ResourceHolder holder; // 创建对象,调用构造函数
        holder.operationThatMightThrow(); // 这可能会抛出异常
    } catch (const std::exception& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    // 当离开 try 块时,holder 对象会自动调用析构函数,释放资源

    return 0;
}

        在这个例子中,ResourceHolder 类有一个构造函数和一个析构函数。构造函数模拟了资源的分配,而析构函数模拟了资源的释放。operationThatMightThrow 成员函数模拟了一个可能抛出异常的操作。

        在 main 函数中,我们创建了一个 ResourceHolder 对象 holder,并尝试调用其可能抛出异常的成员函数。如果在调用 operationThatMightThrow 时抛出了异常,catch 块将捕获该异常,并输出错误信息。

        重要的是,无论是否发生异常,当控制流离开 try 块时,holder 对象的析构函数都会被自动调用,确保分配的资源得到正确释放。这是C++异常安全性的一个重要方面,它确保了即使发生异常,也不会导致资源泄漏。

        此外,如果在 ResourceHolder 的构造函数中抛出异常,并且 holder 是以值方式传递的(比如作为函数参数或返回值),那么对象的析构函数将不会被调用,因为对象没有完全构造出来。这要求我们在编写构造函数时要特别小心,避免在其中抛出异常,除非我们确实需要中断对象的构造过程。如果必须在构造函数中抛出异常,应确保所有已经分配的资源在抛出异常前都被正确释放,以避免资源泄漏。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

攻城狮7号

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值