APUE 标准I/O库

《UNIX环境高级编程》第5章 标准I/O库 笔记

流和FILE对象

之前所描述的所有I/O函数都是围绕文件描述符的。当打开一个文件时,即返回一个文件描述符,然后该文件描述符就用于后续的I/O操作。而对于标准I/O库,它们的操作是围绕流(stream)进行。当标准I/O库打开或者创建一个文件时,我们已使一个流与一个文件相关联

对于ASCI字符集,一个字符用一个字节表示。对于国际字符集,一个字符可用多个字节表示。标准IO文件流可用于单字节或多字节(“宽”)字符集。流的定向( stream’s orientation)决定了所读、写的字符是单字节还是多字节的。fwide函数用于设置流的定向

1
2
3
4
5
6
#include <stdio.h>
#include <wchar.h>
int fwide(FILE *fp, int mode);
// Returns: positive if stream is wide oriented, negative if stream is byte oriented, or 0 if stream has no orientation

mode参数设置为负时为字节定向、为正时为宽定向、为0时不设置流的定向、但是返回该流定向的值

当打开一个流时,标准IO函数 fopen返回一个指向FILE对象的指针。为了引用一个流,需将FILE指针作为参数传给每个标准I/O函数。

标准输入、标准输出和标准错误

对一个进程预定义了3个流,并且这3个流可以自动地被进程使用,它们是:标准输入、标准输出和标准错误。这些流引用的文件与文件描述符 STDIN_FILENO、 STDOUT_FILENO和STDERR_FILENO所引用的相同。

缓冲

标准I/O库提供缓冲的目的是尽可能减少使用read和write调用的次数。标准I/O库最令人迷人的也是其缓冲:

标准I/O提供了以下3种类型的缓冲:

  1. 全缓冲。在这种情况下,在填满标准I/O缓冲区后才进行实际I/O操作。

术语冲洗(flush)说明标准I/O缓冲区的写操作。缓冲区可由标准I/O例程自动地冲洗,或者可以调用函数ff1ush冲洗一个流。

  1. 行缓冲,在这种情况下,当在输入和输出中遇到换行符时,标准I/O库执行I/O操作。当流涉及一个终端时(如标准输入和标准输出),通常使用行缓冲。

对于行缓冲有两个限制。第一,因为标准/O库用来收集每一行的缓冲区的长度是固定的,所以只要填满了缓冲区,那么即使还没有写一个换行符,也进行O操作。第二,任何时候只要通过标准I/O库要求从(a)一个不带缓冲的流,或者(b)一个行缓冲的流(它从内核请求需要数据)得到输入数据,那么就会冲洗所有行缓冲输出流。

  1. 不带缓冲。标准I/O库不对字符进行缓冲存储。

对任何一个给定的流,如果我们并不喜欢这些系统默认,则可调用下列两个函数中的一个更改缓冲类型:

1
2
3
4
5
6
#include <stdio.h>
void setbuf(FILE *restrict fp, char *restrict buf ); // 打开或者关闭缓冲机制
int setvbuf(FILE *restrict fp, char *restrict buf, int mode, size_t size);
// Returns: 0 if OK, nonzero on error

使用setvbuf,我们可以精确说明所需的缓冲类型。这是用mode参数实现的:_IOFBF 全缓冲 _IOLBF 行缓冲 _IONBF 不带缓冲

任何时候,我们可以强制冲洗一个流:

1
2
3
4
5
#include <stdio.h>
int fflush(FILE *fp);
// Returns: 0 if OK, EOF on error

打开流

下列3个函数打开一个标准I/O流

1
2
3
4
5
6
7
#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);
// All three return: file pointer if OK, NULL on error

3个函数的区别如下:

  1. fopen函数打开路径名为pathname的一个指定的文件。
  2. freopen函数在一个指定的流上打开一个指定的文件,如若该流已经打开,则先关闭该流。若该流已经定向,则使用 freopen清除该定向。此函数一般用于将一个指定的文件打开为一个预定义的流:标准输入、标准输出或标准错误。
  3. fopen函数取一个已有的文件描述符,并使一个标准的I/O流与改描述符相结合

type参数指定对该流的读写方式

使用字符b作为type的部分,这使得标准I/O系统可以区分文本文件和二进制文件。因为UNIX内核并不对这两种文件进行区分,所以在UNIX系统环境下指定字符b作为type的部分实际上并无作用。

调用fclose关闭一个打开的流:

1
2
3
4
5
#include <stdio.h>
int fclose(FILE *fp);
// Returns: 0 if OK, EOF on error

读和写流

一旦打开了流,则可在3种不同类型的非格式化I/O中进行选择,对其进行读、写操作:

  1. 每次一个字符I/O
  2. 每次一行I/O
  3. 直接I/O

输入函数

以下3个函数可以用于一次读一个字符:

1
2
3
4
5
6
7
#include <stdio.h>
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);
// All three return: next character if OK, EOF on end of file or error

函数getchar等同于getc(stdin)。前两个函数的区别是,getc可以被实现为宏,而fgetc不能被实现为宏

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

1
2
3
4
5
6
7
8
#include <stdio.h>
int ferror(FILE *fp);
int feof(FILE *fp);
// Both return: nonzero (true) if condition is true, 0 (false) otherwise
void clearerr(FILE *fp);

大多数情况下,每一个流的FILE对象中都维护了两个标志:出错标志和文件结束标志。调用clearerr可以清楚这些标志。

从流中读取数据以后,可以调用 ungetc将字符再压送回流中

1
2
3
4
5
#include <stdio.h>
int ungetc(int c, FILE *fp);
// Returns: c if OK, EOF on error

输出函数

对应于上面每一个输入函数都有一个输出函数:

1
2
3
4
5
6
7
#include <stdio.h>
int putc(int c, FILE *fp);
int fputc(int c, FILE *fp);
int putchar(int c);
// All three return: c if OK, EOF on error

每一行 I/O

以下两个函数提供每次输入一行的功能

1
2
3
4
5
6
#include <stdio.h>
char *fgets(char *restrict buf, int n, FILE *restrict fp);
char *gets(char *buf );
// Both return: buf if OK, NULL on end of file or error

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

对于 fgets,必须指定缓冲的长度n。此函数一直读到下一个换行符为止,但是不超过n个字符,读入的字符被送入缓冲区。

gets是一个不推荐使用的函数。其问题是调用者在使用qets时不能指定缓冲区的长度。这样就可能造成缓冲区溢出(如若该行长于缓冲区长度),写到缓冲区之后的存储空间中,从而产生不可预料的后果。这种缺陷可以被利用制作蠕虫病毒。

fputs和puts提供每次输出一行的功能

1
2
3
4
5
6
#include <stdio.h>
int fputs(const char *restrict str, FILE *restrict fp);
int puts(const char *str);
// Both return: non-negative value if OK, EOF on error

函数 fputs将一个以nul字节终止的字符串写到指定的流,尾端的终止符nu不写出。

二进制I/O

之前提到的I/O函数函数以一次一个字符或一次一行的方式进行操作。如果进行二进制I/O操作,那么我们更愿意一次读或写一个完整的结构。

有下面两个函数提供二进制的I/O操作:

1
2
3
4
5
6
7
8
#include <stdio.h>
size_t fread(void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);
size_t fwrite(const void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);
// Both return: number of objects read or written

这些函数有两个用法:

  1. 读写二进制数组
  2. 读写自定义的一个结构体

其中参数nobj表示要读写的对象的个数

使用二进制I/O的基本问题是,它只能用于读在同一系统上已写的数据。这样就导致了一个问题:在一个系统写的数据要在另一个系统上进行处理。在这种环境下,这两个函数可能就不能正常工作,其原因是:

  1. 在一个结构中,同一成员的偏移量可能随编译程序和系统的不同而不同(由于不同的对齐要求)。
  2. 用来存储多字节整数和浮点值的二进制格式在不同的系统结构间也可能不同

实现细节

在UNIX中,标准I/O库最终都要调用I/O例程来实现(read、write)。每个标准I/O流都有一个与其相关联的文件描述符,可以对一个流调用fi1eno函数以获得其描述符。

1
2
3
4
5
#include <stdio.h>
int fileno(FILE *fp);
// Returns: the file descriptor associated with the stream

临时文件

标准I/O库提供两个函数来创建临时文件:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
char *tmpnam(char *ptr);
// Returns: pointer to unique pathname
FILE *tmpfile(void);
// Returns: file pointer if OK, NULL on error

tmpnam函数产生一个与现有文件名不同的一个有效路径名字符串。每次调用它时,都产生个不同的路径名,最多调用次数是 TMP_MAX

若ptr是NULL,则所产生的路径名存放在一个静态区中,指向该静态区的指针作为函数值返回。

tmpfile创建一个临时二进制文件(类型wb+),在关闭该文件或程序结束时将自动删除这种文件

tmpfi1e函数经常使用的标准UNIX技术是先调用 tmpnam产生一个唯一的路径名,然后用该路径名创建一个文件,并立即un1ink它。

内存流

标准I/O库把数据缓存在内存中,因此每次一字符和每次一行的I/O更有效。我们也可以通过调用 setbuf或 setvbuf函数让IO库使用我们自己的缓冲区。

有3个函数可以用于内存流的创建,第一个函数是fmemopen函数:

1
2
3
4
5
#include <stdio.h>
FILE *fmemopen(void *restrict buf, size_t size, const char *restrict type);
// Returns: stream pointer if OK, NULL on error