緩沖區的優點很明顯,它加快了程序的運行速度,減少了硬件的讀寫次數,讓整個計算機變得流暢起來;但是,緩沖區也帶來了一些負面影響,經過前面幾節的學習相信讀者也見識到了。
那么,該如何消除這些負面影響呢?思路其實也很簡單,在輸入輸出之前清空(刷新)緩沖區即可:
- 對於輸出操作,清空緩沖區會使得緩沖區中的所有數據立即顯示到屏幕上;很明顯,這些數據沒有地方存放了,只能輸出了。
- 對於輸入操作,清空緩沖區就是丟棄殘留字符,讓程序直接等待用戶輸入,避免引發奇怪的行為。
本節的代碼用到了一些暫時沒有學到的知識,估計很多初學者會不理解,沒關系,那就先記住吧,記不住就復制吧,總之,按照葫蘆畫瓢就完了。
清空輸出緩沖區
清空輸出緩沖區很簡單,使用下面的語句即可:
fflush(stdout);
fflush() 是一個專門用來清空緩沖區的函數,stdout 是 standard output 的縮寫,表示標准輸出設備,也即顯示器。整個語句的意思是,清空標准輸出緩沖區,或者說清空顯示器的緩沖區。
Windows 平台下的 printf()、puts()、putchar() 等輸出函數都是不帶緩沖區的,所以不用清空,下面的代碼演示了如何在 Linux 和 Mac OS 平台下清空緩沖區:
- #include<stdio.h>
- #include<unistd.h>
- int main()
- {
- printf("C語言中文網");
- fflush(stdout); //本次輸出結束后立即清空緩沖區
- sleep(5);
- printf("http://c.biancheng.net\n");
- return 0;
- }
程序運行后,第一個 pirntf() 立即輸出,等待 5 秒以后,第二個 printf() 才輸出,這就符合我們的慣性思維了。如果不加fflush(stdout)
語句,程序運行后,第一個 printf() 並不會立即輸出,而是等待 5 秒以后和第二個 scanf() 一起輸出(已在《C語言數據輸出大匯總以及輕量進階》中進行了演示),這有點不符合我們的思維習慣。
清空輸入緩沖區
首先,很遺憾地說,沒有一種既簡潔明了又適用於所有平台的清空輸入緩沖區的方案。只有一種很蹩腳的方案能適用於所有平台,那就是將輸入緩沖區中的數據都讀取出來,但是卻不使用。
大家不要以為我很輕松地就能說出這句話,我FQ查閱了很多英文資料,又測試了很多平台和編譯器才敢說的。
我們先說兩種通用的方案,雖然它很蹩腳,但是卻行之有效。
1) 使用 getchar() 清空緩沖區
getchar() 是帶有緩沖區的,每次從緩沖區中讀取一個字符,包括空格、制表符、換行符等空白符,只要我們讓 getchar() 不停地讀取,直到讀完緩沖區中的所有字符,就能達到清空緩沖區的效果。請看下面的代碼:
- int c;
- while((c = getchar()) != '\n' && c != EOF);
該代碼不停地使用 getchar() 獲取緩沖區中的字符,直到遇見換行符\n
或者到達文件結尾才停止。由於大家所學知識不足,這段代碼暫時無法理解,我也就不細說了,在實際開發中,大家按照下面的形式使用即可:
- #include <stdio.h>
- int main()
- {
- int a = 1, b = 2;
- char c;
- scanf("a=%d", &a);
- while((c = getchar()) != '\n' && c != EOF); //在下次讀取前清空緩沖區
- scanf("b=%d", &b);
- printf("a=%d, b=%d\n", a, b);
- return 0;
- }
輸入示例:
a=100↙
b=200↙
a=100, b=200
按下第一個回車鍵后,只有第一個 scanf() 讀取成功了,第二個 scanf() 並沒有開始讀取,等我們再次輸入並按下回車鍵后,第二個 scanf() 才開始讀取,這就符合我們的操作習慣了。如果沒有清空緩沖區的語句,按下第一個回車鍵后,兩個 scanf() 都讀取了,只是第二個 scanf() 讀取失敗了,讓人覺得很怪異,這點已在《使用scanf從鍵盤輸入數據》中進行了演示。
改變輸入方式,再次嘗試一下:
a=100b=200↙
b=300↙
a=100, b=300
你看,第一次輸入的多余內容並沒有起作用,就是因為它們在第二個 scanf() 之前被清空了。
這種方案的關鍵之處在於,getchar() 是帶有緩沖區的,並且一切字符通吃,或者說一切字符都會讀取,不會忽略。不過這種方案有個缺點,就是要額外定義一個變量 c,對於有強迫症的讀者來說可能有點難受。
2) 使用 scanf() 清空緩沖區
scanf() 還有一種高級用法,就是使用類似於正則表達式的通配符,這樣它就可以讀取所有的字符了,包括空格、換行符、制表符等空白符,不會再忽略它們了。並且,scanf() 還允許把讀取到的數據直接丟棄,不用賦值給變量。
請看下面的語句:
scanf("%*[^\n]"); scanf("%*c");
第一個 scanf() 將逐個讀取緩沖區中\n
之前的其它字符,% 后面的 * 表示將讀取的這些字符丟棄,遇到\n
字符時便停止讀取。此時,緩沖區中尚有一個\n
遺留,第二個 scanf() 再將這個\n
讀取並丟棄,這里的星號和第一個 scanf() 的星號作用相同。由於所有從鍵盤的輸入都是以回車結束的,而回車會產生一個\n
字符,所以將\n
連同它之前的字符全部讀取並丟棄之后,也就相當於清除了輸入緩沖區。
相信很多讀者都不明白這種寫法,沒關系,下節我們在講解 scanf() 的高級用法時還會再解釋。
我們來演示這種方案的效果:
- #include <stdio.h>
- int main()
- {
- int a = 1, b = 2;
- scanf("a=%d", &a);
- scanf("%*[^\n]"); scanf("%*c"); //在下次讀取前清空緩沖區
- scanf("b=%d", &b);
- printf("a=%d, b=%d\n", a, b);
- return 0;
- }
輸入示例 ①:
a=100↙
b=200↙
a=100, b=200
輸入示例 ②:
a=100b=200↙
b=300↙
a=100, b=300
相比使用 getchar(),這種方案不用額外定義一個變量,看起來更加整潔。
兩種不通用、不建議的方案
以上兩種清空輸入緩沖區的方案是通用的,在任何平台、任何編譯器、任何情景下都奏效。除此以外,有些教材和老師可能還講解過其它的方案,常見的有兩種,分別是fflush(stdin)
和rewind(stdin)
。
1) fflush(stdin)
fflush(stdin) 常用於 Windows 平台,在 VC 6.0、VS2010 等較老的編譯器下確實能夠清空緩沖區。
C語言標准規定,當 fflush() 用於 stdout 時,必須要有清空輸出緩沖區的作用;但是C語言標准並沒有規定 fflush() 用於 stdin 時的作用,編譯器的實現者可以自由決定,所以它的行為是未定義的。
較老的微軟編譯器進行了擴展,賦予了 fflush(stdin) 清空輸入緩沖區的功能,例如 VC 6.0、VS2010 等;但是,較新的微軟編譯器又取消了這種擴展,不再支持 fflush(stdin),例如 VS2015、VS2017 等,在這些版本的編譯器下,fflush() 是無效的。
較老的 GCC 是不支持 fflush(stdin) 的,但是最新的 GCC 又開始支持 fflush(stdin) 了。
LLVM/Clang 編譯器始終不支持 fflush(stdin)。
總之,fflush(stdin) 這種不標准的寫法只適用於一部分編譯器,通用性非常差,所以不建議使用。如果你由於個人習慣堅持使用,請測試你的編譯器是否支持。
2) rewind(stdin)
rewind() 函數並沒有清空緩沖區的功能,但是 rewind(stdin) 偏偏在某些編譯器下會導致清空緩沖區的假象,例如 VS2015、LLVM/Clang。在 GCC 下,rewind(stdin) 是沒有任何效果的。
總結
最靠譜、最通用、最有效的清空輸入緩沖區的方案就是使用 getchar() 或者 scanf() 將緩沖區中的數據逐個讀取出來,其它方案都有或多或少的問題。