轉一篇,關於fflush、緩衝區、scanf、EOF等問題

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);,把輸入緩衝清空掉不就行了?”然而這是錯的!CC++標準裏從來沒有定義過fflush(stdin)。也許有人會說:“可是我用fflush(stdin)解決了這個問題,你怎麼能說是錯的呢?”的確,某些編譯器(如VC6)支持用fflush(stdin)來清空輸入緩衝,但是並非所有編譯器都要支持這個功能(linux 下的 gcc不支持),因爲標準中根本沒有定義fflush(stdin)MSDN文檔裏也清楚地寫着fflush on input stream is anextension to the C standardfflush操作輸入流是對C標準的擴充)。當然,如果你毫不在乎程序的移植性,用fflush(stdin)也沒什麼大問題。以下是C99fflush函數的定義:

 

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 recent
operation was not input, the fflush function causes
any unwritten data for that
stream 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 更是會產生非常“有趣”的結果。有興趣的朋友可以試一下。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章