IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++异常处理 -> 正文阅读

[C++知识库]C++异常处理

我好像认识很多个用C的方式写C++的人。难道是因为我圈子里大多是嵌入式工程师嘛?

上周我跟北京研发的一个哥们合作,因为他提供的动态库总是段错误,我说你提供的这个接口没有做入参的检查吧,你发现入参不合法之后给我抛个异常出来吧,别让它再段错误了。

他说我抛了啊,我怎么可能连异常都不知道呢?

于是我说你让我看一眼你是怎么抛异常的。

?if(pos?>?MAX){
?????throw?"out?of?range!";?
?}

我tm想把他和追梦格林扔到八角笼中决斗一番。

1

一知半解有时候甚至还不如完全不懂,后者由于自知害怕出错,一般会采取比较稳妥的方式,前者经常不懂装懂贪功冒进。

让我们拨动秒针,穿梭时空,回到自己刚刚学C语言的时候。

老师说,函数虽然可以写多个入参,但只能有一个返回值(某Go语言开发者:what?),我们需要在写函数的时候指明返回值的类型。

当然,大家的技术水平都是比较高的,多数情况下,函数都会正常的运行。但老虎也有打盹的时候,偶尔也会因为一些疏忽导致函数出错。此时我们应该返回什么呢?

我总结了三种情况,错误码,NULL,空

但这三种情况的用法某种意义上是一样的:都需要对返回值做判断。即如果返回值正常,则继续往下走;如果返回值出错/为NULL/为空则不能继续,因为会引发各种错误。

此时C++迈着大步跑了过来,“加上我,我还有一种情况,叫做异常!”

2

错误码的机制有点像健康码,园区保安需要核对每个人的健康码是绿色还是红色,决定是否让你进入园区工作;当然会很有效,但缺点是太过繁琐,效率低下,影响美观。

异常的机制则像有个人站在30层的楼顶要往下跳,如果没人拿防护网接住他(try-catch),那么他将崩溃殒命。但你也可以选择在1楼到29楼的任何一层拿防护网接住他,询问他崩溃的原因,那么悲剧将有可能被挽回。

当然,你也可以选择先接住他,问明原因后再把他扔出去(throw),告诉他人间的悲欢并不相通,我只是有点八卦。

至于扔出去之后他是挂掉还是又被人接住,取决于你这层楼的下面还有没有防护网。

这30层楼就是函数层层调用搭起来的高楼大厦,所以异常跳楼的时候也是层层传递。即在20层楼处搭建的防护网只能捕捉到20楼以上跳楼的靓仔,却捕获不到从19楼跳楼的住户。所以出于安全起见,我们一般会在一楼支起一个防护网捕捉所有跳楼的靓仔,然后安稳的睡去。

当然如果你选择在30层楼的每一层都搭一个防护网,那我建议你别干消防了,去园区门口当保安查健康码吧,那个适合你。

3

错误码的返回一般有两种形式,一种是占用函数返回值,形如

?int?fd?=?open(char*?filename);

一种是返回值另有他用,所以需要占用一个入参或者使用修改全局变量的方式。函数将错误码填入传进来的入参中,一般为指针形式,形如

?double?val?=?GetValue(RetStatus*?errcode);
?//?或者定义一个全局变量
?RetStatus?ErrCode;
?double?val?=?GetValue();

详细错误信息的获取也有两种方式,一种是直接返回一个错误码和错误信息的结构体,形如

?struct?RetStatus{
?????int?errCode;
?????char[512]?errMsg;
?}
?if?(/*错误1*/){
?????Return?RetStatus{1,"错误1"}
?}else?if?(/*错误2*/){
?????Return?RetStatus{2,"错误2"}
?}else{
?????Return?RetStatus{3,"错误3"}
?}

一种是只返回错误码,错误信息定义为全局变量。接收者通过strerror(errno)的方式获取详细的错误信息。

无论采取什么样的形式来设计,错误码的机制都决定了接收者需要对其进行判断;而异常则不用层层处理,可以避免“必须检查返回值,不能遗漏一个的情况”。

以读文件里面的read函数举例,

int?read(int?fd,char*?buf,int?count);

read函数的返回值,在正常时返回读取到的字节数,在文件末尾调read时返回0,出错时返回-1并设置errno。

假如A函数调用了read,用于将读取到的数据做分割、提取、处理等工作;

B函数调用了A函数,将处理后的数据用plot控件绘制成图形;

C函数调用了B函数,将图形显示到UI界面上。

那么ABC函数得这么写:

?int?A(char*?input,char*?output){
?????int?ret?=?read(fd,input,len);
?????if(ret<0){
?????????return?ret;
?????}
?????//?ret>=0?继续做处理
?????return?len(output);
?}
?int?B(char*?buf,char*?plot){
?????char[512]?output;
?????int?ret?=?A(buf,output);
?????if(ret<0){
?????????return?ret;
?????}
?????//?ret>=0?继续做处理
?????return?len(plot);
?}
?bool?C(char*?data,char*?plot){
?????int?ret?=?B(data,plot);
?????if(ret<0){
?????????printf("Error:%s\n",strerror(errno));
?????????return?false;
?????}
?????//?ret>=0?继续做处理
?????return?true;
?}

由此可见,我们需要在每一层都得判断一下函数返回值是否正确,NULL和空也是一样的情景。这样写一方面写的人觉得繁琐,另一方面看的人也觉得啰嗦。

作为对比,异常则是只需在read函数里throw,在函数C里catch即可。当然前提是A,B函数里的代码要保证异常安全

?void?A(char*?input,char*?output){
?????read(fd,input,len);???
?????//?继续做处理????
?}
?void?B(char*?buf,char*?plot){
?????char[512]?output;
?????A(buf,output);
?????//?继续做处理????
?}
?void?C(char*?data,char*?plot){
?????try{
?????????B(data,plot);?
?????????//?继续做处理
?????}catch(const?std::exception&?e){
?????????std::cerr?<<?e.what()?<<?std::endl;
?????????//?一些释放资源的操作????????
?????}
?}

上述例子比较简单,工作的时候为了保证异常安全,一般建议大家使用RAII的思想,退出作用域,资源随即释放。即使发生异常,现场会恢复到调用前的状态,资源也不会有任何泄漏。也就是所谓的异常安全了。

4

错误码还有一个致命的问题是,使用者可以不接收,不判断。一旦他选择忽略,程序拿着一个错误的结果继续往下走,鬼知道会出什么事情。就好像园区保安有点累,不检查健康码就放人进去,有可能会引起全上海封城3个月的严重后果。

作为对比,异常是不能忽略的,有人跳楼你不处理,那这个程序肯定就挂了。

C++的标准库就是用异常来作为错误处理方式的。如果你使用过std::vector和std::map这类常见容器,一定对out of range,bad alloc,map::at这类异常出错记忆深刻。

使用异常的理由千千万,这儿还有一条最重要的理由:

?class?Student{
?????Student()=default;
?????~Student()=default;
?}

构造函数和析构函数连返回值都没有,我怎么返回错误码?

5

C++标准库中定义了异常类,并且有继承派生关系。我们只能以默认初始化的方式取初始化bad_alloc,bad_cast这些对象,不允许为其提供初始值;而logic_error和runtime_error这些类型的对象,则必须提供字符串初始值来初始化。

这两种常用异常类型的区别是:

logic_error:理论上无需程序运行,读代码就能看出来的异常;

runtime_error:理论上只有程序运行起来才能检测出的异常。

开头北京同事如果这样抛异常,我这边就不会出错啦。

?if(pos?>?MAX){
?????throw?std::logic_error("out?of?range!");?
?}

6

当然了,异常处理也是有一些成本的。为了在运行时处理异常,程序内部要记录大量的信息,标记的对象需要跟踪,抛出和处理都需要编译器对代码进行相应的优化。

所以如果某些函数是绝对不会抛出异常的时候,我们可以在其后标记noexcept,来显式的告知编译器:这个人是个老实人,不会跳楼,不用对他做什么优化工作。

int?add(int,int)?noexpect;

一般而言,类内的移动构造函数、移动赋值运算符和 swap 函数都需要保证不抛异常并标为 noexcept。

不过这个noexcept只是一种承诺和保证,虽然对外承诺说我保证不抛异常,但并不意味着真的不抛异常。出异常了只能说这里发生了一些不符合预期的事情,此时系统会直接调用std::terminate中断程序的执行。

所以如果被标记为不会跳楼的老实人真的跳楼了,那他会死的很干脆。虽然有点出乎意料,但好像确实符合老实人的实际处境。

7

当然了,完全不用异常和完全不用错误码都是比较极端的做法,异常和错误码配合使用味道更佳。

在一些允许频繁出错的地方,比如网络波动引起的错误,硬件设备操作的故障等等,还是老老实实使用错误码比较好。毕竟检查健康码的成本,总比防护跳楼的成本要低一些。

说到跳楼,神探夏洛克 S2E3,被莫里亚蒂用自杀逼入死局的夏洛克,由于事先已经写好了异常处理机制,他自信的站在圣巴塞洛缪医院四层的楼顶,看了看左手边的圣保罗教堂,望着基友华生的侧脸,

纵身一跃。

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-06-29 18:48:02  更:2022-06-29 18:50:48 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 16:43:59-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码