seansie's blog

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

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

基本版本(標配)

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

衝突的根源

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

常見的衝突情況

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

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

取消輸入輸出流同步

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

取消同步的方法

您可以透過以下程式碼取消 cin、cout 與 stdio 的同步:

std::ios_base::sync_with_stdio(false);

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

注意事項

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

cin.tie(0) 的作用

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

預設行為

一般情況下,cin 和 cout 之間存在一種稱為「綁定 (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 語言的標準輸入輸出庫 (例如:不要同時使用 cout 和 printf)。
  • 在競程中,一個輸入通常對應一個輸出,輸出順序不重要,因此使用 cin.tie(0) 後可能出現的輸出順序問題,並不會影響結果。
  • 基本上這樣優化下來已經與C的 printf 與 scanf 速度差不多了,因此如果用習慣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 函式運用,程式設計師能更有效率地處理大量資料,讓程式在競程中表現更出色。