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

纵身一跃。

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值