当前位置: 首页 > news >正文

【c++】异常详解

目录

  • C语言处理错误的局限性
  • 异常的定义
  • 异常的具体使用细则
    • 异常的抛出与捕获
    • 在函数调用链中异常栈展开匹配原则
    • 异常的重新抛出
    • 异常规范
      • throw(类型)
      • noexcept
  • 成熟的异常体系
  • c++自己的异常体系
  • 异常的优缺点
    • 优点
    • 缺点
  • 异常安全

C语言处理错误的局限性

C语言处理错误常常会用到assert和打印错误码这两种方式。assert可以在检测到错误之后立刻终止程序报错,但是assert只会在debug版才会奏效,release版就不会产生效果了,C语言期望程序员在代码测试阶段就完成对全部错误的排查,太过于理想。此外,终止程序的方式也太过于暴力,试想一下生活中我们使用的程序或网站因为一个小掉线就直接报错终止,未免太过于大惊小怪,使用体验很糟糕。而对于打印错误码的方式,首先打不会直接终止程序,也不会在release版失效,这两点看起来很好。c语言错误码是通过全局变量的方式实现的,程序有异常,就会将错误码传给errno(C语言规定的错误码,是一个全局变量),这时可以通过调用perror函数等方式打印错误码,但是这种方式打印的错误码可读性很差,且error作为全局变量需要及时打印,不然下一个错误出来就会被覆盖,这样就要写很多if语句打印错误码,倘若想要使用函数返回统一处理也要一层一层的返回很麻烦,忘记返回也不会报错,就会出现纰漏。总而言之,c语言的这套处理错误的方式还是很麻烦的,所以c++引入了异常来替代这种方式。

异常的定义

异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误。

#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>using namespace std;void func()
{throw "func() error";
}int main()
{try{func();}catch (const char* x){cout << x << endl;}return 0;
}

这就是抛异常的一个简单的使用场景。场景中有三个异常中会使用到的关键字,分别是:
1.throw:判断当前程序出现异常时用来抛出异常所使用的。
2.try:try块中的代码会标识将要被激活的特定异常,它后面通常跟着一个或多个 catch 块。try块中的代码被称为保护代码。
3.catch: 在想要处理问题的地方,通过异常处理程序捕获异常,catch 关键字用于捕获异常,可以有多个catch进行捕获。

异常的具体使用细则

异常的抛出与捕获

异常使用throw关键字抛出,c++支持抛出任意类型的异常,而抛出的异常的类型会与对应try块的catch块的参数所匹配(参数只用来匹配的话可以只写类型),匹配成功执行流就会进入该catch块内部,由于抛出的对象只有一个,所以catch块的参数也就只会有一个。
一个try块是可以有多个catch块的,所有抛出的异常会与对应try块的所有catch块自上而下按照顺序匹配,找到第一个匹配的参数就会进入,当抛出的异常没有被catch捕获时就会报错。
抛出异常的对象的传递类似函数的返回值,会拷贝生成一个临时对象,这个临时对象的生命周期会持续到对应的catch块执行结束,而且这个对象不像一般的临时变量,这个变量是不具有常形的(const,可以被普通引用接收),因为抛出的对象如果具有常性后续对于抛出对象的修改操作就没法进行了。
catch(…)可以捕获任意类型的异常,但是也没法知道捕获的是什么类型,一般不会把它放在中间,而是会将其放在最后,以防出现未知错误时及时兜底。
catch语句中的参数匹配规则并不都是要求类型完全匹配。首先,是支持非const对象向const对象转换的;再者,也是支持数组名转数组指针和函数名转函数指针的;然后,const void*指针可以接受任何类型的指针;最后同样也是最重要的,c++支持抛出派生类对象 / 指针 / 引用被基类对象 / 指针 / 引用来接受,这非常常用,在现实中的实际项目中常常采用以基类为基础继承出各个板块的派生类,再由catch块基类类型接受,再利用多态达到接受体系中各种错误的效果,这个后面会详细讲解。

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

对于throw出的异常,首先要检查throw本身是否在try块中,在的情况下就会自上而下查找与try块对应的catch块,由匹配的就会到匹配的catch块运行。
如果throw不在当前的函数栈中的try块中或者在try块中但是没有与之匹配的catch块,就会退出当前的栈到调用这个函数的栈中看是否在try块中以及是否有与之对应的catch块。
如果到达main函数栈也依旧没有找到与之匹配的,就会报错终止程序。上述的这个沿着调用链查找匹配的catch子句的过程称之为栈展开。一般来说,最后都要加上catch(…)兜底,防止因为疏忽没有捕获导致程序直接终止。

#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>#include<string>using namespace std;void test()
{throw string();
}int main()
{try{test();}catch(...)//接受任何类型{}
}

在throw出异常后,程序就不会在按照正常的流程去跑了,先是按照栈展开(调试中栈展开这个过程是看不到的,编译器在throw后会直接跳转到匹配的catch块,或者没有匹配的直接报错,不会一个栈一个栈地退,这是编译器优化的结果)进行回退,回退时会消除没有匹配的函数栈帧,到达catch块处理完后会继续执行catch子句后面的语句。

异常的重新抛出

有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理。

#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>#include<string>using namespace std;void test_2()
{string str("test_2 error");throw str;
}void test_1()
{int* a = new int(1);test_2();cout << "Deletion successful" << endl;delete a;
}int main()
{try{test_1();}catch (string x){cout << x << endl;}return 0;
}

像上面这种情况,如果自己向堆上动态申请了内存,而且想要在main函数中统一处理异常,此时就会因为throw出的异常导致跳过了堆的内存释放,这样就会导致内存泄漏,

#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>#include<string>using namespace std;void test_2()
{string str("test_2 error");throw str;
}void test_1()
{int* a = new int(1);try{test_2();}catch(string x){cout << "Deletion successful" << endl;delete a;throw;}cout << "Deletion successful" << endl;delete a;
}int main()
{try{test_1();}catch (string x){cout << x << endl;}return 0;
}

这时我们就可以先捕获一下异常,在catch块中释放掉申请的内存,然后重新抛出异常给后面的catch块接受统一处理。重新抛出的方法也很简单,直接在catch块中写throw; 就表示将捕获到的异常再次抛出。

重新抛出的方式可以解决一些简单场景的内存释放,但对于多次内存开辟加多次函数调用,因为内存开辟也是会开辟失败抛异常的,所以会要求捕获多种类型,就会比较麻烦,需要搭配智能指针来使用。

异常规范

throw(类型)

1.异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的后面接throw(类型),列出这个函数可能抛掷的所有异常类型。
2. 函数的后面接throw(),表示函数不抛异常。
3. 若无异常接口声明,则此函数可以抛掷任何类型的异常。

void test_1() throw()//不会抛异常
{//...
}void test_2() throw(int, double)//只会抛int和double类型的异常
{//...
}void test_3() //可以抛任何类型的异常
{//...
}

这套体系看起来严格规定了函数异常抛出的类型,但是即使不遵守,函数也不会报错,这是c++为了兼容旧代码导致的结果。所以这就象是一种口头承诺了,防君子而不防小人。其实,从实际使用的角度上来说,这样的设计也是不好的,每次写函数表明可能会抛出的异常类型会非常麻烦,而且实际开发时就连开发者自己也不会清楚自己这个函数会抛出多少种异常,也许我写的这个函数调用的接口是项目组中的其他人所写的,也有可能未来还要对项目的功能进行扩展调用更多的接口,这些都是不确定因素,全部写明是非常困难且麻烦的。而且throw()是在运行时生成隐式try-catch代码块,动态检查异常类型是否规范,会有一定的性能开销。这种设计本意希望帮助调用者预判异常类型,简化错误处理逻辑,但在实际开发中没什么用处,而且在c++的新版本中也已经逐步被废弃,throw仅作为提示作用兼容老版本代码,​​运行时不会强制验证,所以不推荐使用。

noexcept

void test_1() noexcept//不会抛异常
{//...
}void test_2() //会抛各种类型的异常
{//...
}

noexcept是c++11新增的关键字,用来替代throw()用的。函数后面加noexcept表示这个函数不会抛出异常,编译器会严格检查声明函数是否抛出了异常。相较于throw()的运行时的动态检查 ,noexcept是在编译时就确定的静态检查,编译器还进一步优化了noexcept的异常抛出,noexcept的异常抛出不会进行栈展开一个个释放函数栈帧,而是直接报错,所以noexcept比throw的效率高很多。noexcept声明过的函数表示不会抛出异常,编译器也会对此做出优化,进一步提升函数效率。

综上,实际写代码时如果遇到确定不会抛异常的函数,加一个noexcept优化一下就行了,throw因为各种历史包袱,现在已经被废弃,没什么人用了。

成熟的异常体系

虽然c++在语法上支持我们抛出任意类型的异常,但是在成熟的项目开发中是有一套成熟的异常体系的,一个好的异常体系可以大大提高项目开发效率,减少项目维护成本。

#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>#include<string>using namespace std;class My_Exception
{
protected:int err_id;string err_msg;
public:My_Exception(const string& errmsg, int errid):err_id(errid),err_msg(errmsg){}virtual string what() const{return err_msg;}
};class A : public My_Exception
{string A_err_mesg;
public:A(int x, string y, string z):My_Exception(y, x),A_err_mesg(z){}virtual string what() const{string str("A : ");str += err_msg;str += ",";str += A_err_mesg;return str;}
};class B : public My_Exception
{string B_err_mesg;
public:B(int x, string y, string z) :My_Exception(y, x),B_err_mesg(z){}virtual string what() const{string str("B : ");str += err_msg;str += ",";str += B_err_mesg;return str;}
};void test_A()
{A x(1, "错误描述", "A组的错误信息");throw x;
}void test_B()
{B x(2, "错误描述", "B组的错误信息");throw x;
}int main()
{try{test_A();}catch (My_Exception& x){cout << x.what() << endl;}try{test_B();}catch (My_Exception& x){cout << x.what() << endl;}return 0;
}

上面是一个简单的一场体系,实际的项目中肯定会更成熟以及复杂。但通过上面的例子可以看出一场体系的关键所在。即定义一个类专门用来记录最基本的异常信息,这里就给出了错误id和错误描述,错误id可以表示哪个模块发生了错误,这个我们可以事先定义好,比如数据库模块的id为001,网络模块的id为002,等等;错误描述可以是一些最基本的错误描述,比如权限不足,数据不存在,等等一些通用的错误描述。这些异常信息必须是所有异常都会有的,然后再由这个类衍生出各种派生类,这些派生类记录实际项目中各个模块的错误信息,他们可以在继承父类的最基本的错误信息的基础之上,再定义一些自己独有的错误信息,比如数据库模块的错误就可以给出具体的sql语句,网络模块错误就给出错误码等等,这些东西的自由度很高,我们可以给出很详细的信息极大的方便我们在代码错误之后的错误查找。然后就是这些错误的捕获,我们之所以使用派生类来作为记录异常的类就是因为c++允许用catch子句用基类的对象 / 指针 / 引用接受派生类的对象 / 指针 / 引用,所以我们就可以定义一个what虚函数,体系中每个类的what虚函数都返回自己整理好的独特的错误信息,这样通过多态达到以一种类型接受整个异常体系中所有异常类的效果,如果不使用这套思路,面对大型项目的多个模块,光写catch子句就得写累死,所以这套体系还是强烈推荐的。

c++自己的异常体系

c++有自己的异常体系,也是通过父子类来完成的。
在这里插入图片描述
在这里插入图片描述
这些错误我们在日常写代码时肯定都遇到过一些。
在这里插入图片描述
我们可以用try块包裹住可能会出现错误的位置,使用exception类型就能捕获这些错误发生时会抛出的异常,当然,我们也能主动抛出这些异常,

#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>#include<string>using namespace std;int main()
{try{throw bad_alloc();//new会抛出的异常}catch (const exception& e){cout << e.what() << endl;}return 0;
}

c++虽然有自己的一套异常体系,但是不够好用,很多公司都会有自己的一套异常体系。

异常的优缺点

优点

(1)异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug。
(2)返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们得层层返回错误,最外层才能拿到错误,具体看下面的详细解释。
(3)很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们也需要使用异常。
(4)部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误。

缺点

(1) 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难,有时候打断点都可能断不住。
(2)异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。
(3)C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题。学习成本较高。
(4)C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。
(5)异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常规范有两点:一、抛出异常类型都继承自一个基类。二、函数是否抛异常、抛什么异常,都使用 func() throw();的方式规范化。

总的来说,异常还是利大于弊,日常代码中可以使用来对代码中的各种错误进行检查。c++的异常也被后来的一些面向对象语言所借鉴。

异常安全

(1)构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化。
(2)析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)
(3)C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题。

相关文章:

  • 2.1 微积分基本想法
  • Linux操作系统安全加固
  • Maven私服搭建与登录全攻略
  • Qt进阶开发:QTcpServer的的详解
  • [高阶数据结构]二叉树经典面试题
  • 蚁群算法赋能生鲜配送:MATLAB 实现多约束路径优化
  • Vue:插值表达
  • pytorch模型画质增强简单实现
  • 关系型数据库和非关系型数据库
  • 一次IPA被破解后的教训(附Ipa Guard等混淆工具实测)
  • rust 全栈应用框架dioxus server
  • AI大模型学习十九、利用Dify+deepseekR1 使用文件上传搭建文章理解助手
  • FastMCP v2:构建MCP服务器和客户端的Python利器
  • java 中 DTO 和 VO 的核心区别
  • 一键解锁嵌入式UI开发——LVGL的“万能配方”
  • ASP.NET Core Identity框架使用指南
  • 如何使用 React Hooks 替代类组件的生命周期方法?
  • 【AI大语言模型本质分析框架】
  • 2025年第十六届蓝桥杯软件赛省赛C/C++大学A组个人解题
  • uniapp|商品列表加入购物车实现抛物线动画效果、上下左右抛入、多端兼容(H5、APP、微信小程序)
  • 威尼斯建筑双年展总策划:山的另一边有什么在等着我们
  • 60余年产业积累,“江苏绿心”金湖炼就“超级石油工具箱”
  • 他站在当代思想的地平线上,眺望浪漫主义的余晖
  • 中美会谈前都发生了什么?美方为何坐不住了?
  • 湖北宜昌:在青山绿水间解锁乡村振兴“密码”
  • 总奖池超百万!第五届七猫现实题材征文大赛颁奖在即