標准I\O的緩沖類型
標准I\O根據不同的應用需求,提供了全緩沖、行緩沖、無緩沖三種緩沖方式。
全緩沖:只有當划定的緩沖區被填滿或者數據讀取至末尾時,才開始執行 I\O 操作(執行系統提供的 read\write 操作)。磁盤文件的讀寫一般采用這種方式。
行緩沖:當輸入輸出過程遇到換行符''\n"或者當分配緩沖區已滿時,才開始執行 I\O 操作。一般涉及終端的讀寫操作如 stdin 與 stdout 使用這種緩沖方式。
無緩沖:當有數據產生時,馬上由相應的設備進行處理。一般來說 stderr(standard error) 使用這種緩沖方式,使得有錯誤信息時馬上能夠得到響應。需要注意的是,標准庫不緩存並不意味着操作系統或者設備驅動不緩存。
注意,以上關於 stdin/stdout 的緩沖方式並不是直接規定死的。一些語言的語言規范會對緩沖實現給出一定的限制,但並不具體,只是許多標准I/O是以上述方式實現的而已。可以參考關於流和緩沖區。
行緩沖
標准輸入緩沖區 stdin 使用行緩沖的方式存儲輸入。用戶的輸入數據首先被暫存在臨時緩沖區中,當用戶鍵入回車鍵或臨時緩沖區滿后,stdin 才進行 I/O 操作,將數據由臨時緩沖區拷貝至 stdin 中。C語言提供的輸入輸出函數如 scanf 、getchar 等則從上述緩沖區 stdin 中讀取數據輸入。
scanf 和 getchar 等函數會在 stdin 中讀取數據,若上述緩沖區中已存在數據,則直接讀取其中的數據,若上述緩沖區為空,則上述函數會掛起,等待數據緩沖的完成( 用戶輸入回車鍵或數據緩沖區滿后, stdin 會進行數據緩沖,之后上述函數才能繼續執行)。 用戶一次輸入的數據可能會超過 scanf 、getchar 等函數調用所需要的數據,那么所需數據被讀取后,剩余的數據仍會存放在緩沖區中,之后的函數調用會直接讀取 stdin 中已有的數據。只有當緩沖區為空后,scanf 等函數才會等待用戶輸入(實際應該是等待 stdin 的緩沖。
scanf函數
scanf函數: scanf C++ reference
函數聲明:int scanf( format string , arg1 , arg2 , ...);
從函數聲明可以看到,scanf 的參數由指示讀取動作的格式化字符串( format string )和相應的地址參數 arg1...argn 組成。scanf 函數將輸入從標准輸入緩沖區 stdin 中讀入,並將它們以格式化字符串中指定的格式存儲到額外的參數 arg1...arg2 等指定的內存空間中。其中額外的參數(additional argument)指向的內存空間的數據類型應該與格式化字符串中指定的數據類型相一致。
格式化字符串(format string)
格式化字符串規定了 scanf 等函數如何從輸入緩沖 stdin 中讀取數據,其組成字符的含義如下所示:
(1)空白字符(whitespace)。scanf 會讀取並忽略在 stdin 中下一個非空白字符之前的所有空白字符(空格、換行和 tab),然后讀取格式化字符串中規定格式的數據。若格式化字符串中包含空白字符,則該空白字符會與輸入緩沖區中任意數量的連續空白字符相匹配,並將其從緩沖區中清除(包括0個)。例如格式化字符串"%d %d",會要求 scanf 首先從緩沖區中讀取一個整型(若之前存在空白字符則跳過),再跳過輸入緩沖區中連續的空白字符(與格式化字符串中的空白字符匹配),最后再讀取一個整形;
(2)非空白字符(non whitespace)。對於格式化字符串中既非空白字符又不是格式說明符(format specifier,由%標識)的一部分的字符,scanf 會嘗試從 stdin 中讀取輸入,並將輸入與該字符比較,若匹配,則繼續進行后續讀取,若不匹配,則函數返回錯誤信息;
(3)格式說明符。以 % 開頭的用於指定輸入數據格式的字符。如 %d 指定需要讀取一個整形,%s 需要讀取一個字符串。scanf 等函數首先根據格式說明符嘗試去解析 stdin 中的數據,如對於 %d ,scanf 會嘗試對 stdin 中已有數據以整型的格式進行解析。若解析成功,則將上述解析結果存放到指定的內存中,若解析失敗,如 stdin 中僅存在一個字符 'a',scanf 會退出並返回,但是上述不匹配的數據並不會從緩沖區中清除,后續的 scanf 調用仍從上述輸入開始讀取;
由以上3條規則,通過設置格式化字符串可以規定了 scanf 函數的行為。下面為示例:
scanf("%s,%d",&a,&b); //scanf需先讀取一個字符串,再讀取一個 ','(規則2),最后讀取一個整數
scanf("%d\t%d",&a,&b); //scanf需先讀取一個整數,再將格式化字符串中的 '\t' (空白字符)與緩沖區中0個或多個空白字符匹配並清除(規則1),最后讀取一個整數
scanf("%d%d",&a,&b); //scanf需要先讀取一個整數,之后再讀取一個整數,兩個整數之間的空白字符會被忽略(規則1)
字符和字符串的讀取
對於 stdin 中的字符的讀取,scanf 、 getchar 等函數會讀取緩沖區中的第一個字符,包括空白字符和非空白字符。
對於 stdin 中的字符串的讀取,scanf 會在開始處理后(跳過第一個非空白字符之前的空白字符,規則1)讀取到的第一個空白字符處退出,並在讀取的字符串尾部加入'\0'作為結束標志。
緩沖區讀取數據問題示例
例1:
程序先輸出變量未初始化之前的值,再使用scanf讀取輸入,再顯示讀取輸入之后的值
printf("%d,%d,%c\n",a,b,c); //輸出未初始化之前的值
scanf("%d%d",&a,&b); scanf("%c",&c); printf("%d,%d,%c\n",a,b,c); //輸出初始化之后的值
結果如下圖所示

解釋如下:
(1)用戶輸入至緩沖區中的數據實際為 12 + 空格 + a + 換行符 ;
(2)第一次讀取輸入時,首先將讀取到的第一個數字12賦值給變量 a,之后 scanf 會試圖讀取下一個十進制數,但是發現下一個非空白字符(忽略輸入的空格)為字符 'a',與其所需要讀取的數據類型不符,scanf 會退出並返回一個常數值來表示錯誤信息.此時字符 'a' 並未被讀取,仍然存在於緩沖區中;
(3)第二次讀取輸入時,scanf 就會發現緩沖區中第一個非空白字符為字符 'a',從而會將字符 'a' 賦值給變量 c,並退出。
故而,再次輸出變量時,變量 a 和 c 均已改變,而變量 b 只能保持原值。
例2:
用於測試的函數先讀取一個字符串,再讀取一個字符,並將結果輸出
scanf("%s",a); scanf("%c",&b); printf("%s,%d",a,(int)b);
輸出結果如下

解釋如下:
(1)用戶在輸入時,實際進入緩沖區中的數據為字符串'"abcd" + 換行符;
(2)第一次讀取時,scanf 會讀取一個字符串,並在遇到第一個空白字符處停止,這里為換行符,即讀取的字符串為"abcd",scanf 函數還會在該字符串尾部加入'\0'進行存儲;
(3)第二次讀取時,scanf 會讀取一個字符,進行字符讀取時空白符也被視為有效輸入字符,故而 scanf 會讀取換行符,而換行符的ASCII值即為10;
例3:
將讀取輸入的要求換一下,要求讀取兩個字符串

結果scanf會再次等待用戶輸入

原因在於在讀取第一個字符串后,緩沖區中剩余一個換行符,而根據規則1,在讀取字符串之前會跳過所有的空白字符,之后scanf會發現此時緩沖區已經為空,從而需要再次等待用戶輸入。
事實上,對於上述情況,除非第二次讀取的參數是可以讀取空白字符的 %c,其他的參數均會使得 scanf 認為緩沖區已為空,從而進入等待用戶輸入的狀態。
getchar
getchar 是用於字符輸入的C庫函數,其函數的聲明包含在頭文件 stdio.h,函數聲明為: int getchar(void).其功能是讀取標准輸入stdin中的一個字符。
getchar 從標准輸入中讀取數據,而 stdin 是采用行緩沖的方式記錄用戶輸入,也就是只有當用戶鍵入回車鍵或輸入至緩沖區末尾時,才會開始 I\O 操作,亦即讀取一個字符。這樣用戶可以一次輸入不止一個字符,讀取過后緩沖區可能不為空。當再次調用 getchar 時,若緩沖區不為空,getchar 就會直接讀取在緩沖區中字符,而不是等待用戶輸入。可以認為是getchar 等待的是行緩沖的完成,而不是用戶輸入的完成,在行緩沖完成后,只要緩沖區不為空,getchar 就可以讀取字符,而不需要等待用戶輸入。
/*codeblocks13.12*/ #include <stdio.h> int main(void) { char ch = '\0'; while(ch != '\n') { printf("輸入一個字符:"); ch = getchar(); printf("\n"); putchar(ch); printf("\n"); } return 0; }
程序的運行結果如下:

可以明顯看到,后續執行中並不要求用戶輸入,getchar()會直接讀取緩沖區中的數據。而且對於字符的讀取操作而言,換行符'\n'也被視為一個字符,而不是單純的結束標志。
等待用戶輸入的字符輸入
getchar 可以直接從緩沖區中讀取字符,而不等待用戶輸入,但這種方式也有可能帶來潛在的錯誤。這里給出兩種等待用戶輸入的字符傳入方式。
1.使用 getche 與 getch 函數。上述函數均從鍵盤上讀入一個字節,其中后者不會將字符回顯到屏幕上。以這兩個函數讀取字符時,都是通過調用函數讀取一個鍵盤輸入且只有一個。如調用 getche,鍵盤敲擊 'abc' 時,只有一個字符 'a' 會被讀取。其他字符為無效輸入。但上述函數並不被包含在標准 C 函數庫中,需要通過頭文件 conio.h 來使用,並不被所有的編譯器實現支持。
2.在每次調用 getchar 函數之后,手動對緩沖區進行清除操作。可以使用 fflush() 函數清理緩沖區。C標准規定 fflush()函數可用來刷新輸出(stdout)緩沖區(一般是將緩沖區數據寫回存儲設備)。但對於標准輸入(stdin)則沒有明確定義。部分編譯器定義了 fflush( stdin )的實現,如微軟的VC。也就是不同的編譯器對於 fflush( stdin )的支持可能不同。GCC編譯器沒有定義它的實現,所以不能使用 fflush( stdin )來刷新輸入緩沖區。
