《Linux – Linux高级编程 – 第一部分 标准IO及文件IO》第2章 标准I/O

2.1 Linux I/O概述

2.1.1文件I/O和标准I/O的概念

文件I/O:文件I/O称之为不带缓存的IO(unbuffered I/O)。不带缓存指的是每个read,write都调用内核中的一个系统调用。也就是一般所说的低级I/O——操作系统提供的基本IO服务,与os绑定,特定于linix或unix平台。

标准I/O:标准I/O是ANSI C建立的一个标准I/O模型,是一个标准函数包和stdio.h头文件中的定义,具有一定的可移植性。标准I/O库处理很多细节。例如缓存分配,以优化长度执行I/O等。标准的I/O提供了三种类型的缓存。

1>全缓存:当填满标准I/O缓存后才进行实际的I/O操作。
2>行缓存:当输入或输出中遇到新行符时,标准I/O库执行I/O操作。
3>不带缓存:stderr就是了。

2.1.2文件I/O和标准I/O的区别

文件I/O 又称为低级磁盘I/O,遵循POSIX相关标准。任何兼容POSIX标准的操作系统上都支持文件I/O。标准I/O被称为高级磁盘I/O,遵循ANSI C相关标准。只要开发环境中有标准I/O库,标准I/O就可以使用。(Linux 中使用的是GLIBC,它是标准C库的超集。不仅包含ANSI C中定义的函数,还包括POSIX标准中定义的函数。因此,Linux 下既可以使用标准I/O,也可以使用文件I/O)。

通过文件I/O读写文件时,每次操作都会执行相关系统调用。这样处理的好处是直接读写实际文件,坏处是频繁的系统调用会增加系统开销,标准I/O可以看成是在文件I/O的基础上封装了缓冲机制。先读写缓冲区,必要时再访问实际文件,从而减少了系统调用的次数。

文件I/O中用文件描述符表现一个打开的文件,可以访问不同类型的文件如普通文件、设备文件和管道文件等。而标准I/O中用FILE(流)表示一个打开的文件,通常只用来访问普通文件。

2.1.3各自使用的函数

表1
标准IO 文件IO(低级IO)
打开 fopen,freopen,fdopen open
关闭 fclose close
getc,fgetc,getchar fgets,gets fread read
putc,fputc,putcharfputs,puts, fwrite write

1.fopen与open
标准I/O使用fopen函数打开一个文件:

FILE* fp=fopen(const char* path,const char *mod)

其中path是文件名,mod用于指定文件打开的模式的字符串,比如"r","w","w+","a"等等,可以加上字母b用以指定以二进制模式打开(对于 linix系统,只有一种文件类型,因此没有区别),如果成功打开,返回一个FILE文件指针,如果失败返回NULL,这里的文件指针并不是指向实际的文件,而是一个关于文件信息的数据包,其中包括文件使用的缓冲区信息。

文件IO使用open函数用于打开一个文件:

int fd=open(char *name,int how);

与fopen类似,name表示文件名字符串,而how指定打开的模式:O_RDONLY(只读),O_WRONLY(只写),O_RDWR (可读可写),还有其他模式请man 2 open。成功返回一个正整数称为文件描述符,这与标准I/O显著不同,失败的话返回-1,与标准I/O返回NULL也是不同的。

2.fclose与close
与打开文件相对的,标准I/O使用fclose关闭文件,将文件指针传入即可,如果成功关闭,返回0,否则返回EOF
比如:

if(fclose(fp)!=0)  
printf("Error in closing file");

而文件IO使用close用于关闭open打开的文件,与fclose类似,只不过当错误发生时返回的是-1,而不是EOF,成功关闭同样是返回0。C语言用error code来进行错误处理的传统做法。

3. 读文件,getc,fscanf,fgets和read
标准I/O中进行文件读取可以使用getc,一个字符一个字符的读取,也可以使用gets(读取标准io读入的)、fgets以字符串单位进行读取(读到遇 到的第一个换行字符的后面),gets(接受一个参数,文件指针)不判断目标数组是否能够容纳读入的字符,可能导致存储溢出(不建议使用),而fgets使用三个参数:

char * fgets(char *s, int size, FILE *stream);

第一个参数和gets一样,用于存储输入的地址,
第二个参数为整数,表示输入字符串的最大长度,
最后一个参数就是文件指针,指向要读取的文件。最 后是fscanf,与scanf类似,只不过增加了一个参数用于指定操作的文件,比如fscanf(fp,"%s",words)

文件IO中使用read函数用于读取open函数打开的文件,函数原型如下:

ssize_t numread=read(int fd,void *buf,size_t qty);

其中fd就是open返回的文件描述符,buf用于存储数据的目的缓冲区,而qty指定要读取的字节数。如果成功读取,就返回读取的字节数目(小于等于qty)

4. 判断文件结尾
如果尝试读取达到文件结尾,标准IO的getc会返回特殊值EOF,而fgets碰到EOF会返回NULL,而对于*nix的read函数,情况有所不同。read读取qty指定的字节数,最终读取的数据可能没有你所要求的那么多(qty),而当读到结尾再要读的话,read函数将返回0。

5. 写文件:putc,fputs,fprintf和write
与读文件相对应的,标准C语言I/O使用putc写入字符,比如:

putc(ch,fp);

第一个参数是字符,
第二个是文件指针。而fputs与此类似:

fputs(buf,fp);

仅仅是第一个参数换成了字符串地址。而fprintf与printf类似,增加了一个参数用于指定写入的文件,比如:

fprintf(stdout,"Hello %s.\n","dennis");

切记fscanf和fprintf将FILE指针作为第一个参数,而putc,fputs则是作为第二个参数。
在文件IO中提供write函数用于写入文件,原型与read类似:

ssize_t result=write(int fd,void *buf ,size_t amt);

fd是文件描述符,buf是将要写入的内存数据,amt是要写的字节数。如果写入成功返回写入的字节数,通过result与amt的比较可以判断是否写入正常,如果写入失败返回-1。

  1. 随机存取:fseek()、ftell()和lseek()
    标准I/O使用fseek和ftell用于文件的随机存取,先看看fseek函数原型
    int fseek(FILE *stream, long offset, int whence);

    第一个参数是文件指针;
    第二个参数是一个long类型的偏移量(offset),表示从起始点开始移动的距离;
    第三个参数就是用于指定起始点的模式,stdio.h指定了下列模式常量:
    SEEK_SET 文件开始处
    SEEK_CUR 当前位置
    SEEK_END 文件结尾处

看几个调用例子:

fseek(fp,0L,SEEK_SET); //找到文件的开始处 
fseek(fp,0L,SEEK_END); //定位到文件结尾处 
fseek(fp,2L,SEEK_CUR); //文件当前位置向前移动2个字节数

而ftell函数用于返回文件的当前位置,返回类型是一个long类型,比如下面的调用:

fseek(fp,0L,SEEK_END);//定位到结尾 
long last=ftell(fp); //返回当前位置

那么此时的last就是文件指针fp指向的文件的字节数。
与标准I/O类似,*nix系统提供了lseek来完成fseek的功能,原型如下:

off_t lseek(int fildes, off_t offset, int whence);

fildes是文件描述符,而offset也是偏移量,whence同样是指定起始点模式,唯一的不同是lseek有返回值,如果成功就 返回指针变化前的位置,否则返回-1。whence的取值与fseek相同:SEEK_SET,SEEK_CUR,SEEK_END,但也可以用整数 0,1,2相应代替。

2.1.4系统调用与库函数

上面我们一直在讨论文件I/O与标准I/O的区别,其实可以这样说,文件I/O是系统调用、标准I/O是库函数,看下面这张图:

yDwQcn.png

图1

POSIX:Portable Operating System Interface 可移植操作系统接口
ANSI:American National Standrads Institute 美国国家标准学会

1、系统调用
操作系统负责管理和分配所有的计算机资源。为了更好地服务于应用程序,操作系统提供了一组特殊接口——系统调用。通过这组接口用户程序可以使用操作系统内核提供的各种功能。例如分配内存、创建进程、实现进程之间的通信等。

为什么不允许程序直接访问计算机资源?答案是不安全。单片机开发中,由于不需要操作系统,所以开发人员可以编写代码直接访问硬件。而在32位嵌入式系统中通常都要运行操作系统,所以开发人员可以编写代码直接访问硬件。而在32位嵌入式系统中通常都要运行操作系统,程序访问资源的方式都发生了改变。操作系统基本上都支持多任务,即同时可以运行多个程序。如果允许程序直接访问系统资源,肯定会带来很多问题。因此,所有软硬件资源的管理和分配都有操作系统负责。程序要获取资源(如分配内存,读写串口)必须由操作系统来完成,即用户程序向操作系统发出服务请求,操作系统收到请求后执行相关的代码来处理。

用户程序向操作系统提出请求的接口就是系统调用。所有的操作系统都会提供系统调用接口,只不过不同的操作系统提供的系统调用接口各不相同。Linux 系统调用接口非常精简,它继承了Unix 系统调用中最基本的和最有用的部分。这些系统调用按照功能大致可分为进程控制、进程间通信、文件系统控制、存储管理、网络管理、套接字控制、用户管理等几类。

2、库函数
库函数可以说是对系统调用的一种封装,因为系统调用是面对的是操作系统,系统包括Linux、Windows等,如果直接系统调用,会影响程序的移植性,所以这里使用了库函数,比如说C库,这样只要系统中安装了C库,就都可以使用这些函数,比如printf() scanf()等,C库相当于对系统函数进行了翻译,使我们的APP可以调用这些函数。

3、用户编程接口API
前面提到利用系统调用接口程序可以访问各种资源,但在实际开发中程序并不直接使用系统调用接口,而是使用用户编程接口(API)。为什么不直接使用系统调用接口呢?

原因如下:
1)系统调用接口功能非常简单,无法满足程序的需求。
2)不同操作系统的系统调用接口不兼容,程序移植时工作量大。
用户编程接口通俗的解释就是各种库(最重要的就是C库)中的函数。为了提高开发效率,C库中实现了很多函数。这些函数实现了常用的功能,供程序员调用。这样一来,程序员不需要自己编写这些代码,直接调用库函数就可以实现基本功能,提高了代码的复用率。使用用户编程接口还有一个好处:程序具有良好的可移植性。几乎所有的操作系统上都实现了C库,所以程序通常只需要重新编译一下就可以在其他操作系统下运行。

用户编程接口(API)在实现时,通常都要依赖系统调用接口。例如,创建进程的API函数fork()对应于内核空间的sys_fork()系统调用。很多API函数通过多个系统调用来完成其功能。还有一些API函数不要调用任何系统调用。

在Linux 中用户编程接口(API)遵循了在Unix中最流行的应用编程界面标准——POSIX标准。POSIX标准是由IEEE和ISO/IEC共同开发的标准系统。该标准基于当时想用的Unix 实践和经验,描述了操作系统的系统调用编程接口(实际上就是API),用于保证应用程序可以在源代码一级商多种操作系统上运行。这些系统调用编程接口主要是通过C库(libc )实现的。

yDwlXq.png

图2 系统调用、API及系统命令之间的关系

2.2标准I/O

2.2.1标准I/O的由来

标准I/O指的是ANSI C 中定义的用于I/O操作的一系列函数。

只要操作系统安装了C库,标准I/O函数就可以调用。换句话说,如果程序中使用的是标准I/O函数,那么源代码不需要任何修改就可以在其他操作系统下编译运行,具有更好的可移植性。

除此之外,使用标准I/O可以减少系统调用的次数,提高系统效率。标准I/O函数在执行时也会用到系统调用。在执行系统调用时,Linux必须从用户态切换到内核态,处理相应的请求,然后再返回到用户态。如果频繁的执行系统调用会增加系统的开销。为避免这种情况,标准I/O在使用时为用户控件创建缓冲区,读写时先操作缓冲区,在合适的时机再通过系统调用访问实际的文件,从而减少了使用系统调用的次数。

2.2.2流的含义

标准I/O的核心对象就是流。当用标准I/O打开一个文件时,就会创建一个FILE结构体描述该文件(或者理解为创建一个FILE结构体和实际打开的文件关联起来)。我们把这个FILE结构体形象的称为流,我们在stdio.h里可以看到这个FILE结构体。

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 */  

这个结构体:1)对 fd 进行了封装;2)对缓存进行了封装 unsigned char *buffer; 这而指向了buffer 的地址,实际这块buffer是cache,我们要将其与用户控件的buffer分开。

标准I/O函数都是基于流的各种操作,标准I/O中的流的缓冲类型有下面三种:

1.全缓冲
在这种情况下,实际的I/O操作只有在缓冲区被填满了之后才会进行。对驻留在磁盘上的文件的操作一般是有标准I/O库提供全缓冲。缓冲区一般是在第一次对流进行I/O操作时,由标准I/O函数调用malloc函数分配得到的。

术语flush描述了标准I/O缓冲的写操作。缓冲区可以由标准I/O函数自动flush(例如缓冲区满的时候);或者我们对流调用fflush函数。

2.行缓冲
在这种情况下,只有在输入/输出中遇到换行符的时候,才会执行实际的I/O操作。这允许我们一次写一个字符,但是只有在写完一行之后才做I/O操作。一般的,涉及到终端的流–例如标注输入(stdin)和标准输出(stdout)–是行缓冲的。

3.无缓冲
标准I/O库不缓存字符。需要注意的是,标准库不缓存并不意味着操作系统或者设备驱动不缓存。
标准I/O函数时库函数,是对系统调用的封装,所以我们的标准I/O函数其实都是基于文件I/O函数的,是对文件I/O函数的封装,下面具体介绍·标准I/O最常用的函数。

2.2.3流的打开与关闭

使用标准I/O打开文件的函数有fopen() 、fdopen() 、freopen()。他们可以以不同的模式打开文件,都返回一个指向FILE的指针,该指针指向对应的I/O流。此后,对文件的读写都是通过这个FILE指针来进行。

表2 fopen()函数描述
所需头文件 #include
函数原型 FILE fopen(const char path, const char *mode);
函数参数 path:包含要打开的路径及文件名 , mode:文件的打开方式
函数返回值 成功:指向FILE指针,失败:NULL

关闭流的函数为fclose(),该函数将流的缓冲区内的数据全部写入文件中,并释放相关资源。

表3 fclose()函数描述
所需头文件 #include
函数原型 int fclose(FILE *fp);
函数参数 fp:已打开的流指针
函数返回值 成功:0,失败:EOF

2.2.4流的读写

1、按字符(字节)输入/输出

字符输入/输出函数一次仅读写一个字符。

表4字符输入函数原型
所需头文件 #include
函数原型 int fgetc(FILE stream); int getc(FILE stream); int getchar(void);
函数参数 stream:要输入的文件流
函数返回值 成功:读取的字符,失败:EOF

函数getchar等价于get(stdin)。前两个函数的区别在于getc可被实现为宏,而fgetc则不能实现为宏。这意味着:
1)getc 的参数不应当是具有副作用的表达式。
2)因为fgetc一定是一个函数,所以可以得到其地址。这就允许将fgetc的地址作为一个参数传给另一个参数;
3)调用fgetc所需时间很可能长于调用getc,因为调用函数通常所需的时间长于调用宏。

这三个函数在返回下一个字符时,会将其unsigned char 类型转换为int类型。说明为什么不带符号的理由是,如果是最高位为1也不会使返回值为负。要求整数返回值的理由是,这样就可以返回所有可能的字符值再加上一个已出错或已达到文件尾端的指示值。在中的常量EOF被要求是一个负值,其值经常是-1。这就意味着不能将这三个函数的返回值存放在一个字符变量中,以后还要将这些函数的返回值与常量EOF相比较。

注意,不管是出错还是到达文件尾端,这三个函数都返回同样的值。为了区分这两种不同的情况,必须调用ferror或feof。

#include <stdio.h>  
int ferror (FILE *fp);  
int feof (FILE *fp);  

两个函数返回值;若条件为真则返回非0值(真),否则返回0(假);
在大多数实现中,为每个流在FILE对象中维持了两个标志:
1>出错标志。
2>文件结束标志。

表5字符输出-函数原型
所需头文件 #include
函数原型 int fputc(int c, FILE stream);int putc(int c, FILE stream);int putchar(int c);
函数参数 stream:要输出的文件流
函数返回值 成功:输出的字符,失败:EOF

putc()和fputc()向指定的流输出一个字符(节),putchar()向stdout输出一个字符(节)。

2、按行输入、输出

行输入/输出函数一次操作一行。

表6行输入函数原型
所需头文件 #include
函数原型 char fgets(char s, int size, FILE stream);char gets(char *s);
函数参数 s:存放输入字符串的缓冲区的首地止,size:输入的字符串长度,stream:对应的文件流
函数返回值 成功:s,失败:NULL

这两个函数都指定了缓冲区的地址,读入的行将送入其中。gets从标准输入读,而fgets则从指定的流读。

gets函数容易造成缓冲区溢出,不推荐使用;

fgets从指定的流中读取一个字符串,当遇到 \n 或读取了 size – 1个字符串后返回。注意,fgets不能保证每次都能读出一行。 如若该行(包括最后一个换行符)的字符数超过size -1 ,则fgets只返回一个不完整的行,但是,缓冲区总是以null字符结尾。对fgets的下一次调用会继续执行。

表7行输出函数原型
所需头文件 #include
函数原型 int fputs(const char s, FILE stream);int puts(const char *s);
函数参数 s:存放输出字符串的缓冲区的首地止,stream:对应的文件流
函数返回值 成功:非负值,失败:NULL

函数fputs将一个以null符终止的字符串写到指定的流,尾端的终止符null不写出。注意,这并不一定是每次输出一行,因为它并不要求在null符之前一定是换行符。通常,在null符之前是一个换行符,但并不要求总是如此。

下面举个例子:模拟文件的复制过程:

#include <stdio.h>  
#include <string.h>  
#include <unistd.h>  
#include <fcntl.h>  
#define maxsize 5  

int main(int argc, char *argv[])  
{  
    FILE *fp1 ,*fp2;  
    char buffer[maxsize];  
    char *p,*q;  
    if(argc < 3)  
    {  
printf("Usage:%s <srcfile> <desfile>\n",argv[0]);  
            return -1;  
}  

    if((fp1 = fopen(argv[1],"r")) == NULL)  
    {  
perror("fopen argv[1] fails");  
return -1;  
    }  
    if((fp2 = fopen(argv[2],"w+")) == NULL)  
    {  
perror("fopen argv[2] fails");  
return -1;  
}  

    while((p = fgets(buffer,maxsize,fp1)) != NULL)  
    {  
fputs(buffer,fp2);  
    }  
    if(p == NULL)  
    {  
if(ferror(fp1))  
perror("fgets failed");  
if(feof(fp1))  
printf("cp over!\n");  
    }  
    fclose(fp1);  
    fclose(fp2);  
    return 0;  
}  

执行结果如下:

yDwoHf.png

我们可以看到,这里将time.c拷贝给1.c ,1.c和time.c大小一样,都是1183个字节。

2.2.3格式化输入输出

这里举个相关应用例子:循环记录系统时间

实验内容:程序每秒一次读取依次系统时间并写入文件。

#include <stdio.h>  
#include <unistd.h>  
#include <time.h>  

#define N 128 

int main(int argc, char *argv[])  
{  
    int n;  
    char buf[N];  
    FILE *fp;  
    time_t t;  
    char *t_buf = buf;

    if(argc < 2)  
    {  
        printf("Usage : %s <file >\n",argv[0]);  
        return -1;  
    }  

    if((fp = fopen(argv[1],"a+")) == NULL)  
    {  
        perror("open fails");  
        return -1;  
    }  

    while(1)  
    {  
        time(&t);  
        t_buf = ctime(&t);
        printf("%s",t_buf);
        fprintf(fp,"%s",t_buf);  

        fflush(fp);  
        sleep(1);  
    }  

    fclose(fp);  

    return 0;  
}  

执行结果如下:

yDwHUS.png

我们可以看到日期已经写到1.txt文件中了。

2.3从标准输入输出看流和缓冲区

学习标准输入输出,我们都会遇到一个概念,流和缓冲区,但到底什么是流,什么是缓冲区呢?

书《C Primer Plus》上说,C程序处理一个流而不是直接处理文件。后面的解释十分抽象:流(stream)是一个理想化的数据流,实际输入或输出映射到这个数据流』。这个流具体是一个怎么样的东西呢?

流这个定义非常的形象。我们可以这样理解:

你声明一个FILE *fp ,并把fopen(某个文件)返回的值赋予fp这两个动作就相当于建立了一个水龙头,当你用getc(fp)之类的输入函数读取文件字符时就相当于拧开了水龙头,每读取一个字符,这个文件就像水一样的流动一下,fp所指的地址自然就向后移动了一位。

int ch;  
while((ch=getc(fp))!=EOF)  
    putchar(ch);  

你看这个循环,可以读取一个文件的所有字符。如果不是流的话,ch永远是第一个字符,不会更新。也可以理解为,fp自动++(一个字符的大小)。

但流的概念意味着什么呢?
1.流是独立于设备之外而操纵外设一种逻辑手段。
2.大多数外设都是互异的,所以(操纵)它们需要专门的编程技术。
3.流对程序员隐藏这些不同点,而准许他们以同样的方式来处理大多数外设。
4.考虑到一连串的字符需要一次读一个,流(相当于)是具有缓冲作用的接口。
5.个人计算机都是基于流架构的。

各大权威对流的说法有些不一致,我认为流既是数据的源或目的地的抽象,也是源和目的地之间流动信息的表示。但流起码都暗含以下的几个方面:
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()操作,流自动打开。

那缓冲区又是什么意思呢?

缓冲区(Buffer):
为了匹配计算机快速设备和慢速设备间的通信步伐,计算机中大量使用硬件缓冲区(如CPU中的Cache,内存相对于硬盘和CPU),流是传输信息的一种逻辑表示,对流的各种不同操作也可能存在使用缓冲的需求。但是这里的buffer只是一种逻辑概念,不是物理设备。缓冲区存在于流与具体的设备终端或者存储介质上的文件之间。就好像运货到一个公司里一样,合同上的要求是运到X公司,但是实际上是真的把货物运到X公司的总部大楼吗?不是。应该是运到X公司的仓库中。这里的仓库就有点像我们所说的缓冲区了。也可以这么说,流运动到目的,先经过的是缓存区。

当然,我们常用的scanf() 与 printf() 属于行缓冲,下面我们来看个例子,可以帮助我们理解缓冲区在标准输入输出中的作用:

#include <stdio.h>  
int main()  
{  
    printf("hello world");  
    while(1);  
}  

我们看看输出结果:

yDwO3j.png

打出是个空的,为什么呢?

我们上面提到标准输入输出是行缓冲,即一行满了才会刷新,那什么是刷新呢?刷新就是将数据从缓冲区取出来,真正能刷新,要满足什么条件呢?

1、满刷新,即一行满了(1024个字节)才会刷新;
2、遇到’\n’会刷新;
3、调用fflush()函数;
4、程序结束 fclose();
我们可以看到上面的程序,应为有while(1),程序一直没有结束,没有’\n’,没有满行,没有fflush(),所以并不会输出;

这样理解的话,我们可以改动一下了,就写一个吧,加’\n’:

#include <stdio.h>  
int main()  
{  
    printf("helloworld\n");  
    while(1);  
}  

执行结果如下:

yDwXgs.png

可以看到打印出来了,其他方法在这就不写了,大家可有从这个简单的例子中看到缓冲区与流的概念。

Related posts

Leave a Comment