為了在命令列程式中實作和用戶的互動,我們撰寫的程式的運行程序中往往涉及到對標準輸入/輸出流的多次讀寫,
在C語言中接受用戶輸入這一塊,有著一個老生常談的問題:“怎么樣及時清空輸入流中的資料?”
這也是這篇小筆記的主題內容,

先從緩沖區說起,
緩沖區是記憶體中劃分出來的一部分,通常來說,緩沖區型別有三種:
- 全緩沖
- 行緩沖
- 無緩沖
行緩沖
在C語言中緩沖區這個概念的存在感還是挺強的,比較常用到的緩沖區型別則是行緩沖了,如標準輸入流 stdin 和標準輸出流 stdout一般(終端環境下)就是在行緩沖模式下的,
行緩沖,顧名思義,就是針對該緩沖區的I/O操作是基于行的,
-
在遇到換行符前,程式的輸入和輸出都會先被暫存到流對應的緩沖區中
-
而在遇到換行符后(或者緩沖區滿了),程式才會進行真正的I/O操作,將該緩沖區中的資料寫到對應的流 (stream) 中以供后續讀取,
就標準輸入stdin而言,用戶的輸入首先會被存到相應的輸入緩沖區中,每當用戶按下回車鍵輸入一個換行符,程式才會進行I/O操作,將緩沖區暫存的資料寫入到stdin中,以供輸入函式使用,

而對標準輸出stdout來說,輸出內容也首先會被暫存到相應的輸出緩沖區中,每當輸出資料遇到換行符時,程式才會將緩沖區中的資料寫入stdout,繼而列印到螢屏上,
這也是為什么在緩沖模式下,輸出的內容不會立即列印到螢屏上:
#include <stdio.h>
int main()
{
// 設定緩沖模式為行緩沖,緩沖區大小為10位元組
setvbuf(stdout, NULL, _IOLBF, 10);
fprintf(stdout, "1234567"); // 這里先向stdout對應的緩沖區中寫入了7位元組
getchar(); // 這里等待用戶輸入
printf("89"); // 再向stdout對應的緩沖區中寫入了2位元組
getchar(); // 接著等待用戶輸入
printf("Print!"); // 再向stdout對應的緩沖區中寫入了6位元組
getchar(); // 最后再等待一次用戶輸入
return 0;
}
運行效果:

可以看到,直到執行到第二個getchar()時,螢屏上沒有新的輸出,
而在執行了printf("Print!")之后,輸出緩沖區被填滿了,輸出緩沖區中現有的10位元組的資料被寫入到stdout中,繼而才在螢屏上列印出123456789P,
緩沖區內容被讀走后,剩余的字串rint!接著被寫入輸出緩沖區,程式運行結束后,輸出緩沖區中的內容會被全部列印到螢屏上,所以會在最后看到rint!,
C語言中常用的輸入函式
輸入函式做的作業主要是從檔案流中讀取資料,亦可將讀取到的資料儲存到記憶體中以供后續程式使用,
基于字符
// 從給定的檔案流中讀一個字符 (fgetc中的 f 的意思即"function")
int fgetc( FILE *stream );
// 同fgetc,但是getc的實作*可能*是基于宏的
int getc( FILE *stream );
// 相當于是getc(stdin),從標準輸入流讀取一個字符
int getchar(void);
// 回傳獲取的字符的ASCII碼值,如果到達檔案末尾就回傳EOF(即回傳-1)
基于行
// 從給定的檔案流中讀取(count-1)個字符或者讀取直到遇到換行符或者EOF
// fgets中的f代表“file”,而s代表“string”
char *fgets( char *restrict str, int count, FILE *restrict stream );
// 回傳指向字串的指標或者空指標NULL
格式化輸入
// 按照format的格式從標準輸入流stdin中讀取所需的資料并儲存在相應的變數中
// scanf中的f代表“format”
int scanf( const char *restrict format, ... );
// 按照format的格式從檔案流stream中讀取所需的資料并儲存在相應的變數中
// fscanf中前一個f代表“file(stream)”,后一個f代表“format”
int fscanf( FILE *restrict stream, const char *restrict format, ... );
// 按照format的格式從字串buffer中截取所需的資料并儲存在相應的變數中
// sscanf中的第一個s代表“string”,字串
int sscanf( const char *restrict buffer, const char *restrict format, ... );
// 回傳一個整型數值,代表成功根據格式賦值的變數數(arguments)
最常到的輸入流問題
先來個不會出問題的示例:
#include <stdio.h>
int main()
{
char test1[200];
char test2[200];
char testChar;
printf("Input a Character: \n");
testChar = getchar();
fprintf(stdout, "Input String1: \n");
scanf("%s", test1);
fprintf(stdout, "Input String2: \n");
scanf("%s", test2);
printf("Got String1: [ %s ]\n", test1);
printf("Got String2: [ %s ]\n", test2);
printf("Got Char: [ %c ]\n", testChar);
return 0;
}
運行效果:

出問題的示例:
#include <stdio.h>
int main()
{
char test[200];
char testChar1, testChar2, testChar3;
fprintf(stdout, "Input String: \n");
scanf("%3s", test);
printf("[1]Input a Character: \n");
testChar1 = getchar();
printf("[2]Input a Character: \n");
testChar2 = fgetc(stdin);
printf("[3]Input a Character: \n");
testChar3 = getchar();
printf("Got String: [ %s ]\n", test);
printf("Got Char1: [ %c ]\n", testChar1);
printf("Got Char2: [ %c ]\n", testChar2);
printf("Got Char3: [ %c ]\n", testChar3);
return 0;
}
運行效果:

因為我將格式設定為了%3s,所以scanf最多接收包含三個字符的字串,
在這個示例中,我按要求輸入了一條字串Hello,并按下回車輸入一個換行符,緩沖區資料Hello\n被寫入到了stdin中,而scanf只從標準流stdin中讀走了Hel這一部分字串,
此時,標準流stdin中實際上還剩3個字符:
lo\n(回車輸入的換行符)
于是接下來三次針對字符的輸入函式只會分別從stdin中取走這三個字符,而不會等待用戶輸入,這就沒有達到我想要的效果,
在基本的命令列程式中很容易遇到這類問題,這也是為什么需要及時清空輸入流stdin中的資料,
如何處理殘余內容
?? 以下內容假設stdout和stdin兩個標準流都是在行緩沖模式下的,
標準輸出流stdout
雖然本文主要是寫輸入流,但這里我還是掠過一下標準輸出流stdout,C語言標準庫中提供了一個用于重繪輸出流緩沖區的函式:
int fflush( FILE *stream );
// 如果成功了,回傳0,否則回傳EOF(-1)
要清空標準輸出流對應的緩沖區,只需要使用fflush(stdout)即可,上面的這個例子可以修改成這樣:
#include <stdio.h>
int main()
{
// 設定緩沖模式為行緩沖,緩沖區大小為10位元組
setvbuf(stdout, NULL, _IOLBF, 10);
fprintf(stdout, "1234567"); // 這里先向stdout對應的緩沖區中寫入了7位元組
fflush(stdout); // 重繪緩沖區,將緩沖區中的資料寫入到標準輸出流中
getchar(); // 這里等待用戶輸入
printf("89"); // 再向stdout對應的緩沖區中寫入了2位元組
fflush(stdout);
getchar(); // 接著等待用戶輸入
printf("Print!"); // 再向stdout對應的緩沖區中寫入了6位元組
getchar(); // 最后再等待一次用戶輸入
return 0;
}
運行效果:

可以看到,加入fflush(stdout)后,輸出緩沖區的內容會被及時寫入stdout中,繼而列印到螢屏上,
值得注意的是,fflush(stdin)的行為是未定義(不確定)的:
For input streams (and for update streams on which the last operation was input), the behavior is undefined.
不同平臺的編譯器對此有不同的解釋,
-
比如在Windows平臺上,無論是
VC6.0這種目前一些學校教學還在使用的古董編譯器,還是gcc 8.x.x,大體還是支持通過這種操作清空輸入流的, -
但是在Linux平臺上的
gcc編譯器就不買賬了,是不支持fflush(stdin)這種操作的,
因此,盡量避免fflush(stdin)這種寫法,這十分不利于代碼的可移植性,
標準輸入流stdin
上面提到因為可移植性要避免fflush(stdin)這種寫法,接下來記錄一下可移植性高的寫法,
接受格式化輸入時去除多余空白符
這一種其實用的比較少,但我覺得還是得記一下,
whitespace characters: any single whitespace character in the format string consumes all available consecutive whitespace characters from the input. Note that there is no difference between "\n", " ", "\t\t", or other whitespace in the format string.
上面這段解釋來自于cppreference,也就是說,格式化字串中的空白符(如"\n", " ", "\t\t")會吸收輸入字串中的一段連續的空白符,
也就是說,下面這句格式化輸入函式:
scanf(" %c %c",&recvChar1,&recvChar2);
可以從stdin中讀取形如 \n a b, \t a b這樣的資料,其中a之前的空白符和a與b之間的空白符都會被吸收,scanf得以能準確獲取字符a和b,
依靠這個特性,我們可以在接收輸入時自動剔除stdin中殘留的空白符:
// 因為格式%s不會匹配多余的空白符,這里按回車后,stdin中會殘留一個換行符\n
scanf("%s",recvStr);
// 在格式%c前加一個空格,可以吸收掉上面殘留的換行符\n,程式便能如預期接受用戶輸入
scanf(" %c",&recvChar);
然而,這一種方法僅只能剔除多余的空白符,
使用中括號字符集
這個解決方法可以和上面剔除空白符的方法進行結合,
格式化輸入有一個說明符 %[set],它的功能和正則運算式中的中括號[ ]十分類似:
-
其中
set代表一個用于匹配的字符集,一般情況下匹配的是存在字符集中的字符 -
字符集的第一個字符如果是
^,則表示取反,匹配的是不存在于該字符集中的字符 -
可以在中括號中使用短橫線
-來表達一個范圍,比如%[0-9]代表匹配0-9之間的字符,值得注意的是,對于短橫線-,可能在不同編譯器之間有不同實作,它是implementation-defined的,
另還有一個說明符 * ,它被稱為賦值抑制或賦值屏蔽符,如字面意思,在%引導的格式轉換字串中如果包含*,這個格式匹配的內容不會被賦給任何變數,
于是,可以給出如下的陳述句:
// 星號 * 代表不會把匹配到的內容賦給變數,相當于“吸收”掉了
// [^\n] 代表除了換行符外一律匹配
scanf("%*[^\n]");
因為用戶結束一次輸入的標志通常是按回車輸入一個換行符,殘留的內容往往末尾是一個換行符,上面這句的原理就是吸收掉stdin中所有的殘余字符,直至達到最后一個字符,也就是換行符,
然而,換行符不會被上面這句所吸收,所以在接下來的輸入中只需要忽略stdin中的殘余空白符即可(換行符就是空白符之一):
scanf("%*[^\n]");
scanf(" %c",&recvChar);
這種方法已經可以解決一般情況下的輸入殘余問題,不過在后續接受格式化輸入時還得忽略換行符\n,還是有點麻煩,
回圈取走殘余字符
這一種方法能在清除殘余時順便吸收掉末尾的換行符\n,
取字符需要用到取單個字符的輸入函式,這里為了方便,選用的是getchar(),
一般情況下可以這樣寫:
// getchar() 會從 stdin 中取走一個字符
while(getchar() != '\n')
;
(使用前提:stdin中有殘余)
while回圈會一直進行,直至getchar()取到的字符為換行符\n為止,這樣就可以順帶吸收掉末尾的換行符了,能相對完美地清除掉stdin中的殘余內容,
(在行緩沖模式下,用戶的一次輸入通常以一個換行符結束)
不過咧,還可以考慮更周全點,在getchar()獲取字符失敗的時候會回傳EOF,但此時并不滿足while回圈的退出條件,對此可以再完善一下:
// 臨時儲存字符
// 之所以是整型(int),是因為EOF是一個代表 負值整型(通常為-1) 的宏
int tempChar;
// tempChar=getchar()這種賦值陳述句本身的回傳值就是所賦的值
while ((tempChar = getchar()) != '\n' && tempChar != EOF)
;
這樣一來,當getchar()失敗時,程式執行就會跳出回圈,
綜上,針對stdin中的殘余內容的清除,最建議采用的便是最后這種處理方法,
不過其他的方法也是可以在一些場景中使用的,這就見仁見智了...
什么時候會回傳EOF
這里提一個題外的點:什么時候getchar()會回傳EOF?再進一步想,什么時候程式會認為標準流stdin達到了檔案流末尾?
實際上,這里的EOF往往是用戶輸入的一個特殊二進制值[3],輸入方式:
-
在Windows系統下是 Ctrl + Z(F6應該也行)
-
在Linux下是 Ctrl + D
當用戶在輸入中發送EOF時,標準流stdin就會被標記為EOF,因此getchar()就會獲取字符失敗而回傳EOF,
// 測驗用代碼
#include <stdio.h>
int main()
{
char testChar;
fprintf(stdout, "Input Char: \n");
testChar = getchar();
if (testChar == EOF)
{
printf("Received EOF\n");
}
else
{
printf("Received a char\n");
}
return 0;
}
EOF在C語言中是一個宏,定義在頭檔案stdio.h中,其值為一個負值的整型(并不一定是 -1),因此上面用tempChar != EOF來判斷getchar()失敗,
處理殘余的陳述句放在哪里
現在咱已經搞清楚了清除殘余的代碼,那么這些代碼該放在哪呢?
對于標準輸出流stdout來說,fflush陳述句往往放在輸出函式執行完成之后,以立刻將輸出內容列印到螢屏上:
printf("Hello ");
printf("World!\n");
fflush(stdout);
當然,如果嫌麻煩可以在輸出前直接通過setbuf關閉stdout的緩沖:
setbuf(stdout, NULL);
對于標準輸入流stdin來說,處理殘余的陳述句往往放在每次輸入函式執行之后,以及時清理流中殘余內容:
int c;
char testChar1, testChar2;
scanf("%*s"); // * 用于屏蔽賦值
while ((c = getchar()) != '\n' && c != EOF)
;
testChar1 = getchar();
while ((c = getchar()) != '\n' && c != EOF)
;
scanf("%c", &testChar2);
當然,這樣就顯得有點冗余了,
實際上可以將清除的陳述句封裝進函式或者定義為宏(不過確實不太建議定義為宏),這樣也更便于維護,
總結
之前瀏覽了很多相關文章,標題和內容大多都寫著“清空輸入緩沖區”,現在想一下,這樣寫可能是不對的,因為實際我清空的是標準輸入流stdin中的殘留內容,在用戶輸入完成(輸入換行符)的那一刻,輸入緩沖區實際上就已經被清空了,
也就是說,標準流和對應的緩沖區要辨別清楚,二者不是同一個概念(一個stream一個buffer),千萬不能混淆了,

最后,感謝你看到這里~
本筆記可能還是有錯誤出現,也請各位多指教!
參考文獻
本筆記相關:
-
File input/output - cppreference.com
-
Clarify the difference between input/output stream and input/output buffer - StackOverflow
-
End of File in stdin - StackOverflow
關于"implementation-defined"
- Undefined, unspecified and implementation-defined behavior - StackOverflow
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/499332.html
標籤:其他
上一篇:Python基礎day23:絕對匯入和相對匯入、模塊化編程簡介、軟體開發目錄規范、常見內置模塊collections和time
