(点击上方公众号,可快速关注)
完备的错误处理是健壮的程序必不可少的。在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
}
...
这样能更好看出代码执行流程,更有利代码的理解。
代码冗余
如果上面p1
、p2
、p3
等错误处理代码共性较大,我们可以增加一个变量保存函数的返回值,在执行每个函数时需要判断下,若成功再执行下一个流程,否则最终将执行错误处理的代码:
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 (true);
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下降是比较明显的。
总结
两者之间,内心倾向异常处理机制,但实际用返回值比较多。