1.為什麼 fflush(stdin)是錯的
首先請看以下程式:
#include <stdio.h>
int main( void )
{
int i;
for (;;) {
fputs("Please input an integer: ", stdout);
scanf("%d", &i);
printf("%d\n", i);
}
return 0;
}
這個程式首先會提示使用者輸入一個整數,然後等待使用者輸入,如果使用者輸入的是整數,程式會輸出剛才輸入的整數,並且再次提示使用者輸入一個整數,然後等待使用者輸入。但是一旦使用者輸入的不是整數(如小數或者字母),假設scanf函數最後一次得到的整數是2,那麼程式會不停地輸出“Please input an integer:
2”。這是因為scanf("%d", &i);只能接受整數,如果使用者輸入了字母,則這個字母會遺留在“輸入緩衝區”中。因為緩衝中有資料,故而scanf函數不會等待使用者輸入,直接就去緩衝中讀取,可是緩衝中的卻是字母,這個字母再次被遺留在緩衝中,如此反覆,從而導致不停地輸出“Please
input an integer: 2”。
也許有人會說:“居然這樣,那麼在scanf函數後面加上‘fflush(stdin);’,把輸入緩衝清空掉不就行了?”然而這是錯的!C和C++的標準裡從來沒有定義過fflush(stdin)。也許有人會說:“可是我用fflush(stdin)解決了這個問題,你怎麼能說是錯的呢?”的確,某些編譯器(如VC6)支援用fflush(stdin)來清空輸入緩衝,但是並非所有編譯器都要支援這個功能(linux
下的 gcc就不支援),因為標準中根本沒有定義fflush(stdin)。MSDN文檔裡也清楚地寫著fflush on input stream is anextension to
the C standard(fflush操作輸入資料流是對C標準的擴充)。當然,如果你毫不在乎程式的移植性,用fflush(stdin)也沒什麼大問題。以下是C99對fflush函數的定義:
int fflush(FILE *stream);
如果 stream指向輸出資料流或者更新流(update stream),並且這個更新流
最近執行的操作不是輸入,那麼 fflush 函數將把這個流中任何待寫資料傳送至
宿主環境(host environment)寫入檔案。否則,它的行為是未定義的。
原文如下:
fflush(FILE *stream);
If stream points to an output stream or an update stream in which
the most recentoperation was not input, the fflush function causes
any unwritten data for thatstream to be delivered to the host environment
to be written to the file;otherwise, the behavior is undefined.
其中,宿主環境可以理解為作業系統或核心等。
由此可知,如果stream指向輸入資料流(如stdin),那麼fflush函數的行為是不確定的。故而使用fflush(stdin) 是不正確的,至少是移植性不好的。
2.清空輸入緩衝區的方法
雖然不可以用fflush(stdin),但是我們可以自己寫代碼來清空輸入緩衝區。只需要在scanf函數後面加上幾句簡單的代碼就可以了。
#include <stdio.h>
int main( void )
{
int i, c;
for ( ; ; )
{
fputs("Please input an integer: ", stdout);
scanf("%d", &i);
if ( feof(stdin) || ferror(stdin) )
{
break;
}
while ( (c = getchar()) != '\n' && c != EOF ) ;
printf("%d\n", i);
}
return 0;
}
#include <iostream>
#include <limits> // 為了使用numeric_limits
using std::cout;
using std::endl;
using std::cin;
using std::numeric_limits;
using std::streamsize;
int main()
{
int value;
for ( ; ; )
{
cout << "Enter an integer: ";
cin >> value;
if ( cin.eof() || cin.bad() )
{ // 如果使用者輸入檔案結束標誌(或檔案已被讀完),
// 或者發生讀寫錯誤,則退出迴圈
// do something
break;
}
// 讀到非法字元後,輸入資料流將處於出錯狀態,
// 為了繼續擷取輸入,首先要調用 clear 函數
// 來清除輸入資料流的錯誤標記,然後才能調用
// ignore 函數來清除輸入資料流中的資料。
cin.clear();
// numeric_limits<streamsize>::max() 返回輸入緩衝的大小。
// ignore 函數在此將把輸入資料流中的資料清空。
// 這兩個函數的具體用法請讀者自行查詢。
cin.ignore( numeric_limits<streamsize>::max(), '\n' );
cout << value << '\n';
}
return 0;
}
這是我在別的論壇看到的!樓主文章的觀點不對!誤導人!!
1. 為什麼 fflush(stdin) 是錯的
-----------------------------------------
C和C++的標準裡從來沒有定義過 fflush(stdin)。
---------------------------------------------
錯誤,不能說fflush(stdin)是錯的。作者列出了標準的內容,這顯示作者的確有看過標準,但對標準的內容理解錯誤。標準指出fflush用於輸入資料流的結果是未定義的,但是未定義並不等於是錯誤!同時c和c++的標準也並非從來沒有定義過fflush(stdin),恰恰相反,標準說fflush用於輸入資料流的結果是未定義的本身就是對fflush(stdin)的定義!就是對fflush(stdin)提出的規定!只不過,其結果是未定義而已!
結論應該是:使用fflush(stdin)會產生移植性問題,是不良風格代碼,但不是錯誤。
作者所提出的解決方案:
if ( scanf("%d", &i) != EOF ) {
while ( (c=getchar()) != '\n' && c != EOF ) {
;
}
}
並沒有完全解決了問題,存在重大的漏洞。主要問題在於,使用getchar()這種方法並沒有清除EOF標誌。如果用tc2.0、tc2.01、tc3.0、tc3.1等等編譯器運行上述代碼,輸入時用ctrl+z結尾或者直接輸入ctrl+z,程式肯定會進入一個死迴圈!
原因就是getchar()方式並沒有清除EOF標誌,我在這裡所說的EOF標誌並非指函數返回的EOF,而是指當I/O函數遇到EOF時在其內部產生的EOF標誌。
偶推薦用rewind(stdin)這個方法,rewind不僅清除了stdin中的內容,還清除EOF標誌,用下列語句:
scanf("%d", &i);
rewind(stdin);
代替上述if語句,無論你如何輸入ctrl+z,都不會進入死迴圈,同時也簡單得多,是比較完美的解決方案。
首先感謝您的評論,它促使我重新審視了我這篇文章,並且修正了文中的一些錯漏。特別是文中的兩個程式,如果 stdin 被重新導向到檔案時,會出現死迴圈。現在我已經把這個問題修正了,就算 stdin 被重新導向到檔案,也不會出現死迴圈。如果本文還有其它不足之處,敬請指出,我將不吝感激!
然後,對樓上的一些觀點不敢苟同,在此發表一些淺見。
1. 按照樓上對錯誤的定義,我說 fflush(stdin) 是錯的的確是錯了。不過,每個人對錯誤的理解都不一樣。我認為,如果某種功能明明可以用標準代碼實現,而放著不用,或者不會用,卻依賴編譯器/系統特定的功能實現,這就是錯誤。當然,這隻是我的看法。還有,我覺得使用編譯器/系統特定的功能(如 fflush(stdin);)不算不良風格代碼。我認為不良風格是指代碼一大堆一大堆地堆放在一起,沒有認真地縮排,也缺乏注釋,代碼層次不清晰明了,功能模組分工不細,等等。另外,對樓上“標準說fflush用於輸入資料流的結果是未定義的本身就是對fflush(stdin)的定義”這個見解非常欽佩。我覺得這個見解別樹一格,非常獨到,新穎。樓上的腦筋真靈!我就從來沒想過這點,慚愧!
2. 我的方案的確存在問題,謝謝你的指出。但問題並不是你所說的那樣,而是出在重新導向上。如果 stdin 被重新導向到檔案,我原來的程式的確會導致死迴圈。
樓上說“輸入時用ctrl+z結尾或者直接輸入ctrl+z,程式肯定會進入一個死迴圈!”,我用 TC 測試過了,直接輸入 ctrl+z 不會死迴圈,但是輸入一些資料後用 ctrl+z 結尾的確會出現死迴圈。不過這個卻是 TC 的問題!請看以下代碼:
#include <stdio.h>
int main( void )
{
int ch;
while ( getchar() != EOF ) ;
if ( feof(stdin) )
{
printf("Oh, No! EOF indicator is set now!\n");
}
clearerr(stdin);
if ( !feof(stdin) )
{
printf("Ok! EOF indicator is unset now!\n");
}
if ( getchar() == EOF )
{
printf("But why still cannot read from stdin?\n");
}
return 0;
}
用 TC 編譯運行時輸入 21312313^Z,得到結果如下:
21312313^Z
Oh, No! EOF indicator is set now!
Ok! EOF indicator is unset now!
But why still cannot read from stdin?
由此可見,就算沒有標註 EOF 標記,如果輸入時以 ^Z 結尾,也會導致不能從 stdin 中讀取資料!這是 TC 的問題!我原來的程式之所以會在輸入以 ^Z 結尾時會出現死迴圈,就是因為不能從 stdin 中讀取資料!至於樓上用了 rewind(stdin); 之後就能從 stdin 中讀取資料,看來是 TC 特定的功能!
不過也要感謝樓上,我因此才發現如果 stdin 被重新導向到檔案,我的程式會出現死迴圈。不過當初我寫那兩個程式也僅僅是為了示範一下如何清空 stdin,並沒有考慮太多其它因素。
3. 對於樓上提出的方案表示強烈反對!樓上提出的方案比使用 fflush(stdin); 高明不到哪裡去,都是使用了編譯器特定的功能。
首先我們看一下標準對 rewind 函數的定義:
void rewind(FILE *stream);
rewind 函數把 stream 指向的流的檔案位置標記設定為檔案
開始。如果不考慮它還會清除流的錯誤標記,則 rewind 函數
等同於
(void)fseek(stream, 0L, SEEK_SET);
原文如下:
The rewind function sets the file position indicator for
the stream pointed to by stream to the beginning of the
file. It is equivalent to
(void)fseek(stream, 0L, SEEK_SET)
except that the error indicator for the stream is also
cleared.
K&R 的 The C Programming Language, Second Edition 乾脆就說
rewind(fp); 等同於 fseek(fp, 0L, SEEK_SET); clearerr(fp);
由此可見,標準只是說 rewind 可以把流的檔案位置標記設定為檔案開始,並且清除流的錯誤標記,卻沒有定義 rewind(stdin) 可以清空 stdin 的內容,所以使用 rewind(stdin) 不一定能清空 stdin。而且,如果 stdin 被重新導向到檔案的話,使用 rewind 更是會產生非常“有趣”的結果。有興趣的朋友可以試一下。