编译器功能只是语法检查,只要语法正确,那它就遵循一个原则:程序员总是对的。其实也只能这样,如果脑子里想着A,实现的却是B,而A/B语法上都成立,那编译器除了认为你正确,还能做什么呢?只能我们自己注意区分A/B相似且语法都成立的下列情况。
代码布局与缩进的误导
计算机从不受代码语法和布局影响,而人却易受眼睛影响做出倾向性判断,这些判断有时是错误的。如:
for (i=0; i<max_time; i++)
LeftElmt = Left[i];
Left[i] = Right[i];
Right[i] = LeftElmt;
这种排列方式容易使人认为每次循环3条语句全部执行,实际上没有括号,只有第1条语句执行max_time次,而后两条语句仅执行一次。这里for循环与缩进放在一起,对外传递了误导信号,使人认为三条语句都被循环,而实际编译器却另有解释,结果出乎意料。所以类似情况不要省略{}。
语句结束分号的匹配
C程序中分号是语句结束标志,使用频率最高。常在河边走,哪能不湿鞋,一时手误多写或漏写;号,有时会迷惑编译器,导致Bug。比如:
if (a > b); //多加了一个;
b = a;
这段新代码仍然通顺,编译器检查不出语法错误,因为它相当于:
if (a > b) { }
b = a;
但这样的笔误会导致结果和期望的大不相同,对外表现就是BUG。;号多了不行,少了同样有麻烦,如:
if (a < 6)
return //少了;
b = a;
这里return后漏了结束分号,程序能通过编译,只是把b = a;当作return操作数,变成:
if (a<6)
return b = a;
这样当a >=6时,预想的b=a;会被跳过,因为它和return一起成为a<6的条件执行部分。
还要注意结构体/枚举/类等结尾的;符,if/for/while/switch等模块体紧跟的{}后面都不需要;号,而struct/enum/class定义后必须有,如果遗漏就会导致语意错误,如:
struct pic{
……
}
test(){….}
struct结尾与其后的函数test定义之间,遗漏了;号,变成声明函数test,返回pic结构类型变量。如果;没有遗漏,test缺省返回int型。
switch/case中的break
switch/case结构中,case后的模块中是否包含break,程序语法都正确,只是逻辑截然不同。如:
switch(color)
{
case 1: printf ("red");
case 2: printf ("yellow");
case 3: printf ("blue");
}
假设color值是2,输出结果是yellowblue,而不是有人预想的yellow,因为一旦某case分支缺少break,流程执行完当前case后,自然转向下一case,直到break出现才跳出。这似乎让人费解,明明case 3并不成立,怎么它的条件分支也会执行?问题在于人们潜意识里总把case等同于if,但编译器并不这么认为。不要把switch/case简单和if/else画等号,C不会提供两个功能完全互换的功能。
switch/case这种特殊流程是双刃剑。一方面,某些情况下故意去掉break,能方便实现if/else等其他语句很难实现的特殊控制流程。另一方面,写代码时不小心遗漏break语句,编译器不能识别并提醒,只会认为程序员有意这么做,包不包括break;都成立时,这种“程序员总正确”的推定就产生bug。
形似的单双目运算符
C语言为编译器以及编程的方便,最大程度“复用”一些字符,为使用概率高的操作分配较短的符号,以提高整体效率。&和&&,|和||以及=与“==”,都是这种情况,比如用=表示赋值而用==表示比较,因为赋值操作出现概率高于比较,所以为其分配更短的符号。
尽管这种方法使C更加简洁,但由于符号极度相似,稍不注意,就会因笔误出现混淆。估计所有C程序员都犯过类似下面的错误:
if(i=0) //原意应为if(i == 0)
这种下意识的随手错误很难避免,毕竟一个“=”号人们熟悉了几十年,而“==”号相对陌生。=和==方便了编译器,却给程序员们设了一条绊马索。甚至有些错误,除了作者本人调式,其他任何程序员也不可能检查出来,更别说编译器了。如:
if(a&b) //原意if(a && b)。
&和&&哪个都通,编译器不报错,其他人也不可能发现问题。但如果原来预想实现是a&&b,现在的实现就是bug。所以这几对单双目运算符使用时要多注意。