簡(jiǎn)記C語(yǔ)言清空輸入殘留內(nèi)容
先從緩沖區(qū)說(shuō)起。
緩沖區(qū)是內(nèi)存中劃分出來(lái)的一部分。通常來(lái)說(shuō),緩沖區(qū)類型有三種:
全緩沖
行緩沖
無(wú)緩沖
行緩沖#
在C語(yǔ)言中緩沖區(qū)這個(gè)概念的存在感還是挺強(qiáng)的,比較常用到的緩沖區(qū)類型則是行緩沖了,如標(biāo)準(zhǔn)輸入流?stdin
?和標(biāo)準(zhǔn)輸出流?stdout
一般(終端環(huán)境下)就是在行緩沖模式下的。
行緩沖,顧名思義,就是針對(duì)該緩沖區(qū)的I/O操作是基于行的。
在遇到換行符前,程序的輸入和輸出都會(huì)先被暫存到流對(duì)應(yīng)的緩沖區(qū)中
而在遇到換行符后(或者緩沖區(qū)滿了),程序才會(huì)進(jìn)行真正的I/O操作,將該緩沖區(qū)中的數(shù)據(jù)寫到對(duì)應(yīng)的流?(stream) 中以供后續(xù)讀取。
就標(biāo)準(zhǔn)輸入stdin
而言,用戶的輸入首先會(huì)被存到相應(yīng)的輸入緩沖區(qū)中,每當(dāng)用戶按下回車鍵輸入一個(gè)換行符,程序才會(huì)進(jìn)行I/O操作,將緩沖區(qū)暫存的數(shù)據(jù)寫入到stdin
中,以供輸入函數(shù)使用。
而對(duì)標(biāo)準(zhǔn)輸出stdout
來(lái)說(shuō),輸出內(nèi)容也首先會(huì)被暫存到相應(yīng)的輸出緩沖區(qū)中,每當(dāng)輸出數(shù)據(jù)遇到換行符時(shí),程序才會(huì)將緩沖區(qū)中的數(shù)據(jù)寫入stdout
,繼而打印到屏幕上。
這也是為什么在緩沖模式下,輸出的內(nèi)容不會(huì)立即打印到屏幕上:
int main(){ // 設(shè)置緩沖模式為行緩沖,緩沖區(qū)大小為10字節(jié) setvbuf(stdout, NULL, _IOLBF, 10); fprintf(stdout, "1234567"); // 這里先向stdout對(duì)應(yīng)的緩沖區(qū)中寫入了7字節(jié) getchar(); // 這里等待用戶輸入 printf("89"); // 再向stdout對(duì)應(yīng)的緩沖區(qū)中寫入了2字節(jié) getchar(); // 接著等待用戶輸入 printf("Print!"); // 再向stdout對(duì)應(yīng)的緩沖區(qū)中寫入了6字節(jié) getchar(); // 最后再等待一次用戶輸入 return 0;}
運(yùn)行效果:
可以看到,直到執(zhí)行到第二個(gè)getchar()
時(shí),屏幕上沒(méi)有新的輸出。
而在執(zhí)行了printf("Print!")
之后,輸出緩沖區(qū)被填滿了,輸出緩沖區(qū)中現(xiàn)有的10
字節(jié)的數(shù)據(jù)被寫入到stdout
中,繼而才在屏幕上打印出123456789P
。
緩沖區(qū)內(nèi)容被讀走后,剩余的字符串rint!
接著被寫入輸出緩沖區(qū)。程序運(yùn)行結(jié)束后,輸出緩沖區(qū)中的內(nèi)容會(huì)被全部打印到屏幕上,所以會(huì)在最后看到rint!
。
C語(yǔ)言中常用的輸入函數(shù)#
輸入函數(shù)做的工作主要是從文件流中讀取數(shù)據(jù),亦可將讀取到的數(shù)據(jù)儲(chǔ)存到內(nèi)存中以供后續(xù)程序使用。
基于字符#
// 從給定的文件流中讀一個(gè)字符 (fgetc中的 f 的意思即"function")int fgetc( FILE *stream );
// 同fgetc,但是getc的實(shí)現(xiàn)*可能*是基于宏的int getc( FILE *stream );
// 相當(dāng)于是getc(stdin),從標(biāo)準(zhǔn)輸入流讀取一個(gè)字符int getchar(void);// 返回獲取的字符的ASCII碼值,如果到達(dá)文件末尾就返回EOF(即返回-1)
基于行#
// 從給定的文件流中讀取(count-1)個(gè)字符或者讀取直到遇到換行符或者EOF// fgets中的f代表“file”,而s代表“string”char *fgets( char *restrict str, int count, FILE *restrict stream );// 返回指向字符串的指針或者空指針NULL
格式化輸入#
// 按照f(shuō)ormat的格式從標(biāo)準(zhǔn)輸入流stdin中讀取所需的數(shù)據(jù)并儲(chǔ)存在相應(yīng)的變量中// scanf中的f代表“format”int scanf( const char *restrict format, ... );// 按照f(shuō)ormat的格式從文件流stream中讀取所需的數(shù)據(jù)并儲(chǔ)存在相應(yīng)的變量中// fscanf中前一個(gè)f代表“file(stream)”,后一個(gè)f代表“format”int fscanf( FILE *restrict stream, const char *restrict format, ... );// 按照f(shuō)ormat的格式從字符串buffer中截取所需的數(shù)據(jù)并儲(chǔ)存在相應(yīng)的變量中// sscanf中的第一個(gè)s代表“string”,字符串int sscanf( const char *restrict buffer, const char *restrict format, ... );// 返回一個(gè)整型數(shù)值,代表成功根據(jù)格式賦值的變量數(shù)(arguments)
最常到的輸入流問(wèn)題#
先來(lái)個(gè)不會(huì)出問(wèn)題的示例:
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;}
運(yùn)行效果:
出問(wèn)題的示例:
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;}
運(yùn)行效果:
因?yàn)槲覍⒏袷皆O(shè)置為了%3s
,所以scanf
最多接收包含三個(gè)字符的字符串。
在這個(gè)示例中,我按要求輸入了一條字符串Hello
,并按下回車輸入一個(gè)換行符,緩沖區(qū)數(shù)據(jù)Hello\n
被寫入到了stdin
中。而scanf
只從標(biāo)準(zhǔn)流stdin
中讀走了Hel
這一部分字符串。
此時(shí),標(biāo)準(zhǔn)流stdin
中實(shí)際上還剩3個(gè)字符:
l
o
\n
?(回車輸入的換行符)
于是接下來(lái)三次針對(duì)字符的輸入函數(shù)只會(huì)分別從stdin
中取走這三個(gè)字符,而不會(huì)等待用戶輸入,這就沒(méi)有達(dá)到我想要的效果。
在基本的命令行程序中很容易遇到這類問(wèn)題,這也是為什么需要及時(shí)清空輸入流stdin
中的數(shù)據(jù)。
如何處理殘余內(nèi)容#
?? 以下內(nèi)容假設(shè)stdout
和stdin
兩個(gè)標(biāo)準(zhǔn)流都是在行緩沖模式下的。
標(biāo)準(zhǔn)輸出流stdout#
雖然本文主要是寫輸入流,但這里我還是掠過(guò)一下標(biāo)準(zhǔn)輸出流stdout
。C語(yǔ)言標(biāo)準(zhǔn)庫(kù)中提供了一個(gè)用于刷新輸出流緩沖區(qū)的函數(shù):
int fflush( FILE *stream );// 如果成功了,返回0,否則返回EOF(-1)
要清空標(biāo)準(zhǔn)輸出流對(duì)應(yīng)的緩沖區(qū),只需要使用fflush(stdout)
即可。上面的這個(gè)例子可以修改成這樣:
int main(){ // 設(shè)置緩沖模式為行緩沖,緩沖區(qū)大小為10字節(jié) setvbuf(stdout, NULL, _IOLBF, 10); fprintf(stdout, "1234567"); // 這里先向stdout對(duì)應(yīng)的緩沖區(qū)中寫入了7字節(jié) fflush(stdout); // 刷新緩沖區(qū),將緩沖區(qū)中的數(shù)據(jù)寫入到標(biāo)準(zhǔn)輸出流中 getchar(); // 這里等待用戶輸入 printf("89"); // 再向stdout對(duì)應(yīng)的緩沖區(qū)中寫入了2字節(jié) fflush(stdout);
getchar(); // 接著等待用戶輸入 printf("Print!"); // 再向stdout對(duì)應(yīng)的緩沖區(qū)中寫入了6字節(jié) getchar(); // 最后再等待一次用戶輸入 return 0;}
運(yùn)行效果:
可以看到,加入fflush(stdout)
后,輸出緩沖區(qū)的內(nèi)容會(huì)被及時(shí)寫入stdout
中,繼而打印到屏幕上。
值得注意的是,fflush(stdin)
的行為是未定義(不確定)的:
For input streams (and for update streams on which the last operation was input), the behavior is undefined.
不同平臺(tái)的編譯器對(duì)此有不同的解釋。
比如在Windows平臺(tái)上,無(wú)論是
VC6.0
這種目前一些學(xué)校教學(xué)還在使用的古董編譯器,還是gcc 8.x.x
,大體還是支持通過(guò)這種操作清空輸入流的。但是在Linux平臺(tái)上的
gcc
編譯器就不買賬了,是不支持fflush(stdin)
這種操作的。
因此,盡量避免fflush(stdin)
這種寫法,這十分不利于代碼的可移植性。
標(biāo)準(zhǔn)輸入流stdin#
上面提到因?yàn)榭梢浦残砸苊?code>fflush(stdin)這種寫法,接下來(lái)記錄一下可移植性高的寫法。
接受格式化輸入時(shí)去除多余空白符#
這一種其實(shí)用的比較少,但我覺(jué)得還是得記一下。
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.
上面這段解釋來(lái)自于cppreference,也就是說(shuō),格式化字符串中的空白符(如"\n"
,?" "
,?"\t\t"
)會(huì)吸收輸入字符串中的一段連續(xù)的空白符。
也就是說(shuō),下面這句格式化輸入函數(shù):
scanf(" %c %c",&recvChar1,&recvChar2);
可以從stdin
中讀取形如\n a b
,\t a b
這樣的數(shù)據(jù)。其中a
之前的空白符和a
與b
之間的空白符都會(huì)被吸收,scanf
得以能準(zhǔn)確獲取字符a
和b
。
依靠這個(gè)特性,我們可以在接收輸入時(shí)自動(dòng)剔除stdin
中殘留的空白符:
// 因?yàn)楦袷?s不會(huì)匹配多余的空白符,這里按回車后,stdin中會(huì)殘留一個(gè)換行符\nscanf("%s",recvStr);// 在格式%c前加一個(gè)空格,可以吸收掉上面殘留的換行符\n,程序便能如預(yù)期接受用戶輸入scanf(" %c",&recvChar);
然而,這一種方法僅只能剔除多余的空白符。
使用中括號(hào)字符集#
這個(gè)解決方法可以和上面剔除空白符的方法進(jìn)行結(jié)合。
格式化輸入有一個(gè)說(shuō)明符?%[set]
,它的功能和正則表達(dá)式中的中括號(hào)[ ]
十分類似:
其中
set
代表一個(gè)用于匹配的字符集,一般情況下匹配的是存在字符集中的字符字符集的第一個(gè)字符如果是
^
,則表示取反,匹配的是不存在于該字符集中的字符可以在中括號(hào)中使用短橫線?
-
?來(lái)表達(dá)一個(gè)范圍,比如%[0-9]
代表匹配0-9之間的字符。值得注意的是,對(duì)于短橫線-
,可能在不同編譯器之間有不同實(shí)現(xiàn),它是implementation-defined的。
另還有一個(gè)說(shuō)明符?*
?,它被稱為賦值抑制或賦值屏蔽符。如字面意思,在%
引導(dǎo)的格式轉(zhuǎn)換字串中如果包含*
,這個(gè)格式匹配的內(nèi)容不會(huì)被賦給任何變量。
于是,可以給出如下的語(yǔ)句:
// 星號(hào) * 代表不會(huì)把匹配到的內(nèi)容賦給變量,相當(dāng)于“吸收”掉了// [^\n] 代表除了換行符外一律匹配scanf("%*[^\n]");
因?yàn)橛脩艚Y(jié)束一次輸入的標(biāo)志通常是按回車輸入一個(gè)換行符,殘留的內(nèi)容往往末尾是一個(gè)換行符。上面這句的原理就是吸收掉stdin
中所有的殘余字符,直至達(dá)到最后一個(gè)字符,也就是換行符。
然而,換行符不會(huì)被上面這句所吸收,所以在接下來(lái)的輸入中只需要忽略stdin
中的殘余空白符即可(換行符就是空白符之一):
scanf("%*[^\n]");scanf(" %c",&recvChar);
這種方法已經(jīng)可以解決一般情況下的輸入殘余問(wèn)題,不過(guò)在后續(xù)接受格式化輸入時(shí)還得忽略換行符\n
,還是有點(diǎn)麻煩。
循環(huán)取走殘余字符#
這一種方法能在清除殘余時(shí)順便吸收掉末尾的換行符\n
。
取字符需要用到取單個(gè)字符的輸入函數(shù),這里為了方便,選用的是getchar()
。
一般情況下可以這樣寫:
// getchar() 會(huì)從 stdin 中取走一個(gè)字符while(getchar() != '\n') ;
(使用前提:stdin
中有殘余)
while
循環(huán)會(huì)一直進(jìn)行,直至getchar()
取到的字符為換行符\n
為止,這樣就可以順帶吸收掉末尾的換行符了,能相對(duì)完美地清除掉stdin
中的殘余內(nèi)容。
(在行緩沖模式下,用戶的一次輸入通常以一個(gè)換行符結(jié)束)
不過(guò)咧,還可以考慮更周全點(diǎn)。在getchar()
獲取字符失敗的時(shí)候會(huì)返回EOF
,但此時(shí)并不滿足while
循環(huán)的退出條件,對(duì)此可以再完善一下:
// 臨時(shí)儲(chǔ)存字符// 之所以是整型(int),是因?yàn)镋OF是一個(gè)代表 負(fù)值整型(通常為-1) 的宏int tempChar;// tempChar=getchar()這種賦值語(yǔ)句本身的返回值就是所賦的值while ((tempChar = getchar()) != '\n' && tempChar != EOF) ;
這樣一來(lái),當(dāng)getchar()
失敗時(shí),程序執(zhí)行就會(huì)跳出循環(huán)。
綜上,針對(duì)stdin
中的殘余內(nèi)容的清除,最建議采用的便是最后這種處理方法。
不過(guò)其他的方法也是可以在一些場(chǎng)景中使用的,這就見(jiàn)仁見(jiàn)智了...
什么時(shí)候會(huì)返回EOF#
這里提一個(gè)題外的點(diǎn):什么時(shí)候getchar()
會(huì)返回EOF
?再進(jìn)一步想,什么時(shí)候程序會(huì)認(rèn)為標(biāo)準(zhǔn)流stdin
達(dá)到了文件流末尾?
實(shí)際上,這里的EOF
往往是用戶輸入的一個(gè)特殊二進(jìn)制值[3],輸入方式:
在Windows系統(tǒng)下是?Ctrl +?Z(F6應(yīng)該也行)
在Linux下是?Ctrl +?D
當(dāng)用戶在輸入中發(fā)送EOF
時(shí),標(biāo)準(zhǔn)流stdin
就會(huì)被標(biāo)記為EOF
,因此getchar()
就會(huì)獲取字符失敗而返回EOF
。
// 測(cè)試用代碼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語(yǔ)言中是一個(gè)宏,定義在頭文件stdio.h
中,其值為一個(gè)負(fù)值的整型(并不一定是?-1
),因此上面用tempChar != EOF
來(lái)判斷getchar()
失敗。
處理殘余的語(yǔ)句放在哪里#
現(xiàn)在咱已經(jīng)搞清楚了清除殘余的代碼,那么這些代碼該放在哪呢?
對(duì)于標(biāo)準(zhǔn)輸出流stdout
來(lái)說(shuō),fflush
語(yǔ)句往往放在輸出函數(shù)執(zhí)行完成之后,以立刻將輸出內(nèi)容打印到屏幕上:
printf("Hello ");printf("World!\n");fflush(stdout);
當(dāng)然,如果嫌麻煩可以在輸出前直接通過(guò)setbuf
關(guān)閉stdout
的緩沖:
setbuf(stdout, NULL);
對(duì)于標(biāo)準(zhǔn)輸入流stdin
來(lái)說(shuō),處理殘余的語(yǔ)句往往放在每次輸入函數(shù)執(zhí)行之后,以及時(shí)清理流中殘余內(nèi)容:
int c;char testChar1, testChar2;scanf("%*s"); // * 用于屏蔽賦值while ((c = getchar()) != '\n' && c != EOF) ;testChar1 = getchar();while ((c = getchar()) != '\n' && c != EOF) ;scanf("%c", &testChar2);
當(dāng)然,這樣就顯得有點(diǎn)冗余了。
實(shí)際上可以將清除的語(yǔ)句封裝進(jìn)函數(shù)或者定義為宏(不過(guò)確實(shí)不太建議定義為宏),這樣也更便于維護(hù)。
總結(jié)#
之前瀏覽了很多相關(guān)文章,標(biāo)題和內(nèi)容大多都寫著“清空輸入緩沖區(qū)”?,F(xiàn)在想一下,這樣寫可能是不對(duì)的,因?yàn)閷?shí)際我清空的是標(biāo)準(zhǔn)輸入流stdin
中的殘留內(nèi)容。在用戶輸入完成(輸入換行符)的那一刻,輸入緩沖區(qū)實(shí)際上就已經(jīng)被清空了。
也就是說(shuō),標(biāo)準(zhǔn)流和對(duì)應(yīng)的緩沖區(qū)要辨別清楚,二者不是同一個(gè)概念(一個(gè)stream
一個(gè)buffer
),千萬(wàn)不能混淆了。