做软件开发的朋友们,干的时间长了,相信都会有过类似的体验:一个问题查了很久很久,代码检查了很多遍很多遍,到最后都要怀疑电脑了,突然发现是一个特别隐蔽的错误导致的,而这种错误通常不涉及逻辑流程,却特别容易瞒天过海。比如可能是掺杂了一个中文字符,可能是少写了一个分号,可能是变量重名了,可能是类型强转了,可能是变量未初始化,可能是指针指错了等等等等。反正都是一些不引人注意的细节导致了问题的难缠。今天,博主就遇到了一个类似的问题。
在正式开始之前,简单介绍一下背景。其实这个问题并不复杂,之所以难缠,反倒是环境导致的。博主需要在一个双核CPU上分别实现不同的功能。因为各种原因,导致只能通过CPU0来访问调试。如果要调试CPU1上的程序,那么将相关量写入共享内存,然后通过CPU0读出,以此间接调测。
因为这种方式比较麻烦,所以要调试CPU1的时候,内心总是抗拒的。尤其是当在CPU1中已经增加不少的调试变量后,更加的抗拒新增调测变量。但是,上天总是公平的,一份付出一分收获,自认为没有问题而不愿意调测的代码,执行时往往隐含着神秘的错误。所以,让代码可测,确实是有道理的。
关于这方面,其实已经有很多好的经验了。比如,典型的测试驱动开发,就让代码的可测性可控性大大增强了。但是偷懒是人的本性。虽然说懒是促进人类进步的动力,但那是另一种“懒”,而非这里所说的真的懒。很多时候,我们都是懒得写测试代码,而是直达目的,而且是边想边写,边写边测,缺少设计、规划。为此,很多资深开发者,实力前辈通过各种渠道倡议了许多好用的软件工程方法,包括前面的测试驱动开发,包括敏捷开发,包括重构等等。但是(我们已经讲了很多但是了),具体到开发实践中,正真能够落实的又有多少呢?往往都是入坑了,才慨叹悔不当初啊。所以,经过此次入坑的经验教训,博主也是更加坚定了坚持写测试用例来验证代码逻辑的做法。俗话说磨刀不误砍柴工,这才是比解决问题本身更大的收获。
好了,现在我们具体看看是个什么问题。
我们先看看代码。将关键部分提取出来,整理如下:
#include "stdio.h"
static int state = 0;
int get_state() {
state = 2;
return state;
}
int main(int argc, char **argv)
{
int cur_state = 1;
if (get_state > cur_state) {
printf("state changed \n");
}
return 0;
}
不知大家看出啥了没有。我们编译看看。
编译提示如下:
可以看到,编译有一个警告。当然,这里可以清晰的看到该警告,但是在工程中,一旦有很多警告被忽略的话,那这种警告也就当做不存在了。
我们执行代码,看看效果:
If条件被执行了。在工程中,这种情况可能还不会触发问题。我们修改一些代码,再看看。
这次,我们把状态改成3,如果是函数的话,2大于3就不成立了,但是可以看到,if条件仍然被执行了。
其实这个问题的原因很简单,我们在编写代码过程中,出现了拼写错误,丢失了函数的括弧,编译器将函数名当做函数指针,用一个指针值参与运算了。自然,条件会始终执行。要想让if条件不执行,在不动函数拼写错误的情况下,就需要将变量值改为很大的值才行。我们也顺便验证一下:
可以看到,符合我们的预期。
这种错误,一般是代码编辑工具自动联想时导致的。如果在大工程中出现,还是很隐蔽的,如果存在于一个犄角旮旯的地方,那测出的概率就小,隐患就大。
好了,总结一下,有两点:一,俗话说的不听老人言,吃亏在眼前还是很有道理的。二,由怀疑程序到怀疑电脑推而广之,当你怀疑人生的时候,就该考虑换个活法了。