【C++】浅谈C++中的错误处理【改】

(点击上方公众号,可快速关注)

完备的错误处理是健壮的程序必不可少的。在C++中,错误处理主要通过两种形式:

  • 函数返回值

    简单起见,C中的errno也归为这一类。

  • 异常处理机制,try…catch

函数返回值

函数通过返回值告诉调用者具体的错误,简单明了,在C/C++中被广泛使用。基本的使用方式不再赘述,仅讨论常见问题的解决方式。

嵌套深

如下面的伪代码:

if (func1() == Sucess) {
  ...
  if (func2() == Sucess) {
    ...
    if (func3() == Sucess) {
      ...
      if (func4() == Sucess) {
        ...
      }
      else {  ...  }
    }
    else { ...  }
  }
  else { ... }
}
else { ... }

这里故意减少了缩进,折叠了else分支。这里的代码嵌套深度为4,已经让代码可读性不高了:不容易看出if对应的else,如果层次一深,更不容易阅读,在实际业务代码中,我见到过嵌套11层的,阅读很费劲,得把代码一层一层折叠起来,才能看清逻辑脉络。

这种情况,常见的处理方式是将代码铺开处理,为了后面更好地引用这段代码,这里把代码块的一些调用做了命名:

if (func1() != Sucess) {
  p1()
  return
}
if (func2() != Sucess) {
  p2()
  return
}
if (func3() != Sucess) {
  p3()
  return
}
if (func4() != Sucess) {
  p4()
  return
}
...

这样能更好看出代码执行流程,更有利代码的理解。

代码冗余

如果上面p1p2p3等错误处理代码共性较大,我们可以增加一个变量保存函数的返回值,在执行每个函数时需要判断下,若成功再执行下一个流程,否则最终将执行错误处理的代码:

err = Sucess
if (err == Sucess) {
  err = func1()
}
if (err == Sucess) {
  err = func2()
}
if (err == Sucess) {
  err = func3()
}
if (err == Sucess) {
  err = func4()
}
...

common_process(err);

还有另外一种风格的写法:

err = Sucess;
do {
  err = func1()
  if (err != Sucess) {
    break;
  }
  err = func2()
  if (err != Sucess) {
    break;
  }
  err = func3()
  if (err != Sucess) {
    break;
  }
  err = func4()
  if (err != Sucess) {
    break;
  }
  ...
} while (false);

common_process(err);

这种写法的好处是遇到错误能及时地跳出来,能节省一些基本可以忽略的比较计算成本。

渐进式地资源申请/释放

对于资源申请要格外小心,一定要记得释放,否则很容易引起内存泄漏。当然在C++中,要合理得利用RAII避免内存泄漏,但在实际情况中不可避免地使用到C语言的API,如果每个API都要封装一个C++类,成本也会很高。所以,现实可行的方式是找到正确的使用方式。这里以打开文件为例,假如代码是这样的:

FILE *fp1, *fp2, *fp3, *fp4...;
fp1 = fopen(...)
if (fp1 == NULL) {
  return;
}

fp2 = fopen(...)
if (fp2 == NULL) {
  fclose(fp1);
  return;
}

fp3 = fopen(...)
if (fp3 == NULL) {
  fclose(fp1);
  fclose(fp2);
  return;
}

fp4 = fopen(...)
if (fp4 == NULL) {
  fclose(fp1);
  fclose(fp2);
  fclose(fp3);
  return;
}
...

fclose(fp1);
fclose(fp2);
fclose(fp3);
fclose(fp4);

上面代码非常不利于维护,在后面每加一个if分支都要考虑资源的释放,并且要释放的资源是逐渐递增的。这个时候就需要用到goto语句了:

FILE *fp1, *fp2, *fp3, *fp4...;
fp1 = fopen(...)
if (fp1 == NULL) {
  goto Err1;
}

fp2 = fopen(...)
if (fp2 == NULL) {
  goto Err2;
}

fp3 = fopen(...)
if (fp3 == NULL) {
  goto Err3;
}

fp4 = fopen(...)
if (fp4 == NULL) {
  goto Err4;
}
...

fclose(fp4);
Err4:
  fclose(fp3);
Err3:
  fclose(fp2);
Err2:
  fclose(fp1);
Err1:
  return;

可以看到资源的申请和释放都是渐进地,并且资源申请和释放的顺序相反。很多书籍和言传身教都不建议使用goto语句,你不能因为菜刀容易切到手就不否定菜刀吧,而这种用法很可能是唯一goto不可替代的用法了,毕竟C支持的语言特性本身就不多。

异常处理

前面返回值的例子可以看到,在实际的业务代码处理中间,总是穿插着错误处理,使得流程“中断”不清晰,而异常处理机制则解决了这个问题,在机制上是优于返回值的;但在实际开发中,很少使用异常处理,我主要考虑的点在于:

  • 并不是所有的异常都能捕捉

    不是所有的库都会抛异常,这使得你必须得用返回值处理错误;即使支持异常,一般也会提供非异常的API,这使得返回值的方式更通用,异常处理仅适用于很窄的范围。索性就不如从始至终都用返回值判断了。

  • 效率低

    异常处理会涉及到栈的回溯,相比返回值的方式效率有一定程度的降低。曾经看到一个测试数据,大概是降低20倍的样子,出处已忘。所以异常处理仅适用于概率极小的异常情况,否则会降低程序的吞吐量。曾经看到过一段逻辑:某个服务配置如果不存在就取默认值,这个不存在就是通过抛出异常告知调用方的,改成返回值判断后,CPU下降是比较明显的。

总结

两者之间,内心倾向异常处理机制,但实际用返回值比较多。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值