跳转至

常見錯誤

本頁面主要列舉一些競賽中很多人經常會出現的錯誤。

因環境不同導致的錯誤

  • scanfprintf 使用 %I64d 格式指示符在 Linux 下可能導致輸出格式錯誤。

會引起 CE 的錯誤

這類錯誤多為詞法、語法和語義錯誤,引發的原因較為簡單,修復難度較低。

例:

  • int main() 寫為 int mian() 之類的拼寫錯誤。

  • 寫完 structclass 忘記寫分號。

  • 數組開太大,(在 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" 實例
      
  • 使用 gotoswitch-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 讀入的時候沒加取地址符 &

  • 使用 scanfprintf 的時候參數類型與格式指定符不符。

  • 同時使用位運算和邏輯運算符 == 並且未加括號。

    • 示例:(x >> j) & 3 == 2
  • int 字面量溢出。

    • 示例:long long x = 0x7f7f7f7f7f7f7f7f1<<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 類型),導致得出錯誤的答案而失分。
  • 存圖時,節點編號 0 開始,而題目給的邊中兩個端點的編號從 1 開始,讀入的時候忘記 -1。

  • 大/小於號打錯或打反。

  • 在執行 ios::sync_with_stdio(false); 後混用 scanf/printfstd::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);  // 正確
      

換行符不同

Warning

在正式比賽中會盡量保證選手答題的環境和最終測試的環境相同。

本節內容僅適用於模擬賽等情況,而我們也建議出題人儘量讓數據符合 數據格式

不同的操作系統使用不同的符號來標記換行,以下為幾種常用系統的換行符:

  • LF(用 \n 表示):UnixUnix 兼容系統

  • 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 的逆元)

    示例
    1
    cout << x / 0 << endl;
    
  • 數組(下標)越界

    例如:

    • 未正確設置循環的初值導致訪問了下標為 -1 的值。

    • 無向圖邊表未開 2 倍。

    • 線段樹未開 4 倍空間。

    • 看錯數據範圍,少打一個零。

    • 錯誤預估了算法的空間複雜度。

    • 寫線段樹的時候,pushuppushdown 葉節點。

      正確的做法:不要越界,記得檢查自己的代碼,使得下標訪問數 x,在定義的下標中。

  • 除 main 外有返回值函數執行至結尾未執行任何 return 語句

    即使有一個分支有返回值,但是其他分支卻沒有,結果也是未定義的。

    可以向編譯選項中追加 -Wall,檢查編譯器是否給出有關於函數未 return 的警告。

  • 嘗試修改字符串字面量

    示例
    1
    2
    3
    char *p = "OI-wiki";
    p[0] = 'o';
    p[1] = 'i';
    

    這樣試圖修改字符串字面量會導致 未定義行為,應當使用其他 合適 的數據類型,例如 std::stringchar[]

  • 多次釋放/非法解引用一片內存

    例如:

    • 未初始化就解引用指針。

    • 指針指向的內存區域已經釋放。

      使用 erasedeletefree 操作應注意不要對同一地址/對象多次使用。

  • 嘗試釋放由 new [] 分配的整塊內存的一部分

    例如:

    1
    2
    3
    4
    5
    6
    object *pool = new object[POOL_SIZE];
    
    object *pointer = pool + 10;
    
    // 報錯!
    delete pointer;
    

    常見於使用內存池提前分配整塊內存後,試圖使用 deletefree() 釋放從內存池中獲取的單個對象。

  • 解引用空指針/野指針

    對於空指針:先應該判斷空指針,可以用 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;
    }
    

    可能被編譯器直接優化為:

    1
    int 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<afalse;若 a<btrue,則 b<afalse;若 a<btrueb<ctrue,則 a<ctrue。其中要特別注意第二點。 如果不滿足上述要求,排序時很可能會 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

  • 使用堆空間後忘記 deletefree