流和缓冲

对于不带缓冲的 I/O 库而言,大多数操作都是围绕文件描述符展开的。当打开一个文件时,就返回一个文件描述符,然后该文件描述符就用于后续的 I/O 操作。而对于标准 I/O 库,其操作则是围绕流(stream)进行的。当用标准 I/O 库打开或创建一个文件时,就将一个流与该文件关联起来了。
标准 I/O 文件流可用于单字节或多字节(“宽”)字符集,这是由流的定向(stream's orientation)来决定的。流最初被创建时是没有定向的,如若此时在该流上使用一个多字节 I/O 函数(见<wchar.h>),则将流的定向设置为宽定向的;反之则设置为字节定向的。只有两个函数可改变流的定向。fwide 函数可用于设置流的定向,freopen 函数(见打开流函数)则可清除一个流的定向。

#include<stdio.h>
#include<wchar.h>
int fwide(FILE *fp, int mode);
/* 返回值:宽定向的流返回正值;字节定向的流返回负值;未定向的流返回 0 */

根据 mode 参数的值,fwide 函数执行不同的工作。
1、若 mode 为负,fwide 将试图使指定的流是字节定向的。
2、若 mode 为正,fwide 将试图使指定的流是宽定向的。
3、若 mode 为 0,fwide 将不试图设置流的定向,但返回标识该流定向的值。
注意,fwide 并不改变已定向流的定向,而且也无出错返回。我们唯一可依靠的是,在调用 fwide 前先清除 errno,待 fwide 返回后再检查 errno 的值。
当打开一个流时,标准 I/O 函数 fopen 返回一个指向 FILE 对象的文件指针。该对象通常是一个结构,它包含了标准 I/O 库为管理该流需要的所有信息,包括用于实际 I/O 的文件描述符、指向用于该流缓冲区的指针、缓冲区的长度、当前在缓冲区中的字符数以及出错标志等。应用程序没必要检验 FILE 对象。为了引用一个流,需将 FILE 指针作为参数传递给每个标准 I/O 函数。
对一个进程预定义了三个流,即标准输入、标准输出和标准错误,分别用定义在头文件<stdio.h>中的三个预定义文件指针 stdin、stdout 和 stderr 加以引用。这些流可自动地被进程使用,它们引用的文件分别与文件描述符 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO 所引用的相同。
对于任何一个给定流的系统默认缓冲类型,我们也可使用下列两个函数之一来更改(这两个函数应该在对打开流执行任何一个其他操作之前调用)。

#include <stdio.h>
void setbuf(FILE *restrict fp, char *restrict buf);
int setvbuf(FILE *restrict fp, char *restrict buf, int mode, size_t size);
/* 返回值:若成功,返回 0;否则,返回非 0 */

可使用 setbuf 函数打开或关闭缓冲机制。为了带缓冲进行 I/O,参数 buf 必须指向一个长度为 BUFSIZE 的缓冲区(该常量位于<stdio.h>中)。通常在此之后该流就是全缓冲的,但是如果该流是与一个终端设备相关,那么某些系统也可将其设置为行缓冲的。为了关闭缓冲,将 buf 设置为 NULL。
使用 setvbuf,我们可以利用 mode 参数精确地说明所需的缓冲类型:
1、_IOFBF:全缓冲;
2、_IOLBF:行缓冲;
3、_IONBF:不带缓冲。
若指定一个不带缓冲的流,则忽略 buf 和 size 参数。如果指定全缓冲或行缓冲,则 buf 和 size 可选择地指定一个缓冲区及其长度。如果该流是带缓冲的,而 buf 是 NULL,则标准 I/O 库将根据 BUFSIZE 的值自动地为该流分配适当长度的缓冲区(某些 C 函数库实现使用 stat 结构中的成员 st_blksize 的值来决定最佳 I/O 缓冲区长度)。
下表总结了这两个函数的动作,以及它们的各个选项。
[img]http://dl2.iteye.com/upload/attachment/0126/3061/e0d516b6-3e51-3b03-8692-bb6849ddef7f.png[/img]
如果在一个函数内分配一个自动变量类的标准 I/O 缓冲区,则从该函数返回前必须先关闭该流。另外,某些实现将缓冲区的一部分用于存放它自己的管理操作信息,所以可以存放在缓冲区中的实际数据字节数少于 size。一般而言,应由系统选择缓冲区的长度,并自动分配缓冲区。在这种情况下关闭此流时,标准 I/O 库将自动释放缓冲区。
任何时候,都可使用 fflush 函数强制冲洗一个流,它会把该流中所有未写的数据都传送至内核。如若其参数 fp 是 NULL,则此函数将导致所有输出流被冲洗。

#include <stdio.h>
int fflush(FILE *fp); /* 返回值:若成功,返回 0;否则,返回 EOF */

要打开一个标准 I/O 流,可使用下列 3 个函数。

#include <stdio.h>
FILE *fopen(const char *restrict pathname, const char *restrict type);
FILE *freopen(const char *restrict pathname, const char *restrict type, FILE *restrict fp);
FILE *fdopen(int fd, const char *type);
/* 返回值:若成功,都返回文件指针;否则,都返回 NULL */

这 3 个函数的区别如下:
1、fopen 函数打开路径名为 pathname 的一个指定的文件。
2、freopen 在一个指定的流上打开一个指定的文件,如若该流已打开,则先关闭该流。若该流已经定向,则使用 freopen 清除该定向。此函数一般用于将一个指定的文件打开为一个预定义的流:标准输入、标准输出或标准错误。
3、fdopen 取一个已有的文件描述符,并使一个标准的 I/O 流与该描述符相结合。此函数常用于由创建管道和网络通信管道函数返回的描述符。因为这些特殊类型的文件不能用标准 I/O 函数 fopen 打开,所以必须先调用设备专用函数以获得一个文件描述符,然后用 fdopen 使一个标准 I/O 流与其相结合。
type 参数指定对该流的读、写方式。其可选值如下表所示。
[img]http://dl2.iteye.com/upload/attachment/0126/3348/da129ec6-5fc2-37c3-a1b1-7bb3d5a932de.png[/img]
这里使用字符 b 为 type 的一部分,可使标准 I/O 系统能够区分文本文件和二进制文件。因为 UNIX 内核并不区分这两种文件,所以在 UNIX 环境下指定字符 b 实际上并无作用。
对于 fdopen,type 参数的意义稍有区别。因为该描述符已被打开,所以 fdopen 为写而打开并不截断该文件。
当用追加写类型打开一个文件后,每次写都将数据写到文件的当前尾端处。如果有多个进程用标准 I/O 追加写方式打开同一文件,那么来自每个进程的数据都将被正确地写到文件中。
当以读和写类型打开一个文件时,具有下列限制:
* 如果中间没有 fflush、fseek、fsetpos 或 rewind,则在输出的后面不能直接跟随输入。
* 如果中间没有 fseek、fsetpos 或 rewind,或者一个输入操作没有到达文件尾端,则在输入操作之后不能直接跟随输出。
要关闭一个流,可使用 fclose 函数。

#include <stdio.h>
int fclose(FILE *fp); /* 返回值:若成功,返回 0;否则,返回 EOF */

在该文件被关闭之前,冲洗缓冲区中的输出数据,丢弃缓冲区中的任何输入数据。如果标准 I/O 库已经为该流自动分配了一个缓冲区,则释放此缓冲区。
当一个进程正常终止时,则所有带未写缓冲数据的标准 I/O 流都将被冲洗,所有打开的标准 I/O 流都将被关闭。

另外,在 SUSv4 中还支持了内存流,虽然仍使用 FILE 指针进行访问,但其实并没有底层文件,所有的 I/O 都是通过缓冲区与主存之间来回传送字节来完成的。因为避免了缓冲区溢出,内存流非常适合于创建字符串。因为内存流只访问主存,不访问磁盘文件,所以对于把标准 I/O 流作为参数用于临时文件的函数来说,会有很大的性能提升。
有 3 个函数可用于内存流的创建。

#include <stdio.h>
FILE *fmemopen(void *restrict buf, size_t size, const char *restrict type);
FILE *open_memstream(char **bufp, size_t *sizep);
#include <wchar.h>
FILE *open_wmemstream(wchar_t **bufp, size_t *sizep);
/* 返回值:若成功,都返回流指针;否则,都返回 NULL */

fmemopen 函数允许调用者提供缓冲区用于内存流:buf 参数指向缓冲区的开始位置,size 指定了缓冲区大小的字节数。如果 buf 为空,fmemopen 将分配 size 个字节的缓冲区。这种情况下,当流关闭时缓冲区会被释放。type 参数控制如何使用流,其可能的取值如下表所示。
[img]http://dl2.iteye.com/upload/attachment/0126/4355/8bd82d55-e9a8-3aaf-899d-604d2551595a.png[/img]
注意,这些取值与标准 I/O 流的 type 参数取值有些微小差别。
1、无论何时以追加写方式打开内存流时,当前文件位置都设为缓冲区中的第一个 null 字节,故内存流并不适合存储二进制数据(二进制数据在数据尾端之前就可能包含有多个 null 字节)。如果缓冲区不存在 null 字节,就设为缓冲区结尾的后一个字节。当流不是以追加写方式打开时,当前位置就设为缓冲区的开始位置。
2、如果 buf 参数是一个 null 指针,打开流进行读或者写都没有任何意义。因为此时缓冲区是通过 fmemopen 进行分配的,没有办法找到缓冲区的地址。
3、任何时候需要增加流缓冲区中数据量以及调用 fflush、fseek、fseeko 和 fsetpos 时都会在当前位置写入一个 null 字节。
open_memstream 函数创建的流是面向字节的,open_wmemstream 函数创建的流是面向宽字节的。它们与 fmemopen 函数的不同在于:
1、创建的流只能写打开。
2、不能指定自己的缓冲区,但可以分别通过 bufp 和 sizep 参数访问缓冲区地址和大小。
3、关闭流后需要自行释放缓冲区。
4、对流添加字节会增加缓冲区大小。
但是在缓冲区地址和大小的使用上必须遵循一些原则。第一,缓冲区地址和长度只有在调用 fclose 或 fflush 后才有效;第二,这些值只有在下一次流写入或调用 fclose 前才有效。因为缓冲区可以增长,所以可能需要重新分配,此时缓冲区的内存地址值在下一次调用 fclose 或 fflush 时会改变。
下列程序使用 fmemopen 函数演示了用已知模式填充缓冲区时流写入是如何操作的。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BSZ 48

int main(){
FILE *fp;
char buf[BSZ];

memset(buf, 'a', BSZ-2);
buf[BSZ-2] = '\0';
buf[BSZ-1] = 'X';
printf("after memset: %s, len = %ld\n", buf, (long)strlen(buf));
if((fp=fmemopen(buf, BSZ, "w+")) == NULL){
printf("fmemopen failed\n");
exit(1);
}
printf("initial buffer contents: %s\n", buf);
fprintf(fp, "hello, world");
printf("before fflush: %s\n", buf);
fflush(fp); // 文件指针此时位于 strlen("hello, world") 字节处
printf("after fflush: %s\n", buf);
printf("len of string in buf = %ld\n", (long)strlen(buf));

memset(buf, 'b', BSZ-2);
buf[BSZ-2] = '\0';
buf[BSZ-1] = 'X';
printf("after memset: %s, len = %ld\n", buf, (long)strlen(buf));
fprintf(fp, "hello, world");
fseek(fp, 0, SEEK_SET); // 把文件指针重置到 buf 开头
printf("after fseek: %s, len = %ld\n", buf, (long)strlen(buf));

memset(buf, 'c', BSZ-2);
buf[BSZ-2] = '\0';
buf[BSZ-1] = 'X';
printf("after memset: %s, len = %ld\n", buf, (long)strlen(buf));
fprintf(fp, "hello, world");
fclose(fp);
printf("after fclose: %s, len = %ld\n", buf, (long)strlen(buf));

exit(0);
}

运行结果:

$ ./fmemopenDemo.out
after memset: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, len = 46
initial buffer contents: # fmemopen 在缓冲区开始处放置 null 字节
before fflush: # 流冲洗后缓冲区才会变化
after fflush: hello, world
len of string in buf = 12 # null 字节追加到字符串结尾
after memset: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, len = 46
after fseek: bbbbbbbbbbbbhello, world, len = 24 # fseek引起缓冲区冲洗,并追加写 null字节
after memset: cccccccccccccccccccccccccccccccccccccccccccccc, len = 46
after fclose: hello, worldcccccccccccccccccccccccccccccccccc, len = 46 # 没有追加写 null 字节
$

本例子给出了冲洗内存流和追加写 null 字节的策略。写入内存流以及推进流的内容大小(相对缓冲区而言,该大小是固定的)时,null 字节会自动追加写。流内容的大小是由写入多少来确定的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值