常見錯誤
本頁面主要列舉一些競賽中很多人經常會出現的錯誤。
因環境不同導致的錯誤
scanf或printf使用%I64d格式指示符在 Linux 下可能導致輸出格式錯誤。
會引起 CE 的錯誤
這類錯誤多為詞法、語法和語義錯誤,引發的原因較為簡單,修復難度較低。
例:
-
int main()寫為int mian()之類的拼寫錯誤。 -
寫完
struct或class忘記寫分號。 -
數組開太大,(在 OJ 上)使用了不合法的函數(例如多線程),或者函數聲明但未定義,會引起鏈接錯誤。
-
函數參數類型不匹配。
-
示例:如使用
<algorithm>頭文件中的max函數時,傳入了一個int類型參數和一個long long類型參數。1 2 3 4
// query 為返回 long long 類型的自定義函數 printf("%lld\n", max(0, query(1, 1, n, l, r)); //錯誤 沒有與參數列表匹配的 重載函數 "std::max" 實例
-
-
使用
goto和switch-case的時候跳過了一些局部變量的初始化。
不會引起 CE 但會引起 Warning 的錯誤
犯這類錯誤時寫下的程序雖然能通過編譯,但大概率會得到錯誤的程序運行結果。這類錯誤會在使用 -W{warningtype} 參數編譯時被編譯器指出。
-
賦值運算符
=和比較運算符==不分。-
示例:
1 2 3 4 5 6 7 8 9
std::srand(std::time(nullptr)); int n = std::rand(); if (n = 1) printf("Yes"); else printf("No"); // 無論 n 的隨機所得值為多少,輸出肯定是 Yes // 警告 運算符不正確: 在 Boolean 上下文中執行了常量賦值。應考慮改用「==」。 -
如果確實想在原應使用
==的語句裏使用=(比如while (foo = bar)),又不想收到 Warning,可以使用 雙括號:while ((foo = bar))。
-
-
由於運算符優先級產生的錯誤。
-
示例:
1 2 3 4 5 6
// 錯誤 // std::cout << (1 << 1 + 1); // 正確 std::cout << ((1 << 1) + 1); // 警告 「<<」: 檢查運算符優先級是否有可能的錯誤;使用括號闡明優先級
-
-
不正確地使用
static修飾符。 -
使用
scanf讀入的時候沒加取地址符&。 -
使用
scanf或printf的時候參數類型與格式指定符不符。 -
同時使用位運算和邏輯運算符
==並且未加括號。- 示例:
(x >> j) & 3 == 2
- 示例:
-
int字面量溢出。- 示例:
long long x = 0x7f7f7f7f7f7f7f7f,1<<62。
- 示例:
-
未初始化局部變量。
未初始化變量會發生什麼
原文:https://loj.ac/d/3679 by @hly1204
例如我們在 C++ 中聲明一個
int a;但不初始化,可能有時候會認為a是一個「隨機」(其實可能不是真的隨機)的值,但是可能將其認為是一個固定的值,但實際上並非如此。我們在簡單的測試代碼中
https://wandbox.org/permlink/T2uiVe4n9Hg4EyWT
代碼是:
1 2 3 4 5 6 7
#include <iostream> int main() { int a; std::cout << std::boolalpha << (a < 0 || a == 0 || a > 0); return 0; }在一些編譯器和環境上開啓優化後,其輸出為 false。
有興趣的話可以看 https://www.ralfj.de/blog/2019/07/14/uninit.html,儘管其是用 Rust 做的實驗,但是本質是一樣的。
-
局部變量與全局變量重名,導致全局變量被意外覆蓋。(開
-Wshadow就可檢查此類錯誤。) -
運算符重載後引發的輸出錯誤。
- 示例:
1 2 3 4
// 本意:前一個 << 為重載後的運算符,表示輸出;後一個 << 為移位運算符,表示將 1 // 左移 1 位。 但由於忘記加括號,導致編譯器將後一個 << // 也判作輸出運算符,而導致輸出的結果與預期不同。 錯誤 std::cout << 1 << 1; 正確 std::cout << (1 << 1);
- 示例:
既不會引起 CE 也不會引發 Warning 的錯誤
這類錯誤無法被編譯器發現,僅能自行查明。
會導致 WA 的錯誤
-
上一組數據處理完畢,讀入下一組數據前,未清空數組。
-
讀入優化未判斷負數。
-
所用數據類型位寬不足,導致溢出。
- 如習語「三年 OI 一場空,不開
long long見祖宗」所描述的場景。選手因為沒有在正確的地方開long long(將整數定義為long long類型),導致得出錯誤的答案而失分。
- 如習語「三年 OI 一場空,不開
-
存圖時,節點編號 0 開始,而題目給的邊中兩個端點的編號從 1 開始,讀入的時候忘記 -1。
-
大/小於號打錯或打反。
-
在執行
ios::sync_with_stdio(false);後混用scanf/printf和std::cin/std::cout兩種 IO,導致輸入/輸出錯亂。-
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// 這個例子將説明關閉與 stdio 的同步後,混用兩種 IO 方式的後果 // 建議單步運行來觀察效果 #include <cstdio> #include <iostream> int main() { // 關閉同步後,cin/cout 將使用獨立緩衝區,而不是將輸出同步至 scanf/printf // 的緩衝區,從而減少 IO 耗時 std::ios::sync_with_stdio(false); // cout 下,使用'\n'換行時,內容會被緩衝而不會被立刻輸出 std::cout << "a\n"; // printf 的 '\n' 會刷新 printf 的緩衝區,導致輸出錯位 printf("b\n"); std::cout << "c\n"; // 程序結束時,cout 的緩衝區才會被輸出 return 0; } -
特別的,也不能在執行
ios::sync_with_stdio(false);後使用freopen。
-
-
由於宏的展開,且未加括號導致的錯誤。
-
示例:該宏返回的值並非 \(4^2 = 16\) 而是 \(2+2\times 2+2 = 8\)。
1 2
#define square(x) x* x printf("%d", square(2 + 2));
-
-
哈希的時候沒有使用
unsigned導致的運算錯誤。- 對負數的右移運算會在最高位補 1。參見:位運算
-
沒有刪除或註釋掉調試輸出語句。
-
誤加了
;。-
示例:
1 2 3
/* clang-format off */ while (1); printf("OI Wiki!\n");
-
-
哨兵值設置錯誤。例如,平衡樹的
0節點。 -
在類或結構體的構造函數中使用
:初始化變量時,變量聲明順序不符合初始化時候的依賴關係。- 成員變量的初始化順序與它們在類中聲明的順序有關,而與初始化列表中的順序無關。參見:構造函數與成員初始化器列表 的「初始化順序」
-
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
#include <iostream> class Foo { public: int a, b; // a 將在 b 前初始化,其值不確定 Foo(int x) : b(x), a(b + 1) {} }; int main() { Foo bar(1, 2); std::cout << bar.a << ' ' << bar.b; } // 可能的輸出結果:-858993459 1
-
並查集合並集合時沒有把兩個元素的祖先合併。
-
示例:
1 2
f[a] = b; // 錯誤 f[find(a)] = find(b); // 正確
-
換行符不同
不同的操作系統使用不同的符號來標記換行,以下為幾種常用系統的換行符:
-
LF(用
\n表示):Unix或Unix兼容系統 -
CR+LF(用
\r\n表示):Windows -
CR(用
\r表示):Mac OS至版本 9
而 C/C++ 利用轉義序列 \n 來換行,這可能會導致我們認為輸入中的換行符也一定是由 \n 來表示,而只讀入了一個字符來代表換行符,這就會導致我們沒有完全讀入輸入文件。
以下為解決方案:
-
多次
getchar(),直到讀到想要的字符為止。 -
使用
cin讀入,這可能會增大代碼常數。 -
使用
scanf("%s",str)讀入一個字符串,然後取str[0]作為讀入的字符。 -
使用
scanf(" %c",&c)過濾掉所有空白字符。
會導致未知的結果
未定義行為會導致未知的結果,可能是 WA,RE 等。編譯器通常會假定你的程序不會出現未定義行為,因此出現開 O2 與不開 O2 代碼行為不一致的情況。
-
除以 0(求 0 的逆元)
示例
1cout << x / 0 << endl; -
數組(下標)越界
例如:
-
未正確設置循環的初值導致訪問了下標為 -1 的值。
-
無向圖邊表未開 2 倍。
-
線段樹未開 4 倍空間。
-
看錯數據範圍,少打一個零。
-
錯誤預估了算法的空間複雜度。
-
寫線段樹的時候,
pushup或pushdown葉節點。正確的做法:不要越界,記得檢查自己的代碼,使得下標訪問數
x,在定義的下標中。
-
-
除 main 外有返回值函數執行至結尾未執行任何 return 語句
即使有一個分支有返回值,但是其他分支卻沒有,結果也是未定義的。
可以向編譯選項中追加
-Wall,檢查編譯器是否給出有關於函數未 return 的警告。 -
嘗試修改字符串字面量
示例
1 2 3
char *p = "OI-wiki"; p[0] = 'o'; p[1] = 'i';這樣試圖修改字符串字面量會導致 未定義行為,應當使用其他 合適 的數據類型,例如
std::string和char[]。 -
多次釋放/非法解引用一片內存
例如:
-
未初始化就解引用指針。
-
指針指向的內存區域已經釋放。
使用
erase或delete或free操作應注意不要對同一地址/對象多次使用。
-
-
嘗試釋放由
new []分配的整塊內存的一部分例如:
1 2 3 4 5 6
object *pool = new object[POOL_SIZE]; object *pointer = pool + 10; // 報錯! delete pointer;常見於使用內存池提前分配整塊內存後,試圖使用
delete或free()釋放從內存池中獲取的單個對象。 -
解引用空指針/野指針
對於空指針:先應該判斷空指針,可以用
p == nullptr或!p。對於野指針:可以釋放指針的時候將其置為
nullptr以規避。 -
有符號數溢出
例如我們有一個表達式
x+1 > x。正常輸出應當是
true,但是在INT_MAX作為x時輸出false,這時稱為signed integer overflow。可以使用更大的數據類型(例如
long long或__int128),或判斷溢出。若保證無負數,亦可使用無符號整型。有符號整數溢出可能影響編譯優化,例如代碼:
1 2 3 4
int foo(int x) { if (x > x + 1) return 1; return 0; }可能被編譯器直接優化為:
1int foo(int x) { return 0; }因為編譯器可以假定有符號整數永遠不會溢出,因此
x > x + 1恆成立。 -
使用未初始化的變量
示例
1 2 3 4 5
int foo(int a) { int t; /* 沒有初始化 */ if (/* 使用 */ t > 3) return a; return 0; }
會導致 RE
-
沒刪文件操作(某些 OJ)。
-
排序時比較函數的錯誤
std::sort要求比較函數是嚴格弱序:a<a為false;若a<b為true,則b<a為false;若a<b為true且b<c為true,則a<c為true。其中要特別注意第二點。 如果不滿足上述要求,排序時很可能會 RE。 例如,編寫莫隊的奇偶性排序時,這樣寫是錯誤的:上述代碼中1 2 3 4 5 6
bool operator<(const int a, const int b) { if (block[a.l] == block[b.l]) return (block[a.l] & 1) ^ (a.r < b.r); else return block[a.l] < block[b.l]; }(block[a.l]&1)^(a.r<b.r)不滿足上述要求的第二點。 改成這樣就正確了:1 2 3 4 5 6 7 8 9
bool operator<(const int a, const int b) { if (block[a.l] == block[b.l]) // 錯誤:不滿足嚴格弱序的要求 // return (block[a.l] & 1) ^ (a.r < b.r); // 正確 return (block[a.l] & 1) ? (a.r < b.r) : (a.r > b.r); else return block[a.l] < block[b.l]; } -
Windows 下棧空間不足,導致棧空間溢出,Windows 向程序發出 SIGSEGV 信號,程序終止並返回 3221225725(即 0xC00000FD, NTSTATUS 定義為
STATUS_STACK_OVERFLOW)。
若使用 gcc 編譯器,可在編譯時加入命令-Wl,--stack=SIZE以擴展棧空間,其中SIZE為棧空間大小字節數。
會導致 TLE
-
分治未判邊界導致死遞歸。
-
死循環。
-
循環變量重名。
-
循環方向反了。
-
-
BFS 時不標記某個狀態是否已訪問過。
-
使用宏展開編寫 min/max
這種錯誤會大大增加程序的運行時間,甚至直接影響代碼的時間複雜度。在初學者寫線段樹時尤為多見。
常見的錯誤寫法是這樣的:
1 2
#define Min(x, y) ((x) < (y) ? (x) : (y)) #define Max(x, y) ((x) > (y) ? (x) : (y))這樣寫雖然在正確性上沒有問題,但是如果直接對函數的返回值取 max,如
a = Max(func1(), func2()),而這個函數的運行時間較長,則會大大影響程序的性能,因為宏展開後是a = func1() > func2() ? func1() : func2()的形式,調用了三次函數,比正常的 max 函數多調用了一次。注意,如果func1()每次返回的答案不一樣,還會導致這種max的寫法出現錯誤。例如func1()為return ++a;而a為全局變量的情況。示例:如下代碼會被卡到單次查詢 \(\Theta(n)\) 導致 TLE。
1 2 3 4 5 6 7 8 9 10 11 12 13
#define max(x, y) ((x) > (y) ? (x) : (y)) int query(int t, int l, int r, int ql, int qr) { if (ql <= l && qr >= r) { ++ti[t]; // 記錄結點訪問次數方便調試 return vi[t]; } int mid = (l + r) >> 1; if (mid >= qr) return query(lt(t), l, mid, ql, qr); if (mid < ql) return query(rt(t), mid + 1, r, ql, qr); return max(query(lt(t), l, mid, ql, qr), query(rt(t), mid + 1, r, ql, qr)); } -
使用 + 運算符向
std::string類字符串追加字符這種錯誤會創建一個臨時
string變量,修改完成後再賦值給原變量。這種錯誤無法被編譯器優化,在數據量大的情況下可能會導致時間複雜度退化。常見 錯誤寫法:
1 2 3
std::string a; char b = 'c'; a = a + b;當執行這段代碼時,程序首先會創建一個臨時
string變量,隨後將a的值存入臨時變量,然後在末尾添加b的值,最後再存入a。從 彙編結果 可以看出,
a = a + b調用了三次std::__cxx11::basic_string中的功能,分別為operator+、operator=和創建變量。正確寫法應該是:
1 2 3
std::string a; char b = 'c'; a += b;這種寫法 會直接將字符
b附加到字符串a中,僅調用了一次operator+=。更詳細的性能比較可參考 Benchmark。 -
沒刪文件操作(某些 OJ)。
-
在
for/while循環中重複執行復雜度非 \(O(1)\) 的函數。嚴格來説,這可能會引起時間複雜度的改變。
會導致 MLE
-
數組過大。
-
STL 容器中插入了過多的元素。
-
經常是在一個會向 STL 插入元素的循環中死循環了。
-
也有可能被卡了。
-
會導致常數過大
-
定義模數的時候,未定義為常量。
-
示例:
1 2
// int mod = 998244353; // 錯誤 const int mod = 998244353; // 正確,方便編譯器按常量處理
-
-
使用了不必要的遞歸(尾遞歸不在此列)。
-
將遞歸轉化成迭代的時候,引入了大量額外運算。
只在程序在本地運行的時候造成影響的錯誤
-
文件操作有可能會發生的錯誤:
-
對拍時未關閉文件指針
fclose(fp)就又令fp = fopen()。這會使得進程出現大量的文件野指針。 -
freopen()中的文件名未加.in/.out。
-
-
使用堆空間後忘記
delete或free。
本页面最近更新:,更新历史
发现错误?想一起完善? 在 GitHub 上编辑此页!
本页面贡献者:H-J-Granger, orzAtalod, ksyx, Ir1d, Chrogeek, Enter-tainer, yiyangit, shuzhouliu, broken-paint
本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用