APUE_第5章 标准IO库

5.1 引言

相对于底层的unix IO, 标准IO库处理很多细节,例如缓冲区分配, 以优化长度执行IO。 这些处理使得用户不用担心如何选择正确的缓冲区块长度(还记得3.9节IO效率的讨论, 当缓冲区块长度等于磁盘块长度的时候, IO效率最好);

5.2 流和FILE对象

底层的unix IO函数都是针对文件描述符的,函数第一个参数文件描述符fd;
对于标准IO库, 其操作是围绕流进行的。使用标准IO库打开或创建一个文件的时候,即将一个流与一个文件相关联;
标准IO文件流可用于单字节和宽字节字符集。
流动定向决定了所读、写的字符是单字节的还是多字节的。
当一个流最初创建的时候, 并没有定向。
如果在一个没有定向的流上使用多字节IO函数, 流的定向就设置为宽定向;
如果在一个没有定向的流上使用单字节IO函数, 流的定向就设置为单定向;
两个函数改变流动定向:freopen函数清楚一个流的定向, fwide函数设置流的定向;

int fwide(FILE *fp, innt mode);

mode = 负数:fwide函数使得流是单字节定向的;
mode = 正数, fwide函数使得流是宽字节定向的;
mode = 0:返回流fp的流动定向;
fwide函数并不改变已经定向的流, 第一个参数fp一定得是一个未定向流;

当打开一个流, 标准IO函数返回一个指向FILE对象的指针。FILE对象是一个结构, 包含有标准IO库为管理该流需要的所有信息,包括:
用于实际IO的文件描述符、指向用于流缓冲区的指针、缓冲区长度、当前缓冲区中的字符数,出错标识;

5.3 标准输入、标准输出、标准出错

STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO

5.4 缓冲

标准IO库的目的是尽可能减少使用read和write调用的次数,其对每一个IO流自动地进行缓存管理;
三种类型的缓冲:
1) 全缓冲。 在填满标准IO缓冲区之后才进行实际的IO操作。驻留在磁盘上的文件通常是由标准IO库实施全缓冲的。在一个流上执行第一次IO操作, 相关标准IO函数会调用malloc获取缓冲区,将流对象中的缓冲区指针指向这个缓冲区;
缓冲区可以自动冲洗(缓冲区满),或是手动调用fflush冲洗。
flush有两层含义,第一层含义对应磁盘文件, flush冲洗意味着将缓冲区中的内容写到磁盘上;第二层含义对应终端驱动程序, flush相当于丢弃缓冲区中的数据;
2) 行缓冲
当输入输出遇到换行符的时候, 执行真正的IO操作;这允许我们一次只写一个字符,但是只有写完一行之后才进行真正的IO操作;当流涉及一个终端时候, 通常使用行缓冲;

行缓冲的两个限制:
(a) 只要缓冲区满了, 即使没有换行符, 也进行IO操作;
(b) 任何时候只要通过标准IO库从不带缓冲的流中或是带缓冲的流中得到输入数据, 就会造成冲洗所有的行缓冲输出流;
3) 不带缓冲
标准IO不对字符进行缓存存储。标准出错stderr是不带缓冲存储的;

ISO C 要求以下缓冲特征:
1) 当且仅当标准输入和标准输出并不涉及交互式设备时候, 才是全缓冲的;涉及终端设备的时候是交互设备, 所以是行缓冲的;
2) 标准出错一定不是全缓冲的;
更改一个给定流动缓冲类型:

void setbuf(FILE *restrict fp, char *restrict buf);
int setvbuf(FILE *restrict fp, char *restrict buf, int mode, size_t size);
int fflush(FILE *fp);

setbuf打开或是关闭缓冲机制。打开缓冲机制的时候, 参数buf必须指定一个长度为BUFSIZE的缓冲区;当buf为NULL的时候, 关闭缓冲;
setvbuf函数选定缓存类型:
_IOFBF 全缓冲
_IOLBF 行缓冲
_IONBF 不带缓冲

如果在一个函数中分配一个自动变量类的标准IO缓冲区, 则在函数返回之前, 必须关闭该流。如果没有关闭该流, 函数返回之后,缓冲区被释放, 流中的指针指向一个已经被释放掉的内存空间,会引起错误;
一般而言, 应当由系统选择缓冲区的长度,并自动分配缓冲区。这样在关闭流动时候, 标准IO库会自动释放缓冲区;

缓存区与流的概念不同, 流数据结构中有文件描述符, 有指向缓冲区的指针;

fflush函数将所有未写的数据都传送到内核。如果fp是NULL,此函数将导致所有的输出流冲洗;

5.5 打开流

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);

fopen:打开一个文件,返回一个文件流;
freopen:在一个指定的流上打开文件,如果这个流已经打开了, 就关闭这个流;如果流已经定向,freopen清除该定向;此函数一般用于将一个指定的文件打开为一个预定义的流(输入、输出、标准出错);
fdopen:将一个标准IO流与一个文件描述符相关联;一般管道、网络通信返回的是文件描述符,不能使用fopen创建文件流,所以使用fdopen创建文件流;

type的15种值:
在这里插入图片描述
b作为byte的一部分, 使得标准IO系统可以区分文本文件和二进制文件。标准IO需要区分文件和二进制,但是底层的unix IO(内核)不能区分文本和二进制;
对于fdopen, 由于已经存在打开的文件了,已经有文件描述符fd了,所以fdopen为写打开并不截短该文件,另外添加方式也不会用于创建该文件;

当以读和写类型打开一个文件时候(type的+ 符号), 具有以下限制:
1) 如果中间没有fflush, fseek, fsetpos, rewind, 则输出后面不能直接跟随输入;
2) 如果中间没有fseek, fsetpos, rewind, 输入后面不能直接接输出;

使用fopen, freopen, fdopen 这三个函数打开的文件流, 可以使用setbuf, setvbuf改变流的定向;

调用fclose函数关闭文件流fp, 关闭文件流之前, 会冲洗缓冲区的输出数据,丢弃缓冲区的输入数据。如果缓冲区是标准IO自动分配的,会释放这个缓冲区;
当一个进程正常终止时候, 所有没有写到缓冲数据的标准IO流都会被冲洗,所有打开的标准IO流都会关闭;
总结:进程关闭时候, 数据被冲洗, 缓冲区被释放, 流被关闭;

5.6 读和定流

对流的三种不同类型的非格式化IO操作
1) 每次一个字符的IO。一次读写一个字符;标准IO函数自动处理缓冲;
2) 每次一行IO。 使用fgets, fputs函数对流进行每次一行的读写;
3) 直接IO。fread, fwrite。每次IO操作读写读写某种数量的对象,而每个对象具有指定的长度。fread, fwrite函数常用于从二进制文件中每次读写一个结构;
1) 输入函数
以下三个函数用于一次读一个字符:

int getc(FILE *fp);		//可实现为宏
int fgetc(FILE *fp);	//是一个函数
int getchar(void);//等价于getc(stdin);

getc函数和fgetc的区别在于, getc可以被实现为宏, fgetc则是函数:
1) getc的参数不应当是具有副作用的表达式, 因为getc可能是宏;
2) fgetc一定是一个函数, 有地址, 可以将fgetc作为一个地址传递给另一个函数;
本来函数返回的应该是char, 但是这里将unsigned char类型转换为int类型。返回int类型的理由是, 可以在返回所有可能的字符值基础上,加上出错值和到达文件尾端的指示值;EOF通常是-1, 出错也是返回-1;
由于出错和EOF都返回-1, 需要地道有ferror, feof 确定到底是哪一种情况;

int ferror(FILE *fp);
int feof(FILE *fp);	//判断返回值是否为真
void clearerr(FILE *fp);	//清除出错标志和EOF标志

大多数实现中, 每个流FILE对象中都维持了两个标志
1) 出错标志
2) 文件结束标志

从流中读取数据之后, 可以调用ungetc函数将字符压回到流中

int ungetc(int c, FILE *fp);

压回的字符又可以从流中读出,但是读出字符的顺序与压回字符的顺序相反(相当于一个栈)。一次只能压送一个字符;
压送的字符不必是上一次读到的字符,不能压送EOF;
有时候需要先看看下一个字符,已决定对当前字符如何处理,就需要用到压回字符的功能;

2) 输出函数

int putc(int c, FILE *fp);	//可实现为宏
int fputc(int c, FILE *fp);	//是一个函数
int putchar(int c);	//等效于putc(c, stdout);

5.7 每次一行IO

char *fgets(char *restrict buf, int n, FILE *restrict fp);
char *gets(char *buf);	//不推荐使用
int fputs(const char *restrict str, FILE *restrict fp);
int puts(const char *str);	//不推荐使用

gets是一个不推荐的函数,问题在于调用者在使用gets时不能指定缓冲区的长度,这可能造成缓冲区溢出;(fgets的缓冲区至少是n字节的,gets的缓冲区是系统默认的)
gets函数并不将换行符存入缓冲区;
fputs将一个以null终止的字符串写到指定的流;通常, null之前是一个换行符;
puts将一个以null终止的字符串写到标准输出,但是终止符不写出。然后puts将一个换行符写到标准输出;

5.8 标准IO的效率

使用getc, putc将标准输入复制到标准输出

int main(void)
{
	int c;
	while((c = getc(stdin)) != EOF)
		if(putc(c, stdout) == EOF)
			err_sys("output error");
	if(ferror(stdin))
		err_sys("input error");
	exit(0);
}

fgets 和 fputs的输入复制到输出

int main()
{
	char buf[MAXLINE];
	while(fgets(buf, MAXLINE, stdin) != NULL)
		if(fputs(buf, stdout) == EOF)
			err_sys("output error");
	if(ferror(stdin))
		err_sys("input error");
	exit(0);
}

区分STDIN_FILENO和stdin:
STDIN_FILENO是文件描述符,其值为0;
stdin是FILE *指针;

5.9 二进制IO

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);

常见用法:
1) 读写一个二进制数组;
例如:

float data[10];
if(fwrite(&data[2], sizeof(float), 4, fp) != 4)
	err_sys("fwrite error");
	
  1. 读写一个数据结构
    例如
struct{
	short count;
	long total;
	char name[NAMESIZE];
}item;
if(fwrite(&item, sizeof(item), 1, fp) != 1)
	err_sys("fwrite error");
	

fread, fwrite 只能处理同一个网络上的数据, 对于异构系统通过网络相互连接的数据,不能适用fread, fwrite函数;
在不同系统之间交换二进制数据的实际解决办法是使用较高级的协议。这一部分技术涉及“关于网络协议使用的交换二进制数据的技术”;

5.10 定位流

三种方式的定位标准IO流
1) ftell 和 fseek函数, 其假定文件的位置可以存放在一个long整型中;

long ftell(FILE *fp);
int fseek(FILE *fp, long offset, int whence);
void rewind(FILE *fp);

对于二进制文件, 其文件位置指示器从文件起始位置开始度量,并以字节为计量单位。 ftell用于二进制文件, 返回字节位置;
为了定义一个文本文件, whence一定要是SEEK_SET, 而且offset只能有两种值:0 或是对文件调用ftell的返回值;
2) ftello, fseeko 函数, 使用off_t 数据类型代替了长整型;

off_t ftello(FILE *fp);
int fseeko(FILE *fp, off_t offset, int whence);

  1. fgetpos, fsetpos函数, 有标准C引入,使用一个抽象数据类型fpos_t 记录文件位置
int fgetpos(FILE *restrict fp, fpos_t *restrict pos);
int fsetpos(FILE *fp, const fpos_t *pos);

fgetpos 函数将文件位置的当前值存入到pos指定的对象中。

5.11 格式化IO

1) 格式化输出

int printf(const char *restrict format, ...);
int fprintf(FILE *restrict fp, const char *restrict format, ...);
int sprintf(char *restrict buf, const char *restrict format, ...);
int snprintf(char *restrict buf, size_t n, const char *restrict format, ...);

printf 将格式化的数据输入到标准输出;
fprintf 将格式化数据写到指定的流;
sprintf 将格式化的字符送到数组buf, sprintf会自动在数组尾部加一个null字节;sprintf函数可能造成buf缓冲区溢出。调用者有责任确保该缓冲区足够大。为了解决缓冲区溢出的问题, 需要使用snprintf函数;

2) 格式化输入

int scanf(const char *restrict format, ...);
int fscanf(FILE *restrict fp, const char *restrict format, ...);
int sscanf(const char *restrict buf, const char *restrict format, ...);

5.12 实现细节

标准IO库最终都是要调用IO例程的,每个标准IO流都有一个与其相关联的文件描述符, 可以对一个流调用fileno以获得其文件描述符;

int fileno(FILE *fp);

5.13 临时文件

char *tmpnam(char *ptr);
FILE *tmpfile(void);

tmpnam 函数产生一个与现有文件名不同的有效路径字符串。每次调用tmpnam, 都产生一个不同的路径名,最多调用次数是TMP_MAX。如果ptr是NULL的, 产生的路径名存放在一个静态区指向该静态区的指针返回。如果ptr不是NULL, 其字符串数组长度至少是L_tmpnam个字符。所产生的路径名存放在该数组中, ptr作为函数值返回;
tmpfile创建一个临时二进制文件,在关闭该文件或是程序结束时将自动删除这种文件;

int main()
{
	char name[L_tmpnam], line[MAXLINE];
	FILE *fp;
	printf("%s\n", tmpnam(NULL));
	tmpnam(name);
	printf("%s\n", name);

	if((fp = tmpfile()) != NULL)
		err_sys("tmpfile error");
	fputs("one line of output\n", fp);
	rewind(fp);
	if(fgets(line, sizeof(line), fp) == NULL)
		err_sys("fgets error");
	fputs(line, stdout);
	exit(0);
}

5.14 标准IO的替代软件

标准IO库并不完善, 主要原因有:
标准IO库效率不高(当缓冲区 = 磁盘块的时候, IO效率最高);IO的效率还与2复制的数据量有关;每当使用一次行函数fgets或是fputs的时候, 需要进行两次复制:第一次是内核与标准IO缓冲之间,相当于调用read, write; 第二次是标准IO与用户程序的行缓冲区之间;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值