編譯器功能只是語法檢查,只要語法正確,那它就遵循一個原則:程序員總是對的。其實也只能這樣,如果腦子裏想着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。所以這幾對單雙目運算符使用時要多注意。