debug 是讓人非常頭痛的事情,在想著怎麽 debug 之前,我們應該先試著想盡辦法讓 bug 不要出現,畢竟只要沒有 bug,就沒有 debug 的問題了。所以在談 debug 之前,我們先來學習一些寫 code 的技巧來減少 bug 出現的機會:
清楚的命名可以讓你的程式增加可讀性,也可以減少變數被誤用的機會,從而降低出 bug 的機會。以下程式碼為例,我們需要讀入一個人的年齡、性別、身高、體重等資訊。如果我們使用上面那種變數命名法,那在輸入時便一目了然,不容易出錯。不過,如果我們選擇下面那種比較懶惰的命名法,那在輸入時就會比較難取用變數,不小心打錯的話也會很難看出來。
#include<iostream>
using namespace std;
int main() {
//do's...
int age, height, weight;
char gender;
cin >> age >> gender >> height >> weight;
//dont's...
int a, b, c;
char d;
cin >> a >> d >> b >> c;
}
如果程式碼中存在同一段 code 需要出現在很多地方,不妨把他寫成函式,這樣能為你的程式碼帶來許多好處:
比起一份完整的程式碼,一小段 code 肯定更好 debug。所以當我們在寫一份很長的程式碼時,盡量每完成一段 code,就針對這段 code,進行一次檢查。這樣有助於我們在不小心寫出 bug 以後盡早發現他,而不是在完成了一整份長到不行的程式碼後,才來找一個一開始就寫錯的東西。
人難免會犯錯,即使我們寫 code 的時候再謹慎,還是難以避免 bug 的出現,因此,debug 的流程是難以避免的。debug 簡單來說,就是在確認自己的想法邏輯沒有錯誤後,逐一檢查程式碼中的哪一個部份並沒有照預期的方式運作。檢查的方式除了閱讀程式碼,用人腦來執行程式以外,也可善用一些技巧,獲得程式執行時的各種資訊來助自己 debug。以下是一些在程式競賽中常被使用的 debug 技巧:
輸出大法指的是把可能出錯的變數,在各種可能出錯的地方輸出出來,從而檢查程式的運算可能是哪裡出問題。不過,這樣子在程式中加上額外輸出的話,我們就得在上傳之前,把所有額外輸出註解掉,要是上傳完還得繼續 debug 的話,我們又得花一堆時間取消註解,相當麻煩。想解決這個問題的話,可以試著搭配 #define
以及 #ifdef
,具體使用方式如下:
#include<iostream>
using namespace std;
#define debug
int main() {
int a, b;
cin >> a >> b;
#ifdef debug
cerr << a << " " << b << endl;
#endif
cout << a + b << endl;
}
我們用 #ifdef
和 #endif
把我們用來輸出 debug 訊息的程式碼包起來。這邊我們使用 cerr
標準錯誤來輸出錯誤訊息,與 cout
標準輸出不同的地方是,cout
在輸出時不會立刻輸出出去,而 cerr
則會直接在程式執行到時直接將內容輸出,方便我們即時看到這些 debug 訊息。接著,我們只需要在程式碼前面 #define debug
或是在編譯指令中加上 -Ddebug
便可以讓編譯器在編譯程式碼的時候將輸出 debug 訊息的指令也進行編譯。不過使用 #define debug
的話,要記得在上傳之前將這行刪掉,不然 judge 的編譯器就會把你用來輸出 debug 訊息的指令拿去編譯了。
要注意 cerr
預設會跟 cout
繫結 (tie) 在一起,如果不想被 cout
的 flush
影響的話,可以用 cerr.tie(0)
解除繫結。此外,cerr
的輸出不會被 judge 讀到,因此忘記刪除的話並不會造成 WA,但是輸出需要的時間可能會造成 TLE,因此上傳程式碼前最好還是要把 cerr
拿掉。
如果會嫌打一堆 #ifdef
和 #endif
太麻煩,我們可以將 debug 時輸出訊息的程式碼寫成一個函式,再用 #ifdef
和 #endif
把這些程式碼包起來。
#ifdef Debug
#define debug(x) cerr << x << '\n'
#else
#define debug(x)
#endif
如果稍微熟悉語法的話,甚至可以自己設計 debug code 來讓它顯示更多資訊,以下是一份能夠顯示所在行數以及輸出的變數名稱的 debug code:
#ifdef LOCAL
#define debug(arg...) cerr << "#" << __LINE__ << " :[" << #arg << "]", __print(args)
template<class T> void __print(T && x) { cerr << x << endl; }
template<class T, class ...S> void __print(T && x, S&&...y) {cerr << x << ", ", __print(y...);}
#else
#define debug(...) ((void)0)
#endif
assert
的用途是用來確保程式執行的某個階段時,變數資料有滿足某個條件,用法是傳入一個 bool 變數,並在該變數為 false 時終止程式並造成 RE。通常適用於幫助我們檢查某些變數的值是否處於它應該處於的範圍,我們可以用 assert 在程式的特定階段檢查這個變數,並在其所儲存的值不合理時直接中止程式並回報錯誤。
#include<bits/stdc++.h>
using namespace std;
int main() {
int a, b;
cin >> a >> b;
assert(a > 0 && b > 0); //如果 a 或 b 不滿足 > 0 的話,程式會回傳錯誤 Runtime Erroe
cout << a + b << endl;
}
assert
這個函式是被放在 cassert
這個標頭檔中,如果沒有 #include<bits/stdc++.h>
的話要特別注意!
暴力對拍指的是我們試著隨機生成測資,然後寫一個暴力解,接著檢查暴力解和自己寫的解結果是不是一樣的。透過這個方法,我們可以在不確定程式為什麽出錯的情況下找到一組會讓程式出錯的測資,從而協助我們 debug。
至於要如何隨機生成測資呢?這邊介紹一個好用的 random 工具 mt19937
。mt19937
可以被看成一種資料型別,我們可以用他來生成變數,那這個變數就會是一個隨機變數,我們在宣告隨機變數時會給他一個 seed 作為參數,並在每次生成不一樣的測資時都使用不一樣的 seed,這樣才能確保每次都生成出不一樣的隨機測資。完成宣告後,我們就可以用這個隨機變數生成 $[0, 2^{32} - 1]$ 的值!
#include<bits/stdc++.h>
using namespace std;
int main() {
mt19937 rng(time(NULL));
cout << (rng() % 10) << endl; // 隨機生成介於 0 ~ 9 之間的整數
}
同樣的,mt19937 也被特別放在 random
這個標頭檔之中,沒有 #include<bits/stdc++.h>
的話要特別注意。
當我們完成一份能夠隨機生成測資的程式碼後,我們就可以寫一份具有正確性的暴力解,然後比對兩個程式的輸出是否一致。如果暴力解太難寫,我們也可以直接手算測資的答案。
源自於一個工程師的故事,講述這個工程師在 debug 時會向著隨身攜帶的黃色小鴨說明程式碼來 debug。這邊指的是跟一個假想的人物、或是你帶進考場的玩偶解釋你的程式碼是如何運作的,藉此檢驗程式在邏輯上有沒有明顯不合常理之處。雖然聽起來有些荒謬,但程式的 bug 往往位於那些盲點區域,當我們嘗試去解釋一個程式在做什麽時,我們才比較有機會看到這些盲點。