fflush 函数

1、C99 对 fflush 函数的定义:


int fflush(FILE *stream);

如果stream指向输出流或者更新流(update stream),并且这个更新流最近执行的操作不是输入,那么fflush函数将把任何未被写入的数据写入stream指向的文件(如标准输出文件stdout)。否则,fflush函数的行为是不确定的。
fflush(NULL)清空所有输出流和上面提到的更新流。如果发生写错误,fflush函数会给那些流打上错误标记,并且返回EOF,否则返回0。

由此可知,如果 stream 指向输入流(如 stdin),那么 fflush 函数的行为是不确定的。故而使用 fflush(stdin) 是不正确的,至少是移植性不好的。

,成功返回0,出错返回EOF。

2、使用注意点及误区

为什么 fflush(stdin) 是错的?
首先请看以下程序:
Quote:
程序代码 程序代码
#i nclude
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) 来清空输入缓冲,但是并非所有编译器都要支持这个功能(gcc3.2不支持),因为标准中根本没有定义 fflush(stdin)。MSDN 文档里也清楚地写着 fflush on input stream is an extension to the C standard (fflush 操作输入流是对C标准的扩充)。当然,如果你毫不在乎程序的移植性,用  fflush(stdin) 也没什么大问题。

2.      清空输入缓冲区的方法

虽然不可以用 fflush(stdin),但是我们可以自己写代码来清空输入缓冲区。只需要在 scanf 函数后面加上几句简单的代码就可以了。
Quote:程序代码 程序代码
/* C 版本 */
#i nclude

int main( void )
{
    int i, c;
    for (;;) {
        fputs("Please input an integer: ", stdout);
        if ( scanf("%d", &i) != EOF ) { /* 如果用户输入的不是 EOF */
            /* while循环会把输入缓冲中的残留字符清空 */
            /* 读者可以根据需要把它改成宏或者内联函数 */
            /* 注:C99中也定义了内联函数,gcc3.2支持 */
            while ( (c=getchar()) != '/n' && c != EOF ) {
                  ;
            } /* end of while */
        }
        printf("%d/n", i);
    }
    return 0;
}


运行结果:
当输入数字时,正常;
当输入字母时,i的值不改变,printf输出的仍是上一次的值。由于输入缓冲区每次都被清空,所以也就不存在scanf自动不停读取缓冲区的值的情况。


#include   
#include   

void   main() 
{ 
        int   n   =   5; 
        int   i   ; 
        char   c[5]; 
        for(i   =   0;   i   <   n;   i++) 
        { 
                c[i]=scanf( "   %c ",&c[i]); 
              /*   fflush(stdin);*/ 
                /*printf( "%c\n ",c[i])   ;*/ 
        } 

        for(i=0;i <n;i++) 
        printf( "%d ",c[i]); 
        getch(); 
} 



我们在运行程序的时候,输入如下 




程序便宣告结束,而实际上程序的循环有五次,我们应该输入5次程序才会结束的,这是什么原因呢?我们先看看c语言中的输入缓冲:C语言编译器I/O输入函数一般采用了缓冲式输入方式。也就是说,当你输入“a回车b回车c回车”时,程序先输入a但同时将回车缓冲区里,然后下次再从缓冲区里取出数据,也就是回车。依次类推。可以用getchar()解决问题。根据上面的介绍,   我们再来分析我们的输入过程: 

a   回车 

b回车 



恰好从缓冲区内读取了5次,因此我们的程序宣告结束了。有什么方法解决这个问题呢? 

1、在每次读取之后,将缓冲区刷新——使用fflush(stdin)函数; 

2、scanf( "%c ",&c[i])   ;   改成scanf( "   %c ",&c[i])   ;注意在 "%c "前面加上一个空格; 

3、完全杜绝这种情况发生,使用gets()函数,或者scanf( "%s ",c)   ;一次性作为一个字符串读入; 

下面再介绍一下getch,getche(),getchar();的区别,这几个函数的使用实在是太多了,这三个函数的区别一直都不清楚,今天终于有机会弄明白了: 

getch()和getche()函数   
 这两个函数都是从键盘上读入一个字符。其调用格式为:   
  getch();   
  getche();   
 两者的区别是:getch()函数不将读入的字符回显在显示屏幕上,而getche()  函数却将读入的字符回显到显示屏幕上。   

getchar()函数   

 getchar()函数也是从键盘上读入一个字符,   并带回显。它与前面两个函数的区别在于:   getchar()函数等待输入直到按回车才结束,    

 回车前的所有输入字符都会逐个显示在屏幕上。但只有第一个字符作为函数的返回值。 



/------------------------------------------------------------------------------------------------------------------------------------------------------/
2.10. C标准库的I/O缓冲区 请点评

用户程序调用C标准I/O库函数读写文件或设备,而这些库函数要通过系统调用把读写请求传给内核(以后我们会看到与I/O相关的系统调用),最终由内核驱动磁盘或设备完成I/O操作。C标准库为每个打开的文件分配一个I/O缓冲区以加速读写操作,通过文件的FILE结构体可以找到这个缓冲区,用户调用读写函数大多数时候都在I/O缓冲区中读写,只有少数时候需要把读写请求传给内核。以fgetc/fputc为例,当用户程序第一次调用fgetc读一个字节时,fgetc函数可能通过系统调用进入内核读1K字节到I/O缓冲区中,然后返回I/O缓冲区中的第一个字节给用户,把读写位置指向I/O缓冲区中的第二个字符,以后用户再调fgetc,就直接从I/O缓冲区中读取,而不需要进内核了,当用户把这1K字节都读完之后,再次调用fgetc时,fgetc函数会再次进入内核读1K字节到I/O缓冲区中。在这个场景中用户程序、C标准库和内核之间的关系就像在第 5 节 “Memory Hierarchy”中CPU、Cache和内存之间的关系一样,C标准库之所以会从内核预读一些数据放在I/O缓冲区中,是希望用户程序随后要用到这些数据,C标准库的I/O缓冲区也在用户空间,直接从用户空间读取数据比进内核读数据要快得多。另一方面,用户程序调用fputc通常只是写到I/O缓冲区中,这样fputc函数可以很快地返回,如果I/O缓冲区写满了,fputc就通过系统调用把I/O缓冲区中的数据传给内核,内核最终把数据写回磁盘。有时候用户程序希望把I/O缓冲区中的数据立刻传给内核,让内核写回设备,这称为Flush操作,对应的库函数是fflush,fclose函数在关闭文件之前也会做Flush操作。

下图以fgets/fputs示意了I/O缓冲区的作用,使用fgets/fputs函数时在用户程序中也需要分配缓冲区(图中的buf1和buf2),注意区分用户程序的缓冲区和C标准库的I/O缓冲区。

图 25.1. C标准库的I/O缓冲区








C标准库的I/O缓冲区有三种类型:全缓冲、行缓冲和无缓冲。当用户程序调用库函数做写操作时,不同类型的缓冲区具有不同的特性。

全缓冲

如果缓冲区写满了就写回内核。常规文件通常是全缓冲的。

行缓冲

如果用户程序写的数据中有换行符就把这一行写回内核,或者如果缓冲区写满了就写回内核。标准输入和标准输出对应终端设备时通常是行缓冲的。

无缓冲

用户程序每次调库函数做写操作都要通过系统调用写回内核。标准错误输出通常是无缓冲的,这样用户程序产生的错误信息可以尽快输出到设备。

下面通过一个简单的例子证明标准输出对应终端设备时是行缓冲的。

#include 

int main()
{
printf("hello world");
while(1);
return 0;
}

运行这个程序,会发现hello world并没有打印到屏幕上。用Ctrl-C终止它,去掉程序中的while(1);语句再试一次:

$ ./a.out
hello world$

hello world被打印到屏幕上,后面直接跟Shell提示符,中间没有换行。

我们知道main函数被启动代码这样调用:exit(main(argc, argv));。main函数return时启动代码会调用exit,exit函数首先关闭所有尚未关闭的FILE *指针(关闭之前要做Flush操作),然后通过_exit系统调用进入内核退出当前进程[35]

在上面的例子中,由于标准输出是行缓冲的,printf("hello world");打印的字符串中没有换行符,所以只把字符串写到标准输出的I/O缓冲区中而没有写回内核(写到终端设备),如果敲Ctrl-C,进程是异常终止的,并没有调用exit,也就没有机会Flush I/O缓冲区,因此字符串最终没有打印到屏幕上。如果把打印语句改成printf("hello world\n");,有换行符,就会立刻写到终端设备,或者如果把while(1);去掉也可以写到终端设备,因为程序退出时会调用exitFlush所有I/O缓冲区。在本书的其它例子中,printf打印的字符串末尾都有换行符,以保证字符串在printf调用结束时就写到终端设备。

我们再做个实验,在程序中直接调用_exit退出。

#include 
#include 

int main()
{
printf("hello world");
_exit(0);
}

结果也不会把字符串打印到屏幕上,如果把_exit调用改成exit就可以打印到屏幕上。

除了写满缓冲区、写入换行符之外,行缓冲还有两种情况会自动做Flush操作。如果:

  • 用户程序调用库函数从无缓冲的文件中读取

  • 或者从行缓冲的文件中读取,并且这次读操作会引发系统调用从内核读取数据

那么在读取之前会自动Flush所有行缓冲。例如:

#include 
#include 

int main()
{
char buf[20];
printf("Please input a line: ");
fgets(buf, 20, stdin);
return 0;
}

虽然调用printf并不会把字符串写到设备,但紧接着调用fgets读一个行缓冲的文件(标准输入),在读取之前会自动Flush所有行缓冲,包括标准输出。//对于交互式行缓冲来说可以自己flush,但是其他的不一定,所以小心使用自动flush的功能,必要时自己用fflush操作

如果用户程序不想完全依赖于自动的Flush操作,可以调fflush函数手动做Flush操作。

#include 

int fflush(FILE *stream);
返回值:成功返回0,出错返回EOF并设置errno

对前面的例子再稍加改动:

#include 

int main()
{
printf("hello world");
fflush(stdout);
while(1);
}

虽然字符串中没有换行,但用户程序调用fflush强制写回内核,因此也能在屏幕上打印出字符串。fflush函数用于确保数据写回了内核,以免进程异常终止时丢失数据。作为一个特例,调用fflush(NULL)可以对所有打开文件的I/O缓冲区做Flush操作。

posted @ 2016-01-10 12:20  栗子强  阅读(1610)  评论(0编辑  收藏  举报