关于fflush、缓冲区、scanf、EOF等问题真麻烦

1.为什么 fflush(stdin)是错的

http://u637.springnote.com/pages/6288463.xhtml(已经bad request了)

首先请看以下程序:

#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 更是会产生非常“有趣”的结果。有兴趣的朋友可以试一下。

posted on 2013-06-29 20:52  xuangong  阅读(1015)  评论(0编辑  收藏  举报

导航