15.5 print 除錯法

15.1 說「模擬自己的程式」,測資小可以手動跑;但步驟一多,手動模擬又慢又容易抄錯。最實在的辦法是讓程式自己報告:在關鍵步驟印出中間變數,對照你的預期,找出第一個分岔點。這招叫 print 除錯法——不用學任何新工具,人人第一天就會,資深選手也天天在用。

用 cerr,不要用 cout

印除錯資訊不要cout,用 cerr

cerr << "i = " << i << ", sum = " << sum << '\n';

cerr 把東西送到標準錯誤(stderr)——它是與標準輸出(stdout)分開的另一條輸出通道,專門給錯誤與診斷訊息走。好處立刻見效:

  • 不會混進答案:OJ 只把 stdout 當你的答案比對,cerr 印的東西 OJ 不看——就算忘了刪,也不會因為多印東西而 WA(但印太多還是會拖慢程式,交之前仍應清掉)。
  • 重導向時不搗亂14.7./a.out > out.txt 只把 stdout 寫進檔案,cerr 照樣顯示在螢幕上——答案進檔案、除錯訊息給你看,各走各的。

實際辦一個案子

題意:讀入 n 個成績,輸出平均(答案與正確值誤差在 10^{-4} 內都算對,9.7 的講法)。有人寫出:

#include <iostream>
using namespace std;

int main() {
    int n;
    cin >> n;
    int sum = 0;
    for (int i = 0; i < n; i++) {
        int x;
        cin >> x;
        sum += x;
    }
    double avg = sum / n;       // 兇手藏在這行
    cout << avg << '\n';
    return 0;
}

執行結果(輸入 380 85 91):

85

手算是 256 \div 3 = 85.33\ldots,程式印 85——找到會錯的測資了(15.1 步驟 3 完成)。接著模擬:在每個關鍵步驟裝上探針,印出「這一步算完的所有狀態」:

#include <iostream>
using namespace std;

int main() {
    int n;
    cin >> n;
    int sum = 0;
    for (int i = 0; i < n; i++) {
        int x;
        cin >> x;
        sum += x;
        cerr << "i = " << i << ", x = " << x << ", sum = " << sum << '\n';
    }
    double avg = sum / n;
    cerr << "avg = " << avg << '\n';
    cout << avg << '\n';
    return 0;
}

執行結果(輸入同上,終端機上兩條通道都會顯示):

i = 0, x = 80, sum = 80
i = 1, x = 85, sum = 165
i = 2, x = 91, sum = 256
avg = 85

(最後還有一行 stdout 的 85。)現在逐行對照預期:三圈的 sum80165256——全對,讀入和累加清白;avg 卻是 85 而不是預期的 85.33\ldots——第一個分岔點就在 sum / n 那行。範圍縮到一行,答案呼之欲出:int / int 是整數除法(15.3 清單第 5 項),小數在存進 double 之前就被丟了。修法是上冊 3.7 的轉型:double avg = (double)sum / n;(修正版印出 85.3333)。

print 除錯的兩條守則,這個案例都示範了:

  1. 印之前先寫下預期值。「i = 1 時 sum 應該是 165」——有預期才叫對照,沒預期只是印心安的。
  2. 每個值都帶標籤cerr << sum 印出一個孤零零的 256,過三行你就忘了它是誰;sum = 256 才是給人看的。

查完要清乾淨

除錯輸出是鷹架,房子蓋好要拆。兩種拆法:

  • 手動刪:提交前把 cerr 行全部刪掉或註解——順手 Ctrl+F 搜一次 cerr,確保沒有漏網之魚。
  • 讓前置處理器代勞:用 12.9 提過的 #ifdef 把除錯輸出包起來,只在本機出聲:
#ifdef LOCAL
    cerr << "sum = " << sum << '\n';
#endif

本機編譯時加參數 -DLOCAL(等於在程式開頭補一行 #define LOCAL),這段就存在;OJ 沒有這個定義,這段在編譯前就整段消失——同一份程式碼,本機話癆、OJ 沉默,永遠不會忘記刪。本站實測:加 -DLOCAL 編譯印出 sum = 256 加答案,不加參數只印答案。

動手試試看:回到 15.1 的「最大值減最小值」錯誤程式,這次不用手動模擬——在迴圈裡放一行 cerr 印出每圈的 xmxmn,餵進全正數測資,親眼看著 mn 一動也不動。