3. 程序的調試

編程是一件複雜的工作,因為是人做的事情,所以難免經常出錯。據說有這樣一個典故:早期的計算機體積都很大,有一次一台計算機不能正常工作,工程師們找了半天原因最後發現是一隻臭蟲鑽進計算機中造成的。從此以後,程序中的錯誤被叫做臭蟲(Bug),而找到這些Bug並加以糾正的過程就叫做調試(Debug)。有時候調試是一件非常複雜的工作,要求程序員概念明確、邏輯清晰、性格沉穩,還需要一點運氣。調試的技能我們在後續的學習中慢慢培養,但首先我們要區分清楚程序中的Bug分為哪幾類。

編譯時錯誤

編譯器只能翻譯語法正確的程序,否則將導致編譯失敗,無法生成執行檔。對於自然語言來說,一點語法錯誤不是很嚴重的問題,因為我們仍然可以讀懂句子。而編譯器就沒那麼寬容了,只要有哪怕一個很小的語法錯誤,編譯器就會輸出一條錯誤提示信息然後罷工,你就得不到你想要的結果。雖然大部分情況下編譯器給出的錯誤提示信息就是你出錯的代碼行,但也有個別時候編譯器給出的錯誤提示信息幫助不大,甚至會誤導你。在開始學習編程的前幾個星期,你可能會花大量的時間來糾正語法錯誤。等到有了一些經驗之後,還是會犯這樣的錯誤,不過會少得多,而且你能更快地發現錯誤原因。等到經驗更豐富之後你就會覺得,語法錯誤是最簡單最低級的錯誤,編譯器的錯誤提示也就那麼幾種,即使錯誤提示是有誤導的也能夠立刻找出真正的錯誤原因是什麼。相比下面兩種錯誤,語法錯誤解決起來要容易得多。

運行時錯誤

編譯器檢查不出這類錯誤,仍然可以生成執行檔,但在運行時會出錯而導致程序崩潰。對於我們接下來的幾章將編寫的簡單程序來說,運行時錯誤很少見,到了後面的章節你會遇到越來越多的運行時錯誤。讀者在以後的學習中要時刻注意區分編譯時和運行時(Run-time)這兩個概念,不僅在調試時需要區分這兩個概念,在學習C語言的很多語法時都需要區分這兩個概念,有些事情在編譯時做,有些事情則在運行時做。

邏輯錯誤和語義錯誤

第三類錯誤是邏輯錯誤和語義錯誤。如果程序裡有邏輯錯誤,編譯和運行都會很順利,看上去也不產生任何錯誤信息,但是程序沒有干它該干的事情,而是幹了別的事情。當然不管怎麼樣,計算機只會按你寫的程序去做,問題在於你寫的程序不是你真正想要的,這意味着程序的意思(即語義)是錯的。找到邏輯錯誤在哪需要十分清醒的頭腦,要通過觀察程序的輸出回過頭來判斷它到底在做什麼。

通過本書你將掌握的最重要的技巧之一就是調試。調試的過程可能會讓你感到一些沮喪,但調試也是編程中最需要動腦的、最有挑戰和樂趣的部分。從某種角度看調試就像偵探工作,根據掌握的線索來推斷是什麼原因和過程導致了你所看到的結果。調試也像是一門實驗科學,每次想到哪裡可能有錯,就修改程序然後再試一次。如果假設是對的,就能得到預期的正確結果,就可以接着調試下一個Bug,一步一步逼近正確的程序;如果假設錯誤,只好另外再找思路再做假設。“當你把不可能的全部剔除,剩下的——即使看起來再怎麼不可能——就一定是事實。”(即使你沒看過福爾摩斯也該看過柯南吧)。

也有一種觀點認為,編程和調試是一回事,編程的過程就是逐步調試直到獲得期望的結果為止。你應該總是從一個能正確運行的小規模程序開始,每做一步小的改動就立刻進行調試,這樣的好處是總有一個正確的程序做參考:如果正確就繼續編程,如果不正確,那麼一定是剛纔的小改動出了問題。例如,Linux操作系統包含了成千上萬行代碼,但它也不是一開始就規劃好了內存管理、設備管理、檔案系統、網絡等等大的模組,一開始它僅僅是Linus Torvalds用來琢磨Intel 80386晶片而寫的小程序。據Larry Greenfield 說,“Linus的早期工程之一是編寫一個交替打印AAAA和BBBB的程序,這玩意兒後來進化成了Linux。”(引自The Linux User's Guide Beta1版)在後面的章節中會給出更多關於調試和編程實踐的建議。