关于C++中的异常概念理解

news/2024/10/9 19:30:06/文章来源:https://www.cnblogs.com/hhail08/p/18454971

1. 基本概念

异常,即 exception,是C++中的基本概念之一,在某段程序发生无法继续正常执行的情况时,C++允许程序进行所谓抛出异常(有时也被称为吐出异常)的行为,这些被抛出的异常,会自动地从触发点开始向外传播,直到被捕获(有时也被称为吞下异常)或者程序终止。


2. 语法

2.1 抛出异常

下面用一个简单的例子,说明异常是如何发生的:

// 一个简易除法器,返回 a ÷ b 的结果
float divider(float a, float b)
{// 当除数b为0时,抛出异常!if(b == 0)throw "除数不可为零";return a/b;
}int main(void)
{float a,b;// 从键盘获取被除数a和除数b,并输出结果while(1){cin >> a >> b;cout << divider(a, b) << endl;}return 0;
}

在上述代码中,除法器函数 divider() 中出现了一个新的关键字:throw,它代表抛出异常操作,所谓的抛出异常,就是在程序中触发一个必须处理的情况,如果不处理,程序将会被强行终止。像上述代码,main() 函数没有对除法器 divider() 潜在会抛出的异常进行任何处理,因此当输入的除数 b 为0时,触发的异常将直接关闭程序。

注意
上述代码的流程是:
image
如果异常发生在更里层的函数调用中,那么异常会一直向外传播,直到遇到main函数,如果一直没有任何函数捕获异常,那么异常就会终结程序,这是异常的第一个特征:强制性。

假设有如下代码:

void f3()
{throw "抛出异常";
}
void f2()
{f3();
}
void f1()
{f2();
}int main()
{f1();cout << "此语句不会被执行" << endl;
}

此时,异常发生在函数 f3() 处,然后一路传播到最终的调用者 main() 函数,这就是异常第二个特性:传播性。
image


2.2 捕获异常

如果程序不想被异常终止,那么就应该在异常的传播链上的某一环对其进行处理,即所谓的捕获异常,或吞下异常等操作,即 catch,例如上述调除法器的程序,可以让main函数捕获除数为0的异常:

/ 一个简易除法器,返回 a ÷ b 的结果
float divider(float a, float b)
{// 当除数b为0时,抛出一个字符串异常if(b == 0)throw "除数不可为零";return a/b;
}int main(void)
{float a,b;float ans; // ans = a ÷ b// 从键盘获取被除数a和除数b,并输出结果while(1){cin >> a >> b;// 试图执行 a÷btry{ans = divider(a, b);}catch(const char * &e){// 发生了异常cout << e << endl;break;}// 输出运算结果cout << ans << endl;}return 0;
}

以下是执行效果:

gec@ubuntu:~$ ./a.out
88 0
除数不可为零
gec@ubuntu:~$ 

关键代码:

try{有潜在抛出异常可能的代码块
}
catch(类型1 e)
{处理异常类型1
}
catch(类型2 e)
{处理异常类型2
}
catch(...)
{处理其他异常类型
}

语法要点:

将有可能出现异常且需要捕获这些异常的代码,放入 try{} 代码块中。
try{} 代码块至少带一个 catch(){} 代码块,也可以带多个。
当异常发生时,多个 catch(){} 代码块中最多执行一个,用类型来区分它们。
当没有任何 catch(){} 的类型能匹配异常时,异常会被继续向main函数传播,如果没有任何函数捕获异常,程序将被异常终止。
可以用 catch(...){} 来匹配任意类型的异常。

2.3 异常的类型

捕获异常时,如果有多个 catch(){} 代码块,它们是以异常的类型来彼此区分的,也就是说如果被调函数有触发异常的多种不同的可能,且每种不同的异常情况需要不同的处理方式,那么要抛出不同的异常类型来区分它们,示例代码如下:

void f3(int n)
{// 当遇到三种不同的异常需要区别以待时// 抛出三种不同类型的异常if(n == 1)throw 666;if(n == 2)throw 1.23;if(n == 3)throw "abc";cout << "程序正常执行" << endl;
}
void f2(int n)
{try{f3(n);}catch(const int &e){cout << "遇到了异常情况1:" << e << endl;cout << "处理完毕" << endl;}
}
void f1(int n)
{try{f2(n);}catch(const double &e){cout << "遇到了异常情况2:" << e << endl;cout << "处理完毕" << endl;}
}int main()
{int n;cin >> n;f1(n);cout << "遇到异常情况3时,程序无法处理,将被终止" << endl;
}

上面的程序展示了如何为不同的异常安排一个类型来加以区分,它们分别是:

返回一个 int 型数据时,代表发生了异常情况1
返回一个 double 型数据时,代表发生了异常情况2
返回一个 const char * 型数据时,代表发生了异常情况3
乍一看这设计有问题,因为如果异常情况较多(比如有10种不同的异常情况),那么就很难找到这么多不同的数据类型来对它们进行彼此区分了,而在原来C语言的框架下,不同的错误情况只需要对应不同的错误值即可,比如返回-1、-2、-3来区分不同的错误。

问题的奥秘在于,抛出的异常一般是以类对象的方式出现的,而类是可以继承的,这样一来,不仅可以让每一种不同的类型异常对应灵活定义的类对象,还能利用类的层次关系在顶层的异常父类中定义通用的报错渠道,极大地扩展了对错误信息处理的灵活性。事实上,C++已经提供了标准的异常类,来作为各种不同异常情况的顶层父类,供我们扩展使用,它们被定义在如下头文件之中:

#include <exception>

C++为每种不同的异常定义了标准异常类,既可以直接使用这些异常类,也可以根据实际所需扩展这些类:
image


一个使用上述标准异常类的简单例子:

#include <iostream>
#include <exception>using namespace std;float divider(float a, float b)
{// 抛出标准异常对象if(b == 0)throw invalid_argument("除数不可为零");return a/b;
}
int main(void)
{float a,b;float ans; // ans = a ÷ bwhile(1){cin >> a >> b;try{ans = divider(a, b);cout << ans << endl;}// 捕获参数非法异常catch(invalid_argument &e){cout << e.what() << endl;}}return 0;
}

关于运用类的继承机制,基于标准异常类扩展自定义异常类,可以等到学习了类与继承之后,再自行进行练习,目前可以简单将标准异常类看做就是一种数据类型即可。


3. 异常与返回错误值的区别

在刚接触C++异常机制的时候,很容易会跟C语言的函数出错返回相对比,掌握异常机制的语法规范很简单,但关键还要理解这么设计的初衷和用意。

首先要理解,当一个函数发生异常时,类似于在某个组织中的一个基层人员遇到了某个无法处理的问题,这个问题可能是基层人员能力、权限、格局所致,正确的做法是将该问题逐级上报,每一级有每一级的解决问题的层次,基层解决不了的,上一级有可能可以解决,或者基层不知道该怎么处理的,上一级有可能知道怎么处理,这种层层上报的工作须是自动发生的,如果某一个层级不对基层上报上来的异常做任何处理,那么该异常会自动地传播给上上级,且如果组织中没有任何对已出现的异常负责,也无人进行处理,那么为了避免埋雷,避免留下隐患,整个组织将会因为这个无人认领的异常而被强制终止运转。基于这样的逻辑设定,C++发明了异常机制,来更好地、更方便地维护程序的健壮和安全。
在函数内抛出异常,跟返回错误值,有如下区别:
(1)传播性
返回错误值,不具备自动传播性,某层级的代码无法处理基层传来的错误,依然需要做判定和处理,非常麻烦。
异常具备自动传播性,遇到与本层级无关、或不属于本层级管理的异常,不管它即可,它会自动向主函数传播。
(2)强制性
返回错误值,不具备强制性,容易给程序埋下难以察觉的隐患。异常具备强制性,不处理程序就会自动终止,将隐患尽早暴露出来。
(3)灵活性
返回错误值时,用不同的值来区分不同的错误,类型是固定的,即无法使用类与继承的方式代码重用。触发异常时,用不同的类型来区分不同的错误,天生就支持类与继承方式来代码重用,并且可以通过设定标准异常类来规范各种形式的错误信息,灵活性大大提高。


4. 异常与析构函数

5. 异常安全性问题

#include <iostream>
#include <string>
#include <exception>
#include <stdexcept>using namespace std;class err
{
public:err(string const &s){_msg = s;}string &msg(){return _msg;}private:string _msg;
};void f1(int a)
{// 抛出的异常,可以是普通变量,也可以是类对象// 抛出的异常对象与函数传参类似,被传送至catch语句// 但有以下两点不同:// ① 异常对象能持续追踪try...catch语句,直到遇到匹// 配的类型,或者程序退出// ② 异常对象在匹配的catch语句之后仍然存在不会释放if(a == 1)throw "abcd";else if(a == 2)throw 3.14;else if(a == 3){throw (runtime_error("运行时错误"));}else if(a == 4){throw (err("自定义错误"));}else{throw ('a');}return;
}void f2(int n)
{try{f1(n);}catch(const char *e){cerr << "错误:" << e << endl;}catch(double e){cerr << "错误:" << e << endl;}
}int main(void)
{int n;while(1){cin >> n;try{f2(n);}catch(runtime_error &e){string errstr;errstr.append(__FUNCTION__).append(":").append(e.what());cerr << errstr << endl;}catch(err &e){string errstr;errstr.append(__FUNCTION__).append(":").append(e.msg());cerr << errstr << endl;}catch(...){cerr << "其他未知情况" << endl;}}return 0;
}#include <iostream>using namespace std;bool DBEnable = false;void close()
{if(!DBEnable)throw 100;elsecout << "数据库正常关闭了" << endl;
}// 类对象用来管理一个数据库的连接
// 当对象被释放时,析构函数将自动断开数据库的连接
class A
{bool closed;public:A(){closed = false;}void dbclose();~A();
};void A::dbclose()
{close();closed = true;
}// 当对象被释放时,析构函数将自动断开数据库的连接
// 而析构过程中的 close() 可能会触发异常
A::~A()
{if(!closed){// close会触发异常,不处理的话// 该异常将外溢到析构函数之外的其他代码,造成程序终止或者不明确行为// 因此使用try...catch捕获异常是类对象职责所在// 但如果仅提供此项功能,就意味着类对象的使用者无法干预close所带来的问题try{close();}catch(...){cout << "close触发了异常" << endl;}}
}int main()
{// 对象 a 出了代码块将被释放,届时触发析构函数// 由于 a 的使用者(即本main函数)不对数据库关闭可能触发的异常感兴趣// 因此以下代码可能会错失处理该异常的第一时机{A a;}A b;// 作为类对象的使用者,可以显式地使用 try...catch 来捕获异常// 一旦发现确有问题,便可酌情作出处理// 相反,如果将可能触发异常的 close 只封装在类析构函数中,由于析构函数// 被调用的时机的特殊性,将会使得类对象使用者则很难或无法针对异常做出反应try{b.dbclose();}catch(...){cout << "数据库关闭异常,接下来做出某些操作... ..." << endl;DBEnable = true;}return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/810709.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于FPGA的8PSK调制解调系统,包含testbench,高斯信道模块,误码率统计模块,可以设置不同SNR

1.算法仿真效果本系统在以前写过的8PSK调制解调系统的基础上,增加了高斯信道模块,误码率统计模块,可以验证不同SNR情况下的8PSK误码情况。VIVADO2019.2仿真结果如下(完整代码运行后无水印):设置SNR=30db其对应星座图:设置SNR=15db其对应星座图:设置SNR=10db其对应星座图…

java学习10.9

网上找的javaweb教程 Servlet+Mybatis+Thymeleaf的javaweb图书管理系统 实现用web界面对数据库的增删改查 前端页面css样式为现成的东西修改 项目架构整体页面

实验2

task1点击查看代码 #include<stdio.h> #include<stdlib.h> #include<math.h>#define N 5 #define N1 397 #define N2 476 #define N3 21int main() {int cnt;int random_no,random_major;srand(time(NULL));cnt = 0;while (cnt<N){random_major=rand()%2;…

『模拟赛』多校A层冲刺NOIP2024模拟赛04

『模拟赛记录』多校A层冲刺NOIP2024模拟赛04Rank 赤石场。A. 02表示法 签。 若干天前在洛谷随到过,不过当时只看了眼讨论区就走了www 还好本来不是很难。 发现大体上是一个拆分二的幂的问题,从大到小枚举 2 的幂,判断有没有这个幂只用比较大小关系,然后再对指数做一个同样的…

实现一个烟花效果

1. 首先创建一个烟花类,有烟花上升的效果,烟花爆炸的两种效果(爆炸型和球型)2. 创建线的属性方法,因为上升效果和爆炸效果都会用到3. 上升效果为了达到那种螺旋上升的效果可以通过sin函数实现一个点的偏移量4. 爆炸效果则是将随机生成多条半径不同的线5. 球形效果则是将规…

【Java】反射

Java中的反射机制 动态代理反射 允许对封装类的字段,方法和构造函数的信息进行编程访问 ==》 反射允许对成员变量,成员方法和构造方法的信息进行编程访问 基本操作:获取(获取class对象【字节码对象】) + 解剖 成员变量 Field —— 修饰符、名字、类型、赋值 构造方法 Cons…

DNShell

DNShell 一款基于DNS C2隧道的反弹shell工具。 支持 功能: 支持DNS-recordA-直连型 的C2隧道。 目标: Windows下基于Powershell的反弹。 Linux下基于ShellScript的反弹。 使用方法

【Redis】Redis学习笔记

概况 redis == remote Dictionary Server (远程字典服务) 基于内存的KV键值对内存数据库 作用:分布式缓存,与MySQL共同配合Redis -- 内存 MySQL -- 磁盘Redis -- NoSQL MySQL -- SQL内存存储 和 持久化(RDB+AOF)Redis支持一部将内存中的数据写入硬盘宕机 -- 可自行恢复高…

基于禁忌搜索算法的VRP问题求解matlab仿真,带GUI界面,可设置参数

1.程序功能描述基于禁忌搜索算法的VRP问题求解matlab仿真,带GUI界面,可设置参数。 2.测试软件版本以及运行结果展示MATLAB2022a版本运行 3.核心程序while COUNT<=Itertions ֲ L = zeros(Ant_Num,1); for i=1:Ant_Num Infor_Tabu_tmps = Infor_Tabu(i,:); R = Inf…

CMake 属性之全局属性

CMake 的全局属性是指在 CMake 配置过程中,对整个项目范围生效的设置。这些属性不同于目标 ( Target ) 属性或目录 ( Directory ) 属性,后者仅对特定的目标或目录生效。【写在前面】 CMake 的全局属性是指在 CMake 配置过程中,对整个项目范围生效的设置。 这些属性不同于目标…

自然人信息社工

人,是网络安全全流程中最大的弱点。针对人的攻击往往有出奇不意的效果。而想要利用人的弱点进行攻击,那么对目标的信息收集与了解就是非常重要的了。这篇文章记录了一些常用的用于对人进行身份信息收集的技术。这些技术常被用于溯源取证、社工攻击。 0x00 社工分析中的身份信…

Linux软中断ksoftirqd

前言 在上一篇 LINUX软中断-softirq的描述中,提到过ksoftirqd,这篇文章就介绍ksoftirqd ksoftirqd 是什么? ksoftirqd 是个内核线程,在创建的时候是绑定cpu的,每一个core对应生成一个ksoftirqd 线程 比如当前系统有4个core ~# ps aux | grep ksoftirqd root 3 0.0…