C++ 异常 【无敌详细版】

1. C语言传统的处理错误的方式

传统的错误处理机制:

  1. 终止程序,如assert,缺陷:用户难以接受。如发生内存错误,除0错误时就会终止程序。

  2. 返回错误码,缺陷:需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通过把错误码放到errno中,表示错误。

实际中C语言基本都是使用返回错误码的方式处理错误,部分情况下使用终止程序处理非常严重的 错误。

2. C++异常概念

异常是程序在运行过程中处理错误的一种方式,允许将 错误检测错误处理 解耦。其核心思想是:当无法在局部解决问题时,将问题抛出,交给上层处理

  • try:定义可能抛出异常的代码块。

  • throw:主动抛出一个异常对象。

  • catch:捕获并处理特定类型的异常。

基本语法(如下图所示)

3. 异常的使用

3.1 异常的抛出与捕获

3.1.1异常的抛出和匹配原则

1.抛出的异常对象类型决定了激活哪个 catch 块,类型必须严格匹配或兼容。

如果是string类型和char* 类型呢?(如下图)

根据上面的图片,我们发现哪怕是string类型和char*也是不行的,必须严格匹配。(正确示例如下图)

生活实例

医院分诊系统根据患者症状(异常类型)分配科室:

  • 发热 → 发热门诊(catch 块匹配 FeverException

  • 骨折 → 骨科(catch 块匹配 BoneFractureException

2.选择调用链中与异常类型匹配且距离抛出位置最近的 catch 块。

生活实例

快递派送流程:

  • 你的快递会给放到你家附近的驿站,不会给你放在离你家十几公里远的驿站。

3.抛出异常时,会生成对象的拷贝(即使原对象是临时对象),catch 块处理的是拷贝后的对象。

生活实例

重要文件传递时,快递公司会复印一份(拷贝),原件留存(临时对象销毁),复印件用于处理问题(catch 块处理拷贝)。

4.若抛出派生类异常对象,可用基类引用或指针捕获(需满足继承关系)(实际中非常的实用)

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

1.首先在抛出异常的 try 块所在的函数内查找匹配的 catch 块。

2. 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。

3. 若异常传递到 main 函数仍未被捕获,则调用 std::terminate 终止程序。

4.但是在实际生活中,如果一个程序出现了什么错误,并不会直接终止程序。比如说微信发消息,消息因为网络问题导致没有成功发送,没有成功发送的消息前面会出现一个红色的感叹号,告诉你发送失败,而不会终止微信程序。所以我们遇到没有匹配catch的时候,也不应该直接终止程序。那怎么办?catch(...) 捕获任意异常。

catch(...) 可捕获所有类型异常,但无法直接获取异常信息。

5. 异常被捕获并处理后,程序从 catch 块 之后的代码继续执行,而非回到异常抛出点。

3.2 异常的重新抛出

当一个 catch 块捕获到异常后,可能只能进行 部分处理 (如释放资源、记录日志),但无法彻底解决问题。此时可通过 throw; 将异常原样传递给更外层的调用者,由上层函数继续处理。

关键特点

  1. 保留原始异常类型和错误信息。

  2. 异常处理流程从当前函数退出,继续沿调用链向上查找匹配的 catch 块。

函数内部分配了动态内存,若操作失败需释放内存,再将异常传递给外层处理。

生活实例:快递异常处理流程

场景

快递员(内层函数)派送包裹时发现收件人地址错误(异常),进行初步处理(记录问题),再将问题上报分拣中心(外层函数)。

3.3 异常安全

①构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化。

②析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄露(内存泄露,句柄未关闭等)。

③C++中异常经常会导致资源泄露的问题,比如在new和delete中抛出了异常,导致内存泄露,在lock和unlock之间抛出了异常导致死锁,C++经常使用RALL来解决问题。

3.4 异常规范

①异常规范说明目的是为了让函数使用者知道该函数可能抛出的异常有哪些。可以在函数的后面加上 throw(类型) ,列出这个函数可能抛出的所有异常类型。(如下图)

②函数的后面加上noexcept,表示函数不抛异常

③若无异常接口声明,则此函数可以抛任何类型的异常

4. 自定义异常体系

为什么需要自定义异常体系?

在大型项目中,如果开发者随意抛出不同类型的异常,外层调用者将难以编写通用的异常处理逻辑。自定义异常体系通过以下方式解决问题:

①统一管理:所有异常继承自一个公共基类,确保类型一致性。

②简化捕获:只需捕获基类,即可处理所有派生类异常。

③扩展灵活:新增异常类型时无需修改已有捕获逻辑。

代码示例(如下图):

对比无自定义体系的混乱场景

后果

外层调用者被迫编写复杂捕获逻辑

  • 维护困难:新增异常类型需修改所有调用链。

  • 可读性差:错误处理分散,难以统一日志或恢复操作。

通过自定义异常体系,代码和现实系统一样,实现 错误分类标准化、处理流程统一化、扩展灵活化,如同为程序打造一套高效的"应急管理机制"。

5. C++标准库的异常体系

C++ 提供了一系列标准的异常,定义在 中,我们可以在程序中使用这些标准的异常。它们是以父 子类层次结构组织起来的,如下所示:

下表是对上面层次结构中出现的每个异常的说明:

说明:实际中我们可以可以去继承exception类实现自己的异常类。但是实际中很多公司像上面一 样自己定义一套异常继承体系。因为C++标准库设计的不够好用。

6. 异常的优缺点

C++异常的优点:

  1. 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包 含堆栈调用的信息,这样可以帮助更好的定位程序的bug。

  2. C语言返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那 么我们得层层返回错误,最外层才能拿到错误。

  3. 很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们 也需要使用异常。

  4. 部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如 T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回 值表示错误。
    C++异常的缺点:

  5. 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会 导致我们跟踪调试时以及分析程序时,比较困难。

  6. 异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。 3. C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常 安全问题。这个需要使用RAII来处理资源的管理问题。学习成本较高。

  7. C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。

  8. 异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常 规范有两点:一、抛出异常类型都继承自一个基类。二、函数是否抛异常、抛什么异常,都 使用 func() throw();的方式规范化。

总结:异常总体而言,利大于弊,所以工程中我们还是鼓励使用异常的。另外OO的语言基本都是 用异常处理错误。

好啦,本篇文章到这里就结束啦~

有问题欢迎指正批评~

相关推荐
Antonio9153 分钟前
【设计模式】单件模式
开发语言
-SGlow-22 分钟前
Linux网络相关概念和重要知识(2)(UDP套接字编程、聊天室的实现、观察者模式)
linux·运维·服务器·网络·c++·观察者模式·udp
闲人编程24 分钟前
OpenCV图像读写与Mat对象深度解析
c++·python·opencv
jllws128 分钟前
数据类设计_图片类设计之8_自由图形类设计_(前端架构)
前端·c++·数据类设计
火龙谷38 分钟前
【nodejs】爬虫路漫漫,关于nodejs的基操
开发语言·javascript
hunandede42 分钟前
QT三 自定义控件
开发语言·qt
勇敢滴勇44 分钟前
Qt信号与槽高级特性与项目实战:原理剖析与工程化应用指南
网络·数据库·c++·qt·qt5·qt6.3
码农诗人1 小时前
C++调用ffmpeg解复用、解码案例
c++·ffmpeg·解码·解封装
sin25801 小时前
蓝桥杯C++基础算法-多重背包
c++·算法·蓝桥杯
sin25801 小时前
蓝桥杯C++基础算法-0-1背包
c++·算法·蓝桥杯