一、异常处理的基本概念
异常是指程序运行时出现的不符合常规的情况,它可能源于多种因素,例如试图访问数组的越界索引、除以零的操作、文件无法打开等。C++ 提供了一套异常处理机制,允许开发者在程序中明确地标记可能出现异常的代码区域,并定义当异常发生时的应对策略。
- 异常处理机制允许程序中独⽴开发的部分能够在运⾏时就出现的问题进⾏通信并做出相应的处理,异常使得我们能够将问题的检测与解决问题的过程分开,程序的⼀部分负责检测问题的出现,然后解决问题的任务传递给程序的另⼀部分,检测环节⽆须知道问题的处理模块的所有细节。
- C语⾔主要通过错误码的形式处理错误,错误码本质就是对错误信息进⾏分类编号,拿到错误码以后还要去查询错误信息,⽐较⿇烦。异常时抛出⼀个对象,这个对象可以函数更全⾯的各种信息。
二、异常的抛出和捕获
(一)try 块
try块是异常处理的起点,它将可能引发异常的代码包裹起来。当程序执行到try块中的代码时,如果一切正常,代码会按顺序执行;但如果出现了异常,程序的执行流程就会立即跳转到与之匹配的catch块。
代码语言:javascript
AI代码解释
try { // 可能会抛出异常的代码 int result = 10 / 0; // 除以零,会引发异常 }(二)catch 块
catch块用于捕获并处理try块中抛出的异常。每个catch块都指定了它能够处理的异常类型,当try块中抛出的异常与某个catch块的类型匹配时,该catch块就会被执行。
代码语言:javascript
AI代码解释
catch (const std::exception& e) { // 处理异常,e 是异常对象,包含了异常信息 std::cerr << "发生异常:" << e.what() << std::endl; }(三)throw 表达式
throw表达式用于在代码中显式地抛出异常。它后面可以跟随一个异常对象,这个对象可以是标准库中定义的异常类型,如std::runtime_error、std::logic_error等,也可以是开发者自定义的异常类型。
代码语言:javascript
AI代码解释
if (x == 0) { throw std::runtime_error("除数不能为零"); }(四)异常的抛出和捕获步骤
- 程序出现问题时,我们通过抛出(throw)⼀个对象来引发⼀个异常,该对象的类型以及当前的调⽤链决定了应该由哪个catch的处理代码来处理该异常。
- 被选中的处理代码是调⽤链中与该对象类型匹配且离抛出异常位置最近的那⼀个。根据抛出对象的类型和内容,程序的抛出异常部分告知异常处理部分到底发⽣了什么错误。
- 当throw执⾏时,throw后⾯的语句将不再被执⾏。程序的执⾏从throw位置跳到与之匹配的catch模块,catch可能是同⼀函数中的⼀个局部的catch,也可能是调⽤链中另⼀个函数中的catch,控制权从throw位置转移到了catch位置。这⾥还有两个重要的含义:1、沿着调⽤链的函数可能提早退出。2、⼀旦程序开始执⾏异常处理程序,沿着调⽤链创建的对象都将销毁。
- 抛出异常对象后,会⽣成⼀个异常对象的拷⻉,因为抛出的异常对象可能是⼀个局部对象,所以会⽣成⼀个拷⻉对象,这个拷⻉的对象会在catch⼦句后销毁。(这⾥的处理类似于函数的传值返回)
(五)查找匹配的处理代码
- ⼀般情况下抛出对象和catch是类型完全匹配的,如果有多个类型匹配的,就选择离他位置更近的那个。
- 但是也有⼀些例外,允许从⾮常量向常量的类型转换,也就是权限缩⼩;允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针;允许从派⽣类向基类类型的转换,这个点⾮常实⽤,实际中继承体系基本都是⽤这个⽅式设计的。
- 如果到main函数,异常仍旧没有被匹配就会终⽌程序,不是发⽣严重错误的情况下,我们是不期望程序终⽌的,所以⼀般main函数中最后都会使⽤catch(…),它可以捕获任意类型的异常,但是是不知道异常错误是什么。
三.栈展开
一、栈展开的基本概念
栈展开是指当异常发生时,程序的执行流程从异常发生的点开始,逐层回退到调用栈的上层,直到找到合适的catch块来处理异常。在这个过程中,程序会自动调用沿途栈帧中对象的析构函数,以确保资源得到正确释放。
(一)调用栈
调用栈是程序运行时的一个重要数据结构,它记录了函数的调用顺序。每个函数调用都会在栈上创建一个新的栈帧,栈帧中包含了函数的局部变量、函数参数以及返回地址等信息。当函数返回时,其对应的栈帧会被销毁。
(二)栈展开的目的
栈展开的主要目的是确保在异常发生时,程序能够安全地回退到异常处理代码处,同时清理沿途的资源。这包括:
- 调用沿途栈帧中对象的析构函数,释放动态分配的内存、关闭文件句柄、释放网络连接等。
- 确保程序状态的一致性,避免资源泄漏和数据损坏。
二、栈展开的过程
当异常发生时,程序的执行流程会按照以下步骤进行栈展开:
(一)异常抛出
当执行到throw表达式时,异常被抛出。此时,程序会记录异常的类型和相关信息,并开始寻找合适的catch块来处理异常。
(二)栈帧回退
程序从异常发生的点开始,逐层回退到调用栈的上层。在回退的过程中,会依次检查每个栈帧中的catch块,看是否有与异常类型匹配的catch块。
(三)析构函数调用
在栈帧回退的过程中,会自动调用沿途栈帧中对象的析构函数。这一步骤至关重要,因为它确保了资源得到正确释放。例如,如果栈帧中有一个动态分配的内存对象,其析构函数会释放这块内存,避免内存泄漏。
(四)异常捕获
如果在某个栈帧中找到了与异常类型匹配的catch块,程序的执行流程会跳转到该catch块。此时,栈展开过程结束,程序开始执行异常处理代码。
(五)栈帧清理
当异常处理完成后,程序会继续执行catch块之后的代码。如果异常没有被捕获,程序会一直回退到调用栈的最顶层,并终止运行。
三、栈展开的重要性
(一)资源管理
栈展开确保了在异常发生时,程序能够正确地释放资源。这包括动态分配的内存、文件句柄、网络连接等。如果没有栈展开机制,程序可能会因为异常而遗漏资源释放,导致资源泄漏。
(二)程序稳定性
栈展开机制使得程序在异常发生时能够安全地回退到异常处理代码处,避免程序崩溃。这大大提高了程序的稳定性和可靠性,尤其是在处理复杂的业务逻辑时。
(三)异常安全性
通过栈展开,程序能够确保在异常发生时,对象的状态保持一致。析构函数的调用保证了对象在销毁时能够正确地清理资源,避免数据损坏和未定义行为。
代码语言:javascript
AI代码解释
1. 抛出异常后,程序暂停当前函数的执⾏, 开始寻找与之匹配的catch⼦句,⾸先检查throw本⾝是否在try块内部, 如果在则查找匹配的catch语句,如果有匹配的,则跳到catch的地⽅进⾏处理。 2. 如果当前函数中没有try/catch⼦句,或者有try/catch⼦句但是类型不匹配, 则退出当前函数,继续在外层调⽤函数链中查找,上述查找的catch过程被称为栈展开。 3. 如果到达main函数,依旧没有找到匹配的catch⼦句,程序会调⽤标准库的 terminate 函数终⽌程序。 4. 如果找到匹配的catch⼦句处理后,catch⼦句代码会继续执⾏。在这里插入图片描述
四.C++异常处理中的异常重新抛出
有时catch到⼀个异常对象后,需要对错误进⾏分类,其中的某种异常错误需要进⾏特殊的处理,其他错误则重新抛出异常给外层调⽤链处理。捕获异常后需要重新抛出,直接 throw; 就可以把捕获的对象直接抛出。
一、异常重新抛出的基本概念
异常重新抛出是指在catch块中捕获异常后,使用throw关键字将异常再次抛出。重新抛出的异常会继续沿着调用栈向上传播,直到被更高层次的catch块捕获。如果在当前函数中没有捕获重新抛出的异常,程序会继续回退,直到找到合适的处理代码,或者最终导致程序终止。