seansie's blog

C/C++ IO 基本優化介紹(IO同步取消方法與進階函數使用) #0 | seansie blog

在競程解題時,有些題目會刻意設計大量資料,這時 I/O(輸入輸出)優化就變得格外重要。雖然 C/C++ 相比其他直譯式語言,I/O 效率已經相當不錯,但仍有進一步提升的空間。本文將介紹各種 I/O 優化技巧,從入門的安全方法到進階但可能帶有風險的技巧,讓您在競程中更上一層樓。

基本版本(標配)

在 C++ 中,我們同時擁有來自 C 語言的 scanfprintf,以及 C++ 標準庫提供的 cincout。由於 C++ 是 C 的超集,也就是說 C++ 完全兼容 C 的語法和功能,這使得我們可以在 C++ 中自由使用這兩套輸入輸出函式。然而,這樣的便利性也帶來了潛在的衝突。

衝突的根源

這些衝突主要源自於它們對於輸入緩衝區(input buffer)的處理方式不同。scanfcin 在讀取輸入時,都會從輸入緩衝區中獲取資料。如果我們在程式中混合使用這兩者,可能會導致輸入緩衝區的狀態混亂,進而產生非預期的結果。

常見的衝突情況

  • 殘留的換行符號:當我們使用 scanf 讀取數字後,按下 Enter 鍵會在輸入緩衝區中留下一個換行符號(’\n’)。如果接下來使用 cin 讀取字串,cin 會直接讀取這個換行符號,導致讀取的字串為空。

確實,在多數情況下,開發者會傾向於統一使用 cincoutscanfprintf,避免混用的麻煩。如果您追求程式的輸出效率,取消輸入輸出流同步是個值得考慮的優化策略。

取消輸入輸出流同步

C++ 預設會將 cincout 與 C 語言的 stdio(也就是 scanfprintf 所使用的標準輸入輸出庫)同步。這意味著每次進行 cincout 操作時,都會強制刷新底層的輸入輸出緩衝區,確保與 stdio 的操作保持一致。雖然這增加了安全性,但也可能導致一些效能損耗,尤其是在頻繁進行輸入輸出操作的情況下。

取消同步的方法

您可以透過以下程式碼取消 cincoutstdio 的同步:

std::ios_base::sync_with_stdio(false);

這行程式碼會告知 C++ 標準庫,cincout 不需要再與 stdio 同步。如此一來,cincout 可以更自由地管理自己的緩衝區,從而提升輸入輸出的效率。

注意事項

  • 取消同步後,請勿混用:一旦取消同步,就絕對不要再混用 cincoutscanfprintf,否則可能導致未定義行為(undefined behavior),造成程式崩潰或產生錯誤的結果。

cin.tie(0) 的作用

在 C++ 的輸入輸出流程 (iostream) 中,cin.tie(0) 的作用是解除標準輸入 cin 與標準輸出 cout 之間的綁定關係。

預設行為

一般情況下,cincout 之間存在一種稱為「綁定 (tie)」的關係。這意味著在從 cin 接收輸入之前,會自動清空 (flush) cout 的緩衝區 (也就是將緩衝區的內容輸出)。這種行為在與使用者互動的程式中很自然,但在涉及大量輸入輸出的場景 (如競賽程式設計) 中,可能會成為效能瓶頸。

cin.tie(0) 的效果

執行 cin.tie(0) 後,這種綁定關係會被解除,cin 的操作不再強制清空 cout 的緩衝區。這可以提高輸入輸出處理的效率,尤其是在讀取大量資料時,可能會顯著提升執行速度。

經常搭配使用的操作

cin.tie(0) 通常會與 std::ios::sync_with_stdio(false) 一起使用。std::ios::sync_with_stdio(false) 用於解除 C++ 的輸入輸出流與 C 語言標準輸入輸出庫 (stdio) 之間的同步。這使得 C++ 的輸入輸出流獨立於 stdio,從而實現更快的操作。

取消綁定關係的方法

main函數加上加上此段程式碼即可。

cin.tie(0)

注意事項

  • 使用 std::ios::sync_with_stdio(false)cin.tie(0) 後,不應混用 C++ 的輸入輸出流和 C 語言的標準輸入輸出庫 (例如:不要同時使用 coutprintf)。
  • 在競程中,一個輸入通常對應一個輸出,輸出順序不重要,因此使用 cin.tie(0) 後可能出現的輸出順序問題,並不會影響結果。
  • 基本上這樣優化下來已經與C的 printfscanf 速度差不多了,因此如果用習慣cin cout 的人可以以此法優化有不失方便性。

進階用法

  • puts()

    • 功能:將一個字串 (以 null 字元 ‘\0’ 結尾的字元陣列) 輸出到標準輸出 (通常是螢幕),並自動在結尾加上一個換行字元 ‘\n’。

    • 用法

      #include <stdio.h>
      
      int main() {
          puts("Hello, world!");  // 輸出 "Hello, world!" 並換行
          return 0;
      }
      

    putchar()

    • 功能:將一個單個字元輸出到標準輸出。

    • 用法

      #include <stdio.h>
      
      int main() {
          putchar('A');  // 輸出字元 'A'
          return 0;
      }
      

    gets()

    • 功能:從標準輸入 (通常是鍵盤) 讀取一行字串,直到遇到換行字元 ‘\n’ 為止,並將換行字元替換為 null 字元 ‘\0’,然後將字串儲存到指定的字元陣列中。

    • 重要提醒:由於 gets() 函式不檢查輸入字串的長度是否超過指定的字元陣列大小,可能導致緩衝區溢位 (buffer overflow) 的安全問題。因此,強烈建議使用更安全的 fgets() 函式來取代 gets()

    • 用法

      #include <stdio.h>
      
      int main() {
          char name[50];
          printf("請輸入您的名字:");
          gets(name);  // 從鍵盤讀取名字,並儲存到 name 陣列中
          printf("您好,%s!\n", name);
          return 0;
      }
      

    getchar()

    • 功能:從標準輸入讀取一個單個字元,並返回該字元的 ASCII 碼值 (整數)。如果遇到檔案結尾 (end-of-file, EOF),則返回 EOF。

    • 用法

      #include <stdio.h>
      
      int main() {
          int ch;
          printf("請輸入一個字元:");
          ch = getchar();
          printf("您輸入的字元是:%c\n", ch);
          return 0;
      }
      

    總結

    • puts()putchar() 用於輸出,分別輸出字串和單個字元。
    • gets()getchar() 用於輸入,分別讀取一行字串和單個字元。
    • 為了安全起見,請避免使用 gets(),改用 fgets()

總結

總而言之,在競程的賽場上,當資料量龐大到足以影響程式運行速度時,輸入輸出(I/O)優化便成為決定勝負的關鍵因素。儘管 C/C++ 本身具備良好的 I/O 效率,但仍有優化的空間。透過本文介紹的技巧,從基礎的 cin/cout 與 scanf/printf 衝突排除、取消同步與綁定,到進階的 C 語言 I/O 函式運用,程式設計師能更有效率地處理大量資料,讓程式在競程中表現更出色。