流和缓冲

阅读更多
    对于不带缓冲的 I/O 库而言,大多数操作都是围绕文件描述符展开的。当打开一个文件时,就返回一个文件描述符,然后该文件描述符就用于后续的 I/O 操作。而对于标准 I/O 库,其操作则是围绕流(stream)进行的。当用标准 I/O 库打开或创建一个文件时,就将一个流与该文件关联起来了。
    标准 I/O 文件流可用于单字节或多字节(“宽”)字符集,这是由流的定向(stream's orientation)来决定的。流最初被创建时是没有定向的,如若此时在该流上使用一个多字节 I/O 函数(见),则将流的定向设置为宽定向的;反之则设置为字节定向的。只有两个函数可改变流的定向。fwide 函数可用于设置流的定向,freopen 函数(见打开流函数)则可清除一个流的定向。
#include
#include
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 函数。
    对一个进程预定义了三个流,即标准输入、标准输出和标准错误,分别用定义在头文件中的三个预定义文件指针 stdin、stdout 和 stderr 加以引用。这些流可自动地被进程使用,它们引用的文件分别与文件描述符 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO 所引用的相同。
    对于任何一个给定流的系统默认缓冲类型,我们也可使用下列两个函数之一来更改(这两个函数应该在对打开流执行任何一个其他操作之前调用)。
#include 
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 的缓冲区(该常量位于中)。通常在此之后该流就是全缓冲的,但是如果该流是与一个终端设备相关,那么某些系统也可将其设置为行缓冲的。为了关闭缓冲,将 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 缓冲区长度)。
    下表总结了这两个函数的动作,以及它们的各个选项。
流和缓冲_第1张图片
    如果在一个函数内分配一个自动变量类的标准 I/O 缓冲区,则从该函数返回前必须先关闭该流。另外,某些实现将缓冲区的一部分用于存放它自己的管理操作信息,所以可以存放在缓冲区中的实际数据字节数少于 size。一般而言,应由系统选择缓冲区的长度,并自动分配缓冲区。在这种情况下关闭此流时,标准 I/O 库将自动释放缓冲区。
    任何时候,都可使用 fflush 函数强制冲洗一个流,它会把该流中所有未写的数据都传送至内核。如若其参数 fp 是 NULL,则此函数将导致所有输出流被冲洗。
#include 
int fflush(FILE *fp);    /* 返回值:若成功,返回 0;否则,返回 EOF */

    要打开一个标准 I/O 流,可使用下列 3 个函数。
#include 
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 参数指定对该流的读、写方式。其可选值如下表所示。
流和缓冲_第2张图片
    这里使用字符 b 为 type 的一部分,可使标准 I/O 系统能够区分文本文件和二进制文件。因为 UNIX 内核并不区分这两种文件,所以在 UNIX 环境下指定字符 b 实际上并无作用。
    对于 fdopen,type 参数的意义稍有区别。因为该描述符已被打开,所以 fdopen 为写而打开并不截断该文件。
    当用追加写类型打开一个文件后,每次写都将数据写到文件的当前尾端处。如果有多个进程用标准 I/O 追加写方式打开同一文件,那么来自每个进程的数据都将被正确地写到文件中。
    当以读和写类型打开一个文件时,具有下列限制:
    * 如果中间没有 fflush、fseek、fsetpos 或 rewind,则在输出的后面不能直接跟随输入。
    * 如果中间没有 fseek、fsetpos 或 rewind,或者一个输入操作没有到达文件尾端,则在输入操作之后不能直接跟随输出。
    要关闭一个流,可使用 fclose 函数。
#include 
int fclose(FILE *fp);    /* 返回值:若成功,返回 0;否则,返回 EOF */

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

    另外,在 SUSv4 中还支持了内存流,虽然仍使用 FILE 指针进行访问,但其实并没有底层文件,所有的 I/O 都是通过缓冲区与主存之间来回传送字节来完成的。因为避免了缓冲区溢出,内存流非常适合于创建字符串。因为内存流只访问主存,不访问磁盘文件,所以对于把标准 I/O 流作为参数用于临时文件的函数来说,会有很大的性能提升。
    有 3 个函数可用于内存流的创建。
#include 
FILE *fmemopen(void *restrict buf, size_t size, const char *restrict type);
FILE *open_memstream(char **bufp, size_t *sizep);
#include 
FILE *open_wmemstream(wchar_t **bufp, size_t *sizep);
                          /* 返回值:若成功,都返回流指针;否则,都返回 NULL */

    fmemopen 函数允许调用者提供缓冲区用于内存流:buf 参数指向缓冲区的开始位置,size 指定了缓冲区大小的字节数。如果 buf 为空,fmemopen 将分配 size 个字节的缓冲区。这种情况下,当流关闭时缓冲区会被释放。type 参数控制如何使用流,其可能的取值如下表所示。
流和缓冲_第3张图片
    注意,这些取值与标准 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 
#include 
#include 

#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 字节会自动追加写。流内容的大小是由写入多少来确定的。
  • 流和缓冲_第4张图片
  • 大小: 22.5 KB
  • 流和缓冲_第5张图片
  • 大小: 23.2 KB
  • 流和缓冲_第6张图片
  • 大小: 12.5 KB
  • 查看图片附件

你可能感兴趣的:(标准,I/O,库,stream)