章節:程式測試與除錯
哈囉!歡迎來到程式設計中最重要、最實用的課題之一:測試與除錯。想像一下,你就是個偵探。你已經寫好一個程式(「案件」),但它運作完美嗎?有沒有任何隱藏的問題(「線索」)呢?在這個章節裡,你將學會如何成為一位出色的程式碼偵探!我們會探索如何找出並修正程式中的錯誤(通常稱為「臭蟲」或「Bug」)。這項技能超級重要,因為幾乎沒有程式能一次過完美運作。讓我們一起學習如何讓我們的程式更可靠、更準確吧!
三大麻煩惡魔:程式錯誤的類型
在程式設計的世界裡,錯誤稱為程式錯誤(bug)。你的任務就是找出這些錯誤並修正它們。程式錯誤通常分為三大類。了解它們是擊敗它們的第一步!
1. 語法錯誤
這是最常見且最容易修正的錯誤類型。語法錯誤就像你寫英文時犯了文法錯誤一樣。程式語言有嚴格的文法規則,如果你違反了它們,電腦就會感到困惑,無法理解你的指令。
比喻:想像一下,你寫下 "Cat the on mat sat the." 在英文中。它有所有正確的單字,但文法錯誤,所以它沒有意義。語法錯誤對電腦來說也是一樣的道理。
- 常見原因:指令串錯字(例如輸入 'prnt' 而不是 'print')、缺少括號 `()`、忘記逗號 `,` 或縮排錯誤。
- 如何找出:別擔心,電腦會替你找出這些錯誤的!當你嘗試運行程式時,編譯器或直譯器會停止並給你一個錯誤訊息,通常還會告訴你錯誤發生的確切行數。
2. 執行時錯誤
執行時錯誤有點棘手。它發生在你的程式正在執行時(在「執行時」)。文法(語法)完全正確,但你卻要求電腦執行一些不可能的任務。程式開始運行但執行到一半時卻崩潰了。
比喻:你給別人一個語法上完全正確的指令:「將這塊披薩分給零個人。」指令本身很清楚,但這個動作邏輯上無法執行。
- 常見原因:數字除以零、嘗試開啟不存在的檔案,或嘗試存取列表中不存在的項目(例如,要求一個只有5個項目的列表中的第10個項目)。
- 如何找出:程式會崩潰,通常會顯示一個錯誤訊息,解釋哪裡出了問題,例如 "DivisionByZeroError"。徹底的測試能幫助你在使用者發現這些問題之前,先將它們找出來!
3. 邏輯錯誤
這些是最狡猾、最難找出的一種錯誤!邏輯錯誤的情況是,你的程式語法正確,並且運行時不會崩潰。然而,它卻產生了錯誤的結果。電腦精確地按照你告訴它的去做,只是你告訴它的指令本身是錯的。
比喻:你完美地遵循食譜指示,但食譜卻錯誤地告訴你加入1杯鹽而不是1杯糖。你只有在品嚐最後的蛋糕時,發現它難吃到極點,才會知道出了問題!
- 常見原因:使用了錯誤的數學運算符號(例如 `>` 而不是 `<`)、公式錯誤(例如計算面積而不是周長),或程式流程中的錯誤。
- 如何找出:這些錯誤很難發現,因為電腦不會給你任何錯誤訊息。你必須透過精心挑選的數據,仔細測試你的程式,看看輸出是否符合你的預期。如果不是,你就需要用到你的偵探工具了,我們稍後會介紹!
快速瀏覽特殊數值運算錯誤
有時候,我們會遇到處理數字時特有的錯誤:
溢出錯誤(Overflow Error):當數字太大,無法儲存在變數中時,就會發生此錯誤。想像一下,你想把兩公升汽水倒進一個小咖啡杯裡!
下溢錯誤(Underflow Error):這正好相反。當數字太小(太接近零),電腦無法準確儲存時,就會發生此錯誤。
進位錯誤與截斷錯誤:這些是處理小數時發生的微小不準確性。進位錯誤是由於對數字進行四捨五入而產生,而截斷錯誤則是指數字的尾數被直接捨棄。
重點摘要:三大錯誤類型
語法錯誤:文法錯誤。程式根本無法運行。
執行時錯誤:指令不可能執行。程式執行時崩潰。
邏輯錯誤:思考上的缺陷。程式運行但給出錯誤答案。
成為偵探:程式測試的藝術
如果你不知道臭蟲的存在,就無法修正它們!測試是指運行你的程式,並輸入特定資料,以檢查其行為是否符合預期,並找出任何錯誤的過程。目標是要對你的程式夠狠,試圖找出它的弱點並使其「崩潰」!
設計良好的測試數據
測試中至關重要的一環是選擇正確的數據進行測試。你應該檢查程式本身的數據驗證(它檢查輸入數據是否合理的能力)是否正常運作。我們可以將測試數據分為三類。
讓我們用一個例子:一個程式檢查某人年齡(有效範圍:18至65歲),判斷他們是否符合標準工作職位的資格。
1. 正常數據
這是你預期程式能正確處理的合理、日常的數據。
範例:對於我們的年齡檢查程式,正常數據會是25、40和60歲等。程式應該顯示「符合資格」。
2. 極端(邊界)數據
這是最重要的測試數據類型之一!邊界案例是允許範圍的極端值。臭蟲經常藏在這些邊界值中。
範例:對於18-65歲的範圍,邊界值是18和65。你也應該測試剛好在邊界值之外的數字,例如17和66。
- 輸入 `18` -> 預期輸出:「符合資格」
- 輸入 `65` -> 預期輸出:「符合資格」
- 輸入 `17` -> 預期輸出:「不符合資格」
- 輸入 `66` -> 預期輸出:「不符合資格」
3. 錯誤(無效)數據
這是程式應該拒絕的數據。你正在測試你的程式是否能優雅地處理錯誤輸入而不會崩潰。
範例:對於我們的年齡檢查程式,錯誤數據會是像-5、200,甚至是文字如「Hello」。程式應該回應一個清晰的錯誤訊息,例如「年齡無效」,而且不會崩潰。
重點摘要:有目的的測試
要正確地進行測試,請混合使用不同數據:
正常數據:預期、簡單的輸入。
邊界數據:邊界案例(例如,最小值/最大值)。這是臭蟲最喜歡藏匿的地方!
錯誤數據:你的程式應該拒絕的錯誤輸入。
除錯工具箱:找出並修正臭蟲
好啦!你的測試已經揭示了一個臭蟲。現在是時候進行除錯了——這個過程旨在找出臭蟲的確切原因並加以修正。如果一開始覺得棘手也別擔心;這是一項你可以透過練習就能掌握的技能!
手動除錯:乾跑(Dry Run)
乾跑(Dry Run)是指你假裝自己是電腦。你逐行閱讀你的程式碼,並在追蹤表中追蹤所有變數的值。這是找出邏輯錯誤的極其強大的方法。
範例:讓我們追蹤一個旨在計算 `3 + 2 + 1` 的小程式。
程式碼:
Line 1: total = 0
Line 2: number = 3
Line 3: WHILE number > 1
Line 4: total = total + number
Line 5: number = number - 1
追蹤表:
行數 | number | total | 條件 (number > 1) | 備註 |
---|---|---|---|---|
1 | ? | 0 | - | `total` 已初始化。 |
2 | 3 | 0 | - | `number` 已初始化。 |
3 | 3 | 0 | True | 迴圈開始。 |
4 | 3 | 0 + 3 = 3 | - | `total` 已更新。 |
5 | 3 - 1 = 2 | 3 | - | `number` 已更新。 |
3 | 2 | 3 | True | 迴圈繼續。 |
4 | 2 | 3 + 2 = 5 | - | `total` 已更新。 |
5 | 2 - 1 = 1 | 5 | - | `number` 已更新。 |
3 | 1 | 5 | False | 迴圈結束。最終的 `total` 是 5,而不是 6!我們發現了一個邏輯錯誤!條件應該是 `number > 0`。 |
軟件除錯工具
現代程式設計環境配備了強大的工具來幫助你除錯:
斷點(Breakpoints):這就像你程式碼的「暫停」按鈕。你可以在特定行設置一個斷點。當程式執行到該行時,它會暫停,讓你可以在那個確切的時刻檢查所有變數的值。這對於找出問題開始出錯的地方非常有用。
程式追蹤(或監看):這個功能讓你「監看」一個變數。當程式運行時,除錯器會實時顯示該變數的值,或列出對它進行的每一次更改。這就像一個實時的追蹤表!
標記(Flags):這是一個你可以自己做的簡單但有效的小技巧。標記就是你印到螢幕上的一條訊息,用來檢查你的程式是否到達了某個特定點。例如,你可以添加一行 `print("我現在在迴圈裡了!")` 來確認你的迴圈確實正在運行。
程式碼枝節(Stubs):在編寫大型、模組化的程式時,你可能需要測試某一部分,而該部分又依賴於你尚未編寫的另一部分。程式碼枝節(stub)是一個佔位符函數,它回傳一個簡單、可預測的值。例如,如果你需要一個 `calculateAverage()` 函數但尚未編寫,你可以創建一個程式碼枝節,讓它只 `return 10`,這樣你就可以測試你的其他程式碼了。
重點摘要:除錯工具箱
追蹤表:手動追蹤變數,以了解程式碼的流程。
斷點:在特定行暫停你的程式碼,檢查所有內容。
監看:查看變數的值如何隨程式運行而改變。
不只一種編寫程式的方法:比較解決方案
通常,編寫程式來解決同一個問題有很多不同的方法。一個好的程式設計師會思考哪種解決方案是「最佳」的。但究竟是什麼讓一個解決方案比另一個更好呢?
我們通常根據兩大主要方面來比較解決方案:
1. 運作步驟(效率)
這指的是電腦需要執行多少步驟或操作才能完成任務。較少的步驟通常意味著更快、更有效率的程式。
比喻:想像一下,在字典中尋找一個名字。
- 解決方案A(效率低):從第一頁開始,逐字逐句地閱讀每一個名字,直到找到你想要的那個。這可能需要很長的時間!
- 解決方案B(效率高):將字典翻到中間。如果你的名字在前面,你搜尋前半部分。如果它在後面,你搜尋後半部分。你不斷重複這個過程。它快得多!
2. 資源使用(記憶體)
這指的是你的程式運行時需要多少電腦記憶體(RAM)。高效的解決方案會盡可能少地使用記憶體。
比喻:想像一下,你給某人前往你家的路線指示。
- 解決方案A(高資源使用):給他們一張巨大、詳細的城市地圖印刷本。它包含所有資訊,但笨重且使用大量紙張。
- 解決方案B(低資源使用):給他們簡單、逐向的指示。它輕巧,只包含必要的資訊。
範例:從1到N的數字總和
假設我們想找出從1到100所有數字的總和。
解決方案A(迴圈法):
total = 0
FOR i = 1 TO 100
total = total + i
NEXT i
這個解決方案容易理解,但它大約需要100個步驟(100次加法)。
解決方案B(數學公式法):
$$Total = N \times (N + 1) / 2$$
total = 100 * (100 + 1) / 2
這個解決方案使用了一個巧妙的公式。它只需要一次計算(一次乘法、一次加法和一次除法)。在步驟方面,它的效率高得多!
重點摘要:更佳的程式碼
一個「更好」的程式通常是更有效率的程式。比較解決方案時,問問自己:
1. 它有多快?(更少的運作步驟)
2. 它使用了多少記憶體?(更低的資源使用)