这一章的主题是--缓冲式I/O
一,流与缓冲
流I/O是由C语言的标准函数提供的,这些I/O可以替代系统中提供的read和write函数。事实上流I/O的内
部封装了这两个基本的文件读写系统调用。使用流I/O在某些程度上来讲要方便一些,这些I/O在效率上没有
特别大的差异。
基于流的操作最终会调用read或者write函数进行操作。为了使程序的运行效率最高,流对象通常会提
供缓冲区,以减少调用系统I/O库函数的次数。
基于流的I/O提供以下2种缓冲:
1,全缓冲:直到缓冲区填满,才调用系统I/O函数。对于读操作来说,直到读入的内容的字节数等于缓冲区大
小或者文件以经到达结尾,才进行I/O操作将外存文件内容读入缓冲区;对于写操作来说,直到缓冲区填满,
才进行实际的I/O操作将缓冲区内容写到外存文件中。磁盘文件通常是全缓冲的。
2,行缓冲:直到遇到换行符\n才调用系统I/O函数。对于读操作来说,遇到换行符\n才进行I/O操作,将所
读内容写入缓冲区;对于写操作来说,遇到换行符\n才进行I/O操作,将缓冲区内容写到外存。由于缓冲区大
小是有限制的,所以当缓冲区填满时即使没有遇到\n,也同样会进行实际的I/O操作。标准输入stdin和标准
输出stdout都默认是行缓冲的。
3,无缓冲:没有缓冲区,数据会立即读入或者输出到外存文件和设备上。标准出错stderr是无缓冲的,这样
也能保证错误提示和输出能及时地反馈给用户,供用户排除错误。
二,基于文件流的操作
常用函数:
打开和关闭流#include
fopen函数的第一个参数表示需要打开的文件的路径,第二个参数表示打开的方式。
fdopen函数用于在一个已经打开的文件上建立一个流,第一个参数是已打开文件的文件描述符,第二个参数是与fopen函数的第二个参数一样。只有一点不同的是,由于文件已经打开,所以fdopen函数不会再创建新文件,而且也不会将文件截短为0,这一点要热别注意,这两点在打开文件描述符的时候已经完成。
Type说明如下:
type | 文件类型 | 是否新建 | 是否清空 | 可读 | 可写 | 读写位置 |
r | 文本文件 | NO | NO | YES | NO | 文件开头 |
r+ | 文本文件 | NO | NO | YES | YES | 文件开头 |
w | 文本文件 | YES | YES | NO | YES | 文件开头 |
w+ | 文本文件 | YES | YES | YES | YES | 文件开头 |
a | 文本文件 | NO | NO | NO | YES | 文件结尾 |
a+ | 文本文件 | YES | NO | YES | YES | 文件结尾 |
rb | 二进制文件 | NO | NO | YES | NO | 文件开头 |
r+b或rb+ | 二进制文件 | NO | NO | YES | YES | 文件开头 |
wb | 二进制文件 | YES | YES | NO | YES | 文件开头 |
w+b或wb+ | 二进制文件 | YES | YES | YES | YES | 文件开头 |
ab | 二进制文件 | NO | NO | NO | YES | 文件结尾 |
a+b或ab+ | 二进制文件 | YES | NO | YES | YES | 文件结尾 |
Linux里用fclose函数关闭一个文件流,函数原型如下:
#include
如果执行成功,函数返回0,失败返回EOF,这个值在定义在stdio.h中,其值为-1。fclose函数关闭文件时,该函数会将保存在内存中未来得及写回到磁盘的文件内容写回到磁盘上。了解这一点很重要,如果没有调用fclose函数,就必 须等待内存中缓冲区被填满,由系统将其内容写回到磁盘上去。对于fclose函数是否需要检查返回值的问题困扰着许多程序员。虽然严格地说应该检查所有的 系统调用的返回值,并且进行错误处理,但对于fclose函数出错的几率很小,几乎为0.但如果去关闭一个网络环境中的远程文件,fclose函数就有可 能出错。由于fclose函数在关闭文件时会将缓冲区的内容写回到磁盘上,因此fclose函数实际是进行了一个写操作。在网络环境中,文件的内容是要通 过网络传输到目的主机上并写入磁盘上的。在这个传输过程中,如果网络链接出现问题或者传输数据出错,就会导致文件内容写入失败。这时fclose函数就会 出错。由此可知,如果在本地关闭一个文件可以不用检查返回值;如果在网络环境中关闭一个文件,检查fclose函数的返回值是有必要的。
三,以字符为单位读写数据
每次读写一个字符数据的I/O方式称为每次一个字符的I/O。Linux下使用fgetc函数获得一个字符,其函数原型如下:
#include
函数如果执行成功则返回该字符的ASCLL值,如果执行失败,则返回EOF。
Linux环境下使用fputc函数输出一个字符数据,函数原型如下:
#include
第一个参数表示想要输出的字符的ASCLL值(源),第二个参数表示想要输出的文件流(目的地)。
四,以行为单位读写数据
当输入内容遇到\n时则将流中\n之前的内容送到缓冲区中的I/O方式称为每一次行的I/O。Linux使用下列函数提供一次读入一行的功能。
#include
fgets函数的第一个参数表示存放读入的缓冲区,第二个参数n表示读入的字符个数,此参数的最大值不能超过缓冲区的长度。fgets函数一直读,直到遇到\n为止,如果在n-1个字符內未遇到换行符,则只读入n-1个字符。最后一个字符用于存储字符串结束标志\0.需要注意的是fgets函数会将‘\n’换行符也读进缓冲区中,因此缓冲区的实际有效内容应该是缓冲区实际字节数(不包括‘\0’)减1.fgets函数的第三个参数是需要读入的流对象。
fgets函数的换回值有以下两种情况:1,成功读取一行,返回缓冲区的首地址。2,读取出错或者文件已经到达结尾则返回NULL。
gets函数和fgets函数类似,该函数从标准输入流中读取一行并将其存入一个缓冲区,并不将‘\n’读进缓冲区中。gets函数的返回值和fgets相同。
Linux 环境下用fputs函数和puts函数实现输出一行字符串,其函数原型如下:
#include
puts函数的第一个参数表示存放输出内容的缓冲区,第二个参数表示要输出的文件。如果执行成功则返回输出的字节数,失败返回-1。puts函数用与向标准输出输出一行字符串,其参数和fputs函数的第一个参数相同,如果成功输出,则返回输出的字节数,失败则返回-1,值得注意的是,虽然gets函数不读入\n,但是puts函数却输出\n。fputs和puts函数都不输出字符串的结束符‘\0’。对于I/O来说,fputs函数和fgets函数的搭配是安全又可靠的。
五,gets函数的漏洞
gets函数和fgets函数最大的不同是gets函数的缓冲区虽然由用户提供,但是用户无法指定其一次最多读入多少个字节的内容。这一点导致gets函数变成了一个危险的函数。
把数据写到文件效率最高的方法是用二进制形式写入。二进制输出避免了在数值转换为字符串过程中所涉及的开销和精度损失。但二进制数据并非人眼所能阅读。所以该方法只有当数据被另一个程序按顺序读取时才能使用。
fread函数用于读取二进制数据,fwrite函数用于写入二进制数据。
1.fread()和fwirte函数原型size_t fread(void *buffer,size_t size,size_t count,FILE *stream);size_t fwirte(void *buffer,size_t size,size_t count,FILE *stream);2.buffer是一个指向用于保存数据的内存位置的指针,size是缓冲区中每个元素的字节数,count是读取或写入的元素数,当然stream是数据读取或写入的留。3.函数的返回值是实际读取或写入的元素(并非字节数目)。如果输入过程中遇到了文件末尾或者输出过程中出现了错误,这个数字可能比请求的元素数目要小。
注意和示例
在使用 fread 读二进制文件(png 图片)的时候, 发现读取到内存中的数据和 二进制文件中的数据不一致, 同样, 在 使用 fwrite 写二进制文件(png 图片)的时候, 发现写入到内存中的数据和 二进制文件中的数据和内存中的数据也不一致。
就是在读写二进制文件的时候,必须确保文件的打开形式是以 二进制读写的形式打开的, 即:文件的打开形式必须是 "rb", "wb" 要不然,读写数据的时候,就会出现错误。
/*
* 函数说明: 写二进制文件
* 参数描述: _fileName, 文件名称
* _buf, 要写的内存缓冲。
* _bufLen, 内存缓冲的长度
* 返回值: 0, 成功
* -1, 失败
*
*/
int writeFile(const STR* _fileName, void* _buf, int _bufLen)
{FILE * fp &#61; NULL;if( NULL &#61;&#61; _buf || _bufLen <&#61; 0 ) return (-1);fp &#61; fopen(_fileName, "wb"); // 必须确保是以 二进制写入的形式打开if( NULL &#61;&#61; fp ){return (-1);}fwrite(_buf, _bufLen, 1, fp); //二进制写fclose(fp);fp &#61; NULL;return 0;
}/** 函数说明: 读二进制文件
* 参数描述: _fileName, 文件名称
* _buf, 读出来的数据存放位置
* _bufLen, 数据的长度信息
* 返回值: 0, 成功
* -1, 失败
*
*/
int readFile(const char* _fileName, void* _buf, int _bufLen)
{FILE* fp &#61; NULL;if( NULL &#61;&#61; _buf || _bufLen <&#61; 0 ) return (-1);fp &#61; fopen(_fileName, "rb"); // 必须确保是以 二进制读取的形式打开 if( NULL &#61;&#61; fp ){return (-1);}fread(_buf, _bufLen, 1, fp); // 二进制读fclose(fp);return 0;
}
fread函数和fwrite函数
1.函数功能
用来读写一个数据块。
2.一般调用形式
fread(buffer,size,count,fp);
fwrite(buffer,size,count,fp);
3.说明
&#xff08;1&#xff09;buffer&#xff1a;是一个指针&#xff0c;对fread来说&#xff0c;它是读入数据的存放地址。对fwrite来说&#xff0c;是要输出数据的地址。
&#xff08;2&#xff09;size&#xff1a;要读写的字节数&#xff1b;
&#xff08;3&#xff09;count:要进行读写多少个size字节的数据项&#xff1b;
&#xff08;4&#xff09;fp:文件型指针。
#include
刷新一个流--fflush
#include
fflush 的返回值类型是int类型&#xff0c;那么这个int类型具体的返回是什么呢&#xff1f;
返回值&#xff1a;
如果成功刷新,fflush返回0。指定的流没有缓冲区或者只读打开时也返回0值。返回EOF指出一个错误。
注意:如果fflush返回EOF,数据可能由于写错误已经丢失。
错误和EOF
它是end of file的缩写&#xff0c;表示"文字流"&#xff08;stream&#xff09;的结尾。这里的"文字流"&#xff0c;可以是文件&#xff08;file&#xff09;&#xff0c;也可以是标准输入&#xff08;stdin&#xff09;。
比如&#xff0c;下面这段代码就表示&#xff0c;如果不是文件结尾&#xff0c;就把文件的内容复制到屏幕上。
int c;while ((c &#61; fgetc(fp)) !&#61; EOF) {putchar (c);}
很自然地&#xff0c;我就以为&#xff0c;每个文件的结尾处&#xff0c;有一个叫做EOF的特殊字符&#xff0c;读取到这个字符&#xff0c;操作系统就认为文件结束了。
但是&#xff0c;后来我发现&#xff0c;EOF不是特殊字符&#xff0c;而是一个定义在头文件stdio.h的常量&#xff0c;一般等于-1。
#define EOF (-1)
于是&#xff0c;我就困惑了。
如果EOF是一个特殊字符&#xff0c;那么假定每个文本文件的结尾都有一个EOF&#xff08;也就是-1&#xff09;&#xff0c;还是可以做到的&#xff0c;因为文本对应的ASCII码都是正值&#xff0c;不可能有负值。但是&#xff0c;二进制文件怎么办呢&#xff1f;怎么处理文件内部包含的-1呢&#xff1f;
这个问题让我想了很久&#xff0c;后来查了资料才知道&#xff0c;在Linux系统之中&#xff0c;EOF根本不是一个字符&#xff0c;而是当系统读取到文件结尾&#xff0c;所返回的一个信号值&#xff08;也就是-1&#xff09;。至于系统怎么知道文件的结尾&#xff0c;资料上说是通过比较文件的长度。
所以&#xff0c;处理文件可以写成下面这样&#xff1a;
int c;while ((c &#61; fgetc(fp)) !&#61; EOF) {do something}
这样写有一个问题。fgetc()不仅是遇到文件结尾时返回EOF&#xff0c;而且当发生错误时&#xff0c;也会返回EOF。因此&#xff0c;C语言又提供了feof()函数&#xff0c;用来保证确实是到了文件结尾。上面的代码feof()版本的写法就是&#xff1a;
int c;while (!feof(fp)) {c &#61; fgetc(fp);do something;}
但是&#xff0c;这样写也有问题。fgetc()读取文件的最后一个字符以后&#xff0c;C语言的feof()函数依然返回0&#xff0c;表明没有到达文件结尾&#xff1b;只有当fgetc()向后再读取一个字符&#xff08;即越过最后一个字符&#xff09;&#xff0c;feof()才会返回一个非零值&#xff0c;表示到达文件结尾。
所以&#xff0c;按照上面这样写法&#xff0c;如果一个文件含有n个字符&#xff0c;那么while循环的内部操作会运行n&#43;1次。所以&#xff0c;最保险的写法是像下面这样&#xff1a;
int c &#61; fgetc(fp);while (c !&#61; EOF) {do something;c &#61; fgetc(fp);}if (feof(fp)) {printf("\n End of file reached.");} else {printf("\n Something went wrong.");}
除了表示文件结尾&#xff0c;EOF还可以表示标准输入的结尾。
int c;while ((c &#61; getchar()) !&#61; EOF) {putchar(c);}
但是&#xff0c;标准输入与文件不一样&#xff0c;无法事先知道输入的长度&#xff0c;必须手动输入一个字符&#xff0c;表示到达EOF。
Linux中&#xff0c;在新的一行的开头&#xff0c;按下Ctrl-D&#xff0c;就代表EOF&#xff08;如果在一行的中间按下Ctrl-D&#xff0c;则表示输出"标准输入"的缓存区&#xff0c;所以这时必须按两次Ctrl-D&#xff09;&#xff1b;Windows中&#xff0c;Ctrl-Z表示EOF。&#xff08;顺便提一句&#xff0c;Linux中按下Ctrl-Z&#xff0c;表示将该进程中断&#xff0c;在后台挂起&#xff0c;用fg命令可以重新切回到前台&#xff1b;按下Ctrl-C表示终止该进程。&#xff09;
那么&#xff0c;如果真的想输入Ctrl-D怎么办&#xff1f;这时必须先按下Ctrl-V&#xff0c;然后就可以输入Ctrl-D&#xff0c;系统就不会认为这是EOF信号。Ctrl-V表示按"字面含义"解读下一个输入&#xff0c;要是想按"字面含义"输入Ctrl-V&#xff0c;连续输入两次就行了。