关于流和缓冲区的理解


原文链接:http://bbs.chinaunix.net/viewthread.php?tid=588099

各大权威对流的说法有些不一致,我认为流既是数据的源或目的地的抽象,也是源和目的地之间流动信息的表示。但流起码都暗含以下的几个方面:

1、流是一个抽象的概念,是对信息的一种表达;在程序中,流就是对某个对象输入输出信息的抽象。就像运输工具是对一切运动载体的抽象一样。

2、流是一种“动”的概念,静止存储在介质上的信息只有当它按一定的序列准备“运动”时才称为流。“从程序移进或移出字节”就是“动”的表现。静止的信息具有流的潜力,但不一定是流,就像没有汽油不能行走的汽车一样,它具有运输工具的潜力,但它还不是运输工具(因为它很有可能被当作房子来用了,我就在大街上看见有精明的商人用火车车厢来做酒吧)。
3、流有源头也有目的地;程序中各种移动的信息都有其源和目的,记得编程(特别是汇编)时,老是要确定好某个操作的源操作数和目的操作数。借用佛教一言也即是:“万物皆有因果”,这也就像长江一样,西自唐古拉,而东去太平洋。在高速公路上飞跑的汽车,它必有其出发地和目的地。
4、流一定带有某种信息,没有任何内容的流带着自身来表达“空”信息。就像运输工具一样,它不运货的时候就运着自己这一身的零件(包括驾驶员)并把一样东西运到目的地,那就是它自己和一个“跑空车”的信息。流有最小的信息单元就是二进制位,含有最小的信息包就是字节,C标准库提供两种类型的流:二进制流(binary stream)和文本流(text stream)。二进制流是有未经处理的字节构成的序列;文本流是由文本行组成的序列。而在著名的UNIX系统中,文本流和二进制流是相同的(identical)。

5、流有源头也有目的地,那么它必定与源头和目的地相关联。但人们操作流的时候,最关心的还是其目的地,也就是一个定向(orientation)的意思,就像司机运货一样,它首要关心的问题是目的地,而非起点(操作者都知道)。在C语言中,通过打开流来关联流及其目的地,使用的函数是fopen(),该函数返回一个指向文件的指针(FILE *),该指针包含了足够的可以控制流准确地到达目的地的信息。

FILE是一个结构体(摘自TC2.0中stdio.h文件)

/* Definition of the control structure for streams

*/

typedef struct  {

        short           level;          /* fill/empty level of buffer */

        unsigned        flags;          /* File status flags    */

        char            fd;             /* File descriptor      */

        unsigned char   hold;           /* Ungetc char if no buffer */

        short           bsize;          /* Buffer size          */

        unsigned char   *buffer;        /* Data transfer buffer */

        unsigned char   *curp;          /* Current active pointer */

        unsigned        istemp;         /* Temporary file indicator */

        short           token;          /* Used for validity checking */

}       FILE;                           /* This is the FILE object */

     将它称为流控制结构体(control structure for streams)真好表现出其功能来。举个例子就好像一卡车司机要把货物运到X公司,公司主管就会给他一张地图及X公司的基本信息,这些材料所提供的信息如果足够的话,那么它就能指导着司机准确地将货物送达了。C中FILE这个结构体所起的作用就好像是运输公司把一切有用的指导信息封装起来的档案袋一样。而已有关联的流要终止这种关联,就必须关闭流,使用的函数是fclose(),就像运货公司若不再给X公司运货了,那么他们就必须要终止合作协议了。
    这里要注意的是:C语言中stdin、stdout、stderr分别是标准输入流、标准输出流及标准出错流的逻辑目的,他们都默认对应相应的物理终端。在程序运行伊始,不需要进行open()操作,流自动打开。
说的最清楚的要数Stevens的《UNIX环境高级编程》了,以下摘自Stevens的《UNIX环境高级编程》第五章:

引用:标准I / O提供了三种类型的缓存:
(1) 全缓存。在这种情况下,当填满标准I / O缓存后才进行实际I / O操作。对于驻在磁盘上的文件通常是由标准I / O库实施全缓存的。在一个流上执行第一次I / O操作时,相关标准I / O函数通常调用m a l l o c(见7 . 8节)获得需使用的缓存。
术语刷新( f l u s h)说明标准I / O缓存的写操作。缓存可由标准I / O例程自动地刷新(例如当填满一个缓存时),或者可以调用函数ff l u s h刷新一个流。值得引起注意的是在U N I X环境中,刷新有两种意思。在标准I / O库方面,刷新意味着将缓存中的内容写到磁盘上(该缓存可以只是局部填写的)。在终端驱动程序方面(例如在第11章中所述的t c f l u s h函数),刷新表示丢弃已存在缓存中的数据。
(2) 行缓存。在这种情况下,当在输入和输出中遇到新行符时,标准I / O库执行I / O操作。这允许我们一次输出一个字符(用标准I/O fputc函数),但只有在写了一行之后才进行实际I / O操作。当流涉及一个终端时(例如标准输入和标准输出),典型地使用行缓存。对于行缓存有两个限制。第一个是:因为标准I / O库用来收集每一行的缓存的长度是固定的,所以只要填满了缓存,那么即使还没有写一个新行符,也进行I / O操作。第二个是:任何时候只要通过标准输入输出库要求从( a )一个不带缓存的流,或者( b )一个行缓存的流(它预先要求从内核得到数据)得到输入数据,那么就会造成刷新所有行缓存输出流。在( b )中带了一
个在括号中的说明的理由是,所需的数据可能已在该缓存中,它并不要求内核在需要该数据时才进行该操作。很明显,从不带缓存的一个流中进行输入( ( a )项)要求当时从内核得到数据。
(3) 不带缓存。标准I / O库不对字符进行缓存。如果用标准I / O函数写若干字符到不带缓存
的流中,则相当于用w r i t e系统调用函数将这些字符写至相关联的打开文件上。标准出错流s t d e r r通常是不带缓存的,这就使得出错信息可以尽快显示出来,而不管它们是否含有一个新行字符。
ANSI C要求下列缓存特征:
(1) 当且仅当标准输入和标准输出并不涉及交互作用设备时,它们才是全缓存的。
(2) 标准出错决不会是全缓存的。
但是,这并没有告诉我们如果标准输入和输出涉及交互作用设备时,它们是不带缓存的还
是行缓存的,以及标准输出是不带缓存的,还是行缓存的。S V R 4和4 . 3 + B S D的系统默认使用下列类型的缓存:
• 标准出错是不带缓存的。
• 如若是涉及终端设备的其他流,则它们是行缓存的;否则是全缓存的。



    我们经常要用到标准输入和输出,而ANSI C对stdin、stdout和stderr的缓存特征没有强行的规定,以至于不同的系统可能有不同的stdin、stdout和stderr的缓存特征。目前主要的缓存特征是:stdin和stdout是行缓存;而stderr是无缓存的。


三、一般标准输入易产生的问题

stdin和stdout是行缓存,所以在进行标准输入输出时就存在着缓存刷新(flush)的问题。就像仓库一样,如果不进行有效清理,就有可能将已淘汰的东西错误地当作新货发给客户了。stdin和stdout如果不及时刷新的话,就有可能输入或输出错误的数据。
    此时,回到本文开头给出的两个例子。第一个例子中在system("pause");这条语句的后面加上printf("%d\n", c);这一句,就会发现打印出c的整数值为10,查一下ASCII码10对应的字符是换行符(Line Feed),这样
for (; (c = getchar()) == 'y' || c == 'Y'; )的条件判断为假,所以程序不可能再循环下去。这个换行符LF怎么来的呢?就是我们第一次输入‘y’时按下的回车符。(这里要说明一点是:键盘的回车符在C中被处理成换行符(LF),它与ASCII码中对应的回车符(carriage return ,ASCII为13)是不同的,这样一来依靠键盘一键输入’\r’(carriage return)是不可能了。好在行缓存是以’\n’作为换行标志的)。就是因为标准输入流是行缓存的,所以换行符没有被丢弃反而自动成了下一次的输入了。第二个例子中出错并不是因为有换行符自动做了输入,而是因为某次偶尔的错误输入导致的,程序没了健壮性很大一部分是因为没有考虑到标准输入流具有行缓存特征。

四、此类问题的解决方法小结

既然是有垃圾留在了缓冲区就有治标与治本两大方法了。
1、 治标
a) 清理垃圾。将还驻留在缓存中的无用数据进行清理。
---利用fflush()函数来刷新缓存。但是特别要注意的是fflush()的特点。

(引自ISO/IEC《 ISO/IEC 9899:1999 (E) 》)
引用:Description
 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.
If stream is a null pointer, the fflush function performs this flushing action on all
streams for which the behavior is defined above.


以上突显部分就是说明fflush()用于stdin时是不确定的,它可能返回0以表示刷新成功,但事实上它对stdin没有任何动作。因此用fflush()来刷新stdin引入了很多问题,以下是一些例子:

1)摘自Frequently Asked Questions in comp.lang.c
(http://www.lysator.liu.se/c/c-faq/c-faq-toc.html#c-11)

引用:11.12: How can I flush pending input so that a user's typeahead isn't read at the next prompt?  Will fflush(stdin) work? 
Answer: fflush is defined only for output streams.  Since its definition of "flush" is to complete the writing of buffered characters (not to discard them), discarding unread input would not be an analogous meaning for fflush on input streams.  There is no standard way to discard unread characters from a stdio input buffer, nor would such a way be sufficient; unread characters can also accumulate in other, OS-level input buffers. 



2) 
引用:Question:http://lists.gnu.org/archive/html/bug-glibc/2005-02/msg00063.html
Answer: http://lists.gnu.org/archive/html/bug-glibc/2005-02/msg00073.html



3)(http://www.mega-nerd.com/erikd/BOOK/#Q5)

引用:Q 5 : In chapter 13, listing 6, fflush (stdin) doesn't work like its supposed to. Why is that? 
This is a bug :-). 
The problem with the existing code occurs when a program reads user input, then does some time consuming processing (say more than a couple of seconds) before reading more user input. In this situation, the user might type some more on the keyboard while the program is processing and these characters will be buffered by the operating system and read on the next read call. The idea of the existing code was to use fflush (stdin) to flush all the stored characters from the input buffer. 
As Benjamin Black pointed out, the C Programming FAQ states that the behaviour of fflush () is defined only for output streams. To make matters worse there is no standard ANSI/ISO C way of doing this. Fortunately there is a POSIX solution. The POSIX solution is to use the low-level unbuffered input functions or GNU/Linux specific higher level function which in turn use the lower level ones. There are examples of both here.



更详细的解答:http://www.mega-nerd.com/erikd/BOOK/list1306.html

----利用getchar()在下一次输入之前吃掉第一个字符,这里常被用来吃掉一个换行符(例如第一例中)。这里要注意的是:getchar()是一个宏,等同于宏getc()带参数stdin,它不是函数(虽然此宏中由函数)。并且它每次只能读取一个字符,对于垃圾字符数目不定的情况,就无法把握了。

(摘自TC2.0中stdio.h文件)

引用:#define getc(f) \
  ((--((f)->;level) >;= 0) ? (unsigned char)(++(f)->;curp)[-1] : \
_fgetc (f))
#define putc(c,f) \
  ((++((f)->;level) < 0) ? (unsigned char)((++(f)->;curp)[-1]=(c)) : \
_fputc ((c),f))

#define getchar()  getc(stdin)
#define putchar(c) putc((c), stdout)



---另一个就是gets()函数(这个新手最喜欢,而老手最慎重的函数)。gets()读取与stdin相关联的流中的字符串,直到文件结束或换行符,gets()读取换行符并将其舍去。

(引自ISO/IEC《 ISO/IEC 9899:1999 (E) 》)

引用:Description
The gets function reads characters from the input stream pointed to by stdin, into the array pointed to by s, until end-of-file is encountered or a new-line character is read. Any new-line character is discarded, and a null character is written immediately after the last character read into the array.



一般使用gets()都需要一个临时变量来存放这些读出的缓存区残留数据,所以比较浪费资源,使用它时,为了使程序更具模块性,可以用一个函数来实现:

void  clear_kb(void)

 /* Clears stdin of any waiting characters. */

}

char junk[80];

gets(junk);

 }


但是, gets是一个不推荐使用的函数。问题是调用者在使用gets时不能指定缓存的长度。这样就可能造成缓存越界(如若该行长于缓存长度),写到缓存之后的存储空间中,从而产生不可予料的后果。很多时候,还是推荐用fgets()函数,就像
相对strcpy()函数更推荐使用strncpy()一样。

----scanf()函数。由于scanf()函数在读取输入值时将跳过空白符、制表符以及换行符,所以在某种程度上,由它读取值时可以不受遗留在缓存中的换行符影响。但是scanf()函数作为输入函数本身就存在居多的不安全因素(处理字符串时不检查buffer边界),使用它就有可能像搬起了一块大石头,一不小心就会砸到自己的脚。

b) 出污泥而不染
什么叫“出污泥而不染”呢?那就是程序员自己从缓存中挑拣出有用的信息,采用“挑三拣四”的输入函数(例如fgets()、fread()及文件定位函数fseek()、ftell()、rewind()、fgetpos()、fsetpos())来读取换存,使不需要的信息难以被读取。这种方法听起来很好,但做起来难度非同寻常了。

2、 治本
治标的办法可能在小代码内很见效果,也最容易被人想到,但是,一旦问题变复杂了,使用治标的办法来解决无疑是隔靴挠痒,代码不能有很好的健壮性。其实,出现问题也主要是标准输入流的缓存机制造成的,若我们变被动为主动,直接对缓存本身进行相应操作以方便我们使用,那问题不就好解决多了吗?

(以下摘自Stevens的《UNIX环境高级编程》第五章)

引用: 对任何一个给定的流,如果我们并不喜欢这些系统默认,则可调用下列两个函数中的一个更改缓存类型:
#include <stdio.h>;
void setbuf(FILEf p*, char *b u f) ;
int setvbuf(FILEf p,* char *b u f, int m o d e, size_ts i z e) ;
返回:若成功则为0,若出错则为非0
这些函数一定要在流已被打开后调用(这是十分明显的,因为每个函数都要求一个有效的文件指针作为它们的第一个参数),而且也应在对该流执行任何一个其他操作之前调用。可以使用s e t b u f函数打开或关闭缓存机制。为了带缓存进行I / O,参数buf 必须指向一个长度为B U F S I Z的缓存(该常数定义在< s t d i o . h >;中)。通常在此之后该流就是全缓存的,但是如果该流与一个终端设备相关,那么某些系统也可将其设置为行缓存的。为了关闭缓存,将b u f设置为N U L L。
使用s e t v b u f,我们可以精确地说明所需的缓存类型。这是依靠m o d e参数实现的:
_IOFBF 全缓存
_IOLBF 行缓存
_IONBF 不带缓存
如果指定一个不带缓存的流,则忽略buf 和size 参数。如果指定全缓存或行缓存,则buf 和s i z e可以可选择地指定一个缓存及其长度。如果该流是带缓存的,而buf 是N U L L,则标准I / O库将自动地为该流分配适当长度的缓存。适当长度指的是由s t r u c t结构中的成员s t _ b l k s i z e所指定的值(见4 . 2节)。如果系统不能为该流决定此值(例如若此流涉及一个设备或一个管道),则分配长度为B U F S I Z的缓存。



表5 - 1列出了这两个函数的动作,以及它们的各个选择项。
 

这样一来,本文开头的两个例子就有很多修改的办法了。利用治本的方法解决改贴(http://bbs.chinaunix.net/forum/viewtopic.php?t=586176&show_type=&postdays=0&postorder=asc&amp;start=0)中的一个问题。
#include <stdio.h>; 

void main() 

{ 

int i,j; 

char c; 

printf("\n do you want to cal:y/n \n"); 



while(c=getchar()=='y') 

{ 

                                  printf("input number:"); 

   scanf("%d%d",&i,&j);   

   printf("i*j=%d",i*j); 

          } 

}

这个程序存在问题,这里使用setbuf()来“治疗”:
#include <stdio.h>;



int main(void)

{

   int i,j;

   char c;

   printf("\n do you want to cal:y/n \n");



   while (c=getchar()=='y')

   {

       printf("input number:");

       scanf("%d%d",&i,&j);

       printf("i*j=%d",i*j);

       setbuf(stdin, NULL);

       printf("\n do you want to cal:y/n \n");

   }

  /*system("pause");*/

   return 0;

}

缓冲区的个人理解

转自https://social.microsoft.com/Forums/zh-CN/c8ae82d8-18ed-42f1-aabf-e3c1de4f4d9f

这里所说的缓冲区指的是为标准输入与标准输出设置的缓冲区,为什么要设置一个标准输入缓冲区主要是从效率上来考虑的,如果不设缓冲区会降低cpu的效率,因为它总是会等待用户输入完之后才会去执行某些指令!同样设置一个标准输出缓冲区是为了解决打印的问题!总之这样做的目的就是为了效率!

接下来讲解一下怎么设置标准输入与标准输出缓冲区。

如果我们不认为的设置的话,系统会自动的为标准输入与标准输入设置一个缓冲区,这个缓冲区的大小通常是4Kb的大小,这和计算机中的分页机制有关,因为进程在计算机中分配内存使用的就是分页与分段的机制,并且每个页的大小是4Kb,因此通常情况下缓冲区的大小会设置为4Kb的大小!并且这个缓冲区的类型是一个全缓冲的缓冲区!所谓全缓冲指的是:当缓冲区里的数据写满的时候(或者可以说达到顶端)缓冲区中的数据才会“写”到标准输入磁盘文件中,这里说的写不是将缓冲区中的数据移动到磁盘文件中,而是拷贝到磁盘文件中,也就说此时磁盘文件中保留了一份缓冲区内容的备份!除了全缓冲外还有不缓冲和行缓冲,不缓冲不太常见与常用,在这里我就不做讲解了!下面讲解一下什么是行缓冲。行缓冲指的是当在键盘上敲下回车键的时候数据会存储在缓冲区中,这是毫无疑问的,同时也将缓冲区的数据拷贝一份到磁盘文件中!那么磁盘文件中备份的内容有什么用呢??本人能力有限目前还没有发现有什么用!

当热我们还可以自己设置缓冲区,缓冲区的大小可以由我们自己决定,缓冲区的类型也由我们自己决定!在这里有两个函数,一个是setbuf(  FILE *stream  ,  char *buffer ) 另一个是setvbuf( FILE *stream ,   char *buffer  ,  int mode  ,  unsigned int  size  )

其中缓冲区的类型可以是:_IOFBF:全缓冲  _IOLBF :行缓冲 _IONBF 不缓冲

下面讲解一下缓冲区是怎么工作的!

当我们从键盘输入数据的时候数据并不是直接被我们得到(这个问题我在上面已经讲解过了,不在重复),而是将这些输入的数据放在了缓冲区中,然后我们从缓冲区中得到我们想要的数据如果我们通过函数(setbuf , setvbuf)将缓冲区设置10个字节的大小,而我们从键盘输入了20个字节大小的数据,这样我们输入的前10个数据会放在缓冲区中,因为我们设置的缓冲区的大小只能够装下10个字节大小的数据,装不下20个字节大小的数据。那么剩下的那10个字节大小的数据怎么办呢??暂时放在了输入流中!如果不能够理解这个,那我举一个比较形象的例子:

上面的箭头表示的区域就相当是一个输入流,红色的地方相当于一个开关,这个开关可以控制往深绿色区域(标注的是缓冲区)里放进去的数据,输入20个字节的数据只往缓冲区中放进去了10个字节,剩下的10个字节的数据就被停留在了输入流里!等待下去往缓冲区中放入!接下来系统是如何来控制这个缓冲区呢?

C语言方式下  是一个结构体数组  类型是FILE结构体

struct _iobuf {

       char *_ptr;

       int   _cnt;

       char *_base;

       int   _flag;

       int   _file;

       int   _charbuf;

       int   _bufsiz;

       char *_tmpfname;

       };

结构体中的成员简单的介绍下
_ptr     //指向当前缓冲区内容的指针
_cnt     //如果是输入缓冲区  那他就是显示现在缓冲区里还有多少个有效数据
_base    //缓冲区基地址
_flag    //标志位   具体好像就是什么可写啊可读啊之类的
_file    //这个是设备句柄(也可以说是文件句柄)
_bufsiz  //缓冲区总大小   一般都是0x1000   也就是4k   也就是一个分页

 

在上面我们向缓冲区中放入了10个字节大小的数据,FILE结构体中的_cnt变为了10,说明此时缓冲区中有10个字节大小的数据可以读,同时我们假设缓冲区的基地址即_base为0x00428e60,它是不变的而此时_ptr的值也为0x00428e60表示从0x00428e60这个位置开始读取数据,当我们要从缓冲区中读取5个数据的时候,_cnt变为了5,表示缓冲区还有5个数据可以读,_ptr则变为了0x00428e65,表示下次应该从这个位置开始读取缓冲区中的数据,如果接下来我们再读取5个数据的时候,_cnt则变为了0,表示缓冲区中已经没有任何数据了,_ptr变为了0x00428e69表示下次应该从这个位置开始从缓冲区中读取数据,但是此时缓冲区中已经没有任何数据了,所以要将输入流中的剩下的那10个字节数据放进来,这样缓冲区中又有了10个数据,此时_cnt变为了10,注意了刚才我们讲到_ptr的值是0x00428e69,而当缓冲区中重新放进来数据的时候这个_ptr的值变为了0x00428e60,这是因为当缓冲区中没有任何数据的时候要将_ptr这个值进行一下刷新,使其指向缓冲区的基地址也就是0x00428e60这个值!因为下次要从这个位置开始读取数据!

 

在这里有点需要说明:当我们从键盘输入字符串的时候需要敲一下回车键才能够将这个字符串送入到缓冲区中,那么敲入的这个回车键(\r)会被转换为一个换行符\n,这个换行符\n也会被存储在缓冲区中并且被当成一个字符来计算!比如我们在键盘上敲下了123456这个字符串,然后敲一下回车键\r将这个字符串送入了缓冲区中,那么此时缓冲区中的字节个数是7,而不是6

缓冲区的刷新就是将指针_ptr变为缓冲区的基地址,同时_cnt的值变为0,因为缓冲区刷新后里面是没有数据的!

 


  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值