码农修行:编写优雅代码的32条法则
上QQ阅读APP看书,第一时间看更新

法则06:增强健壮性

本节所列举的问题都属于“低级”错误,越是低级的错误就越不应该犯。它并不是多么高深的东西,只要多加小心就能避免。

数组下标保护

数组下标越界是一个常见的错误。比如下面的代码。

该数组buf下标的合法范围是[0, 99],如果pos的值不在这个合法范围内,执行最后一行赋值语句后会发生不可预知的错误。对于C/C++而言,有可能不会立即出现异常,而是把“其他的”内存破坏了。如果buf定义为一个局部变量,则堆栈可能被破坏,程序返回时会出现异常。如果buf定义为一个全局变量,则会“踩”别人的内存,当这块被“踩”的内存被使用时才会出现问题,而且这类问题往往很难定位。对于Java而言情况会好一点,在执行越界访问的语句时程序就会抛出异常。

不管怎样,养成良好的数据保护意识非常重要。对于数组访问,在访问前必须进行数组下标的合法性检查,代码如下所示。

拒绝不安全的API

这类问题在C/C++中较为常见,比如strcpy、strcat、sprintf这些函数就是不安全的。

比如上面代码,当src中含有的字符个数大于或等于BUF_LEN时,就会导致内存被破坏,出现不可预知的错误。因此在内存操作时必须确保安全。

strncpy在复制时会进行目标地址的长度限制,即使src中的字符个数过多,最多也只会复制sizeof(buf)-1个字符到buf中。同时需要注意,第3个参数不能设为整个空间的大小sizeof(buf),要留1个字节放字符串末尾的'\0'。

也许这些API就不该被“发明”出来,它们都有一个共性,就是在内存操作时,对目标地址的空间没有做安全性保护。在编程时应该避免,甚至应该写到编程规范中加以杜绝。

慎用递归算法

递归算法写起来很简单,但想用好却不容易。最主要的原因是递归的深度不好控制,容易出现堆栈溢出和死循环的问题。因此有的项目在编程规范中明确说明禁用递归算法,要求一律改为非递归算法。这样的要求有点矫枉过正。在使用递归算法时,留意以下几个关键点,可以避免很多问题。

首先把退出条件放在函数最上方,这样比较清晰,防止程序一直不满足退出条件而导致堆栈溢出。

其次,也是最重要的一点,要避免在递归函数中出现过大的局部变量,这会加速堆栈空间的消耗。

出现这种情况时需要优化算法加以避免,实在不能避免则采用动态申请内存的方式替代。

此外,还要留意一些隐式的递归调用。这个案例源于我开发的一个Windows程序。其中有个消息处理函数,主要代码如下所示。

OnMyMessage为WM_MY_MESSAGE消息的处理函数,这里省略了其他代码,只保留了两个关键的地方。一个是其内部定义了一个局部变量buf,大小为2048;另一个是调用SendMessage发送另一个消息。测试中发现堆栈溢出的现象,而崩溃的地方就在OnMyMessage里。这个现象比较奇怪,因为OnMyMessage并没有被其他地方直接调用,只有这一个消息响应的入口,发送消息的地方也都是在其他线程里通过SendMessage发送。而且SendMessage是同步消息,要等到该消息处理完成后才会返回,因此不应该出现函数重入的情况。

通过观察堆栈发现,OnMyMessage确实重入了多次,而其中的局部变量又比较大,从而导致了堆栈溢出。那么问题来了,既然OnMyMessage没有递归调用,为什么会像递归调用一样被重入了多次?原因就出在OnMyMessage里调用了SendMessage(WM_ANONYTHER_MSG)。

实际上调用SendMessage之后,在主线程阻塞等待WM_ANONYTHER_MSG响应时,还是可以再继续处理消息队列上的其他消息。但如果此时消息队列上有大量的WM_MY_MESSAGE,而WM_ANONYTHER_MSG的响应又比较慢时,那么主线程就会不断地处理WM_MY_MESSAGE,从而重复地调用OnMyMessage,造成了一种类似递归调用的现象,最终导致堆栈溢出。

此类问题的修改方法也比较简单,只需将下面OnMyMessage里的局部变量改为动态申请即可,代码如下。