防御式编程
前注:希望我的读书笔记能带你迅速走过25页的书籍,有不妥之处,欢迎指正。http://www.cnblogs.com/jerry19880126/
1. 问题
这一章主要介绍如何编写出健壮性强的代码,简单地说,就是对各种可能的输入,程序都能够给出正确的处理结果。
举个例子,比如进行摄氏温度向热力学温度的转换,已知热力学温度=摄氏温度+273,程序的接口是:
int Celsius2Thermo(int Celsius)
糟糕的程序会直接是
int Celsius2 Thermo (int Celsius)
{
return Celsius + 273;
}
这种程序在大部分情况下是正确的,但万一输入的Celsius是-273呢?程序返回0从数值上看是没有问题的,但这个结果合理吗?热力学第三定律告诉我们,绝对零度不可达,所以这个结果是没有意义的。万一输入是INT_MAX(32位机上是2^31 – 1),结果又是什么呢?数值会上溢,得到的是一个绝对值很大的负数。还可以举出很多例子,比如有变量作为分母的情况,你有没有考虑这个变量可能为0呢?
千万不要说上面的情况在实际中不会出现,若这个子程序做成一个产品,你千万不要假设用户一定会做什么,一定不会做什么,事实上用户什么都会做,他们的行为会产生成千上万种的输入可能!
所以防御式编程主要考虑的就是程序对输入各种数据的稳健性,需要对各种可能的数据类型,以及可能的数据范围进行考虑。
2. 方法
(1) 普通错误处理方法
对付上面的问题,用普通的数据处理方法就足够了,只需要这样写:
int Celsius2 Thermo (int Celsius)
{
if(Celsius <= -273) {…}
else if(Celsius >= INT_MAX – 273) {…}
else
{ return Celsius + 273; }
}
就可以对付输入数据范围的问题了,在{…}里写上相应的警告就可以了,比如给用户提示“您输入的数据无效”等。
在更复杂的情况时,比如电视画面的一帧有一个像素的数据不对,该怎么办?有一些处理方法:一是丢弃这个帧,二是用默认值替换,三是换用上一次的数据,四是用最接近的合法值替换,五是记录到日志文件中。可以根据实际情况,来选择相应的处理方法。
但万一是一些程序本身不好处理的输入呢?比如一些恶意的漏洞攻击(使程序的内存用尽等)或者是在病理分析时,突然传来一个无效的数据(这时怎么处理这个无效数据,是采用默认值,还是不去管它?)高级编程语言提供两种处理这些严重错误的方法,简单一点是“断言”,复杂一点是“异常处理”。
(2) 断言
对于C++而言,在头文件中嵌入include <cassert>,然后调用assert(expression)就行了,其中,expression是逻辑表达式,当表达式值为真时,代码会继续执行,而在表达式为假时,程序就会报错,并强制终止(调用了abort())。
举个简单的例子:
int main()
{
int a = 10;
assert(a < 5);
cout << a << endl;
return 0;
}
这时运行会出现如下界面,可以看出expression的结果为假,所以出现了assertion failed的字样,同时还可以看到,系统自带的assert函数指出了是哪个表达式断言为假,也指出了出问题的源文件和行号,另外,弹框还说明了abort()被调用了,程序被强行终止。
注意系统自带的assert是只接收一个参数的(expression),不接收显示的错误处理信息(事实上,已经显示的很完善了),若用户还想显示自己独有的提示信息,则需要自定义一个assert,下面是我自己写的带双参数的assert子函数:
void myAssert(bool expression, const char* message)
{
if(!expression)
{
cout << message << endl;
exit(1);
}
}
这个函数可以显示出错的消息,但并不完善,因为好的断言函数还应该指明出错的源文件、行号,甚至是具体的哪一条表达式。
断言主要用于开发和测试环节,在产品时给用户看到大红叉就不好了,所以在产品发布的时候最好禁用断言,不要用// 或者 /* … */ 一行行地注释了,在include <cassert>之前写上
#define NDEBUG
就OK了,所有的断言都会失效,这里注意一定是在<cassert>头文件之前写#define。
《代码大全》上提倡先断言而后进行错误处理,我觉得这样不好,因为当断言表达式为假时,程序已经终止了,错误处理就不会执行了。《代码大全》可能是这样考虑的,开发的时候用断言,产品运行的时候定义NDEBUG,让所有断言失效,这样就可以执行错误处理程序了,但万一错误处理程序本身有问题呢?这样在开发和测试阶段就无法暴露问题了。
(3) 异常
C++中的异常是用try{…}catch{…}块来实现的,当try中的代码出问题时,会抛出异常由catch块处理,若本程序不好处理,则throw给其实程序处理。我自己写程序没怎么用过异常处理(因为没有接触过大项目的原因吧),所以这里也不好举例子了。就列出《代码大全》里做出的一些注意事项:
a) 不要在构造函数和析构函数中抛出异常,在构造函数中抛出异常将很可能无法进一步调用析构函数,造成资源的泄露;
b) 尽量局部处理,不要向外抛出;
c) 抛出抽象层次最好一致,比如抛出了EOFException的异常,其他别有用户的程序员就知道你这个模块有对文件的读操作了;
d) 避免偷懒的空catch块;
e) 不要滥用异常,这会使你的程序臃肿不堪的。
最后列出《代码大全》里本章的Key Points
- 一定不要“垃圾进,垃圾出”,一定要拴住垃圾的输入,不能让它扩散!用错误处理程序,用断言,用异常处理,拦截住垃圾输入,并给出好的处理方案;
- 处理不严重的错误时,直接用错误处理程序;处理严重错误时(可能需要终止程序)就用断言和异常,但千万不要滥用。<end>