通过文件I/O读写文件时,每次操作都会执行相关系统调用。这样处理的好处是直接读写实际文件,坏处是频繁的系统调用会增加系统开销,标准I/O可以看成是在文件I/O的基础上封装了缓冲机制。先读写缓冲区,必要时再访问实际文件,从而减少了系统调用的次数。

int fflush(FILE *fp);//如若fp是NULL,则次函数将导致所有输出流被冲洗。

1. 流与FILE对象

标准I/O库的所有操作都是围绕流(stream)进行的。当用标准I/O库打开一个或撞见一个文件时,我们已使一个流与文件关联。
ascii的一个字符用一个字节表示,而国际字符集的一个字符可用多个字符表示,成为宽字符集

当一个流最初被创建时,它并没有定向。如若在未定向的流上使用一个多字节I/O函数, 则将该流的定向设置为宽定向的。若在未定向的流上使用一个单字节I/O函数,则将该流的定向设为字节定向的。
只有两个函数可改变流的定向。freopen函数清除一个流的定向;fwide函数可用于设置流的定向。

#include <stdio.h>
#include <wchar.h>
int fwide(FILE *fp, int mode);
//返回值:若流是宽定向的,返回正值;若流是字节定向的,返回负值;若流是未定向的,返回0
  • 如若mode参数值为负,fwide将试图使指定的流是字节定向的。
  • 如若mode参数值为正,fwide将试图使指定的流是宽定向的。
  • 如若mode参数值为0,fwide将不试图设置流的定向,但返回标识该流定向的值。

注意,fwide并不改变已定向流的方向。
还应注意,fwide无出错返回。 可以在调用前先清除errno,从fwide返回时检查errno的值。

2 标准输入、标准输出与标准错误

文件描述符分别:STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO
可以通过头文件<stdio.h>中定义的文件指针stdin、stdout和stderr加以引用。

3 缓冲

讲解缓冲之前需要先明白冲洗(flush),它是将缓冲区的内容写入磁盘中(即使缓冲区未填满)。
<mark>缓冲的作用:减少系统read和write的次数。</mark>
标准IO提供3中类型的缓冲:

  1. 全缓冲:在填满标准I/O缓冲区后,才进行实际I/O操作。全缓冲使用malloc来分配缓冲区
  2. 行缓冲:在输入或输出中遇到了换行符或者缓冲区被填满,才进行实际I/O操作。例如用getc读取一个字符时,实际读取了一行在缓冲区中。(注:当标准I/O库不使用缓冲或者使用行缓冲需要从内核读取数据时,行输出缓冲会被flush)
  3. 不带缓冲

一般缓冲区类型特征是:

  • 当且仅当标准输入和标准输出并不指向交互式设备时,它们才是全缓冲的。 (iso c)
  • 标准错误绝不会是全缓冲的,通常是不带缓冲的,这就使得出错信息可以尽快显示出来,而不管它们 是否含有一个换行符。
  • 若是指向终端设备的流,则是行缓冲的;否则是全缓冲的。(很多系统默认)

调用下列两个函数可以更改缓冲类型。

#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来说,如果buf参数为空,则关闭数据流的缓冲功能;如果buf的值不为空,指向一个缓冲区,那么就将缓冲类型设置为全缓冲(一些系统实现了当数据流来自Terminal时,设置为行缓冲)
  对于setvbuf的_IPFBF和_IOLBF,如果buf为空,则为系统默认缓冲区,如果buf不为空,则使用用户自定义的缓冲区。

使用setvbuf,我们可以精确地说明所需的缓冲类型。这是用mode参数实现的:
_IOFBF 全缓冲
_ IOLBF 行缓冲
_IONBF 不带缓冲
具体buf、mode、size参数选择如下图。

4. 打开流

#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);
//3个函数的返回值:若成功,返回文件指针;若出错,返回NULL
  1. fopen函数打开路径名为pathname的一个指定的文件
  2. freopen函数在一个指定的流上打开一个指定的文件,如若该流已经打开,则先关闭该流。若该流已经定向,则使用 freopen清除该定向。此函数一般用于将一个指定的文件打开为一个预定的流:标准输入、标准输出或标准错误
  3. fdopen函数取一个已有的文件描述符(可能从open、dup、dup2、fcntl、pipe、socket、socketpair或accept 函数得到此文件描述符),并使一个标准的I/O流与该描述符相结合。此函数常用于由创建管道和网络通信通道函数返回的描述符。 因为这些特殊类型的文件不能用标准I/O函数fopen打开,所以我们必须先调用设备专用函数以获得一个文件描述符,然后用 fdopen使一个标准I/O流与该描述符相结合。

参数type制定对IO流的读写方式。
其中b的作用是区分文本文件和二进制文件——这在unix系统中毫无意义。
+的作用是表示读和写类型

对于fdopen,因为文件描述符已打开,所以fdopen为写而打开并不截短该文件。另外,标准IO添加(追加写)方式也不能用于创建文件,因为如果一个文件描述符引用一个文件则该文件一定已经存在。
若使用读和写类型打开一个文件(+),有以下限制:

  • 若中间没有fflush、fseek、fsetpos或rewind,则在输出之后不能直接跟输入。
  • 若中间没有fseek、fsetpos或rewind,或者一个输入操作没有到文件尾端,则输入之后不能直接跟输出。

用fclose关闭一个打开的流,文件关闭之前对于缓冲区中flush输出数据,丢弃输入数据,然后释放缓冲区。

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

5. 读和写流

对于打开的流有三种I/O读写操作可以选择:

  1. 每次读或写一个字符,若带有缓冲则有标准I/O函数处理所有缓冲。
  2. 每次读或写一行,使用fgets和fputs,以一个换行符终止。fgets应说明能处理的最大行长。
  3. 直接I/O,使用fread和fwrite,每次读或写某个数量的对象,每个对象具有制定的长度,常用语从二进制文件中读写一个结构。

5.1 输入函数

#include <stdio.h>
int getc(FILE *fp);     //宏实现,快
int fgetc(FILE *fp);    //函数实现,可作为参数给另一个参数
int getchar(void); //等同于getc(stdin);
//若成功返回下一个字符;若出错或到达尾端,返回EOF

  返回值将字符从unsigned char转化为int型是为了兼容EOF(该值通常是-1)。
  为了区分出现EOF时究竟是出错还是到达尾端,需要调用ferrorfeof

#include <stdio.h>
int ferror(FILE *fp);  //fp中标记为error返回非0,eof返回0
int feof(FILE *fp);    //fp中标记为error返回0,eof返回非0
void clearerr(FILE *fp);//清除标记

从流中读取数据后可以用回送字符操作int ungetc(int c, FILE *fp);将字符再次压入流中,并且可以清楚文件结束标志。回送字符操作经常用于进行切词或几号切分操作时,先看一下下一个字符再决定如何处理当前字符

5.2 输出函数

#include <stdio.h>
int putc(int c, FILE *fp);     //宏实现,快
int fputc(int c, FILE *fp);    //函数实现,可作为参数给另一个参数
int getchar(int c); //等同于putc(int c, stdin);
//若成功, 返回c;若出错,返回EOF

6 每次一行I/O

6.1每次输入一行

#include <stdio.h>
char *fgets(char *restrict buf, int n, FILE *restrict fp);//从指定流读
char *gets(char *buf);//从标准输入读取
//若成功,返回buf;若出错或到达尾端,返回NULL

buf指定了缓冲区的地址。
fgets必须指定缓冲的长度n,然后读取到换行符或超过n-1的字符为止。
gets无法指定缓冲区长度导致<mark>可能造成缓冲区溢出</mark>。并且gets不会讲换行符存入缓冲区。

6.2 每次输出一行

#include <stdio.h>
char *fputs(const char *restrict str, FILE *restrict fp);
char *puts(const char *str);
//若成功,返回非负值;若出错,返回EOF

两个函数都是将以null为终止符的字符串输出,且不写出null。但puts随后是再想标准输出写入一个换行符,而fputs需要自己写入换行符。

7 标准I/O的效率

编写简单的将标准输入复制到标准输出的程序

#include "apue.h"
int in_to_out_c(){
	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);
}

int in_to_out_line(){
	char buf[MAXLINE];
	while((fgetc(buf, MAXLINE, stdin)) != NULL)
		if(fputc(buf,stdout) == EOF)
			err_sys("output error");
	if(ferror(stdin))
		err_sys("input error");
	exit(0);
}
  • 用户CPU时间:执行用户指令所用的时间 就是用户的进程获得了CPU资源以后,在用户态执行的时间。
  • 系统CPU时间:为该进程执行内核程序所经历的时间 用户进程获得了CPU资源以后,在内核态的执行时间。(本例中为对内核读写请求)

8 二进制I/O

常用语读写一个结构体或二进制数组。

#include <stdio.h>
size_t fread(void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);
size_t fwirte(void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);
//返回读或写的对象数

其中size是欲写的元素/结构的长度,nobj是每个元素/结构的个数。
<mark>对于读,若出错或到达文件尾数字可以少于nobj。</mark>

float data[10];
if(fwrite(&data[2], sizeof(float), 4, fp) != 4)
	err_sys("error");

struct{
	short count;
	long  total;
	char  name[NAMESIZE];
} item;
if(fread(&Item, sizeof(item), 1, fp) != 1)
	if(ferror(fp))
		err_sys("error")

二进制IO操作数据只能用于读取同一系统上已写的数据,因为同一结构的同一成员的偏移量以及多字节整数和浮点数的二进制格式可能随编译程序和系统的不同而不同。

9. 定位流

指获得当前文件位置指示和将文件指针指向某一位置。
(1)ftellfessk假定文件的位置可以存放在一个长整型中。

#include <stdio.h>
long ftell(FILE *fp);  //若成功,返回当前文件位置指示;否则返回-1
int fseek(FILE *fp, long offset, int whence);//若成功,返回0;否则返回-1
void rewiond(FILE *fp);
  • ftell用于二进制文件时返回值就是这种字节位置。
  • fseek用于定位一个二进制文件,offset为偏移量,而whence解释偏移量的方式(与lseek相同)
    SEEK_SET表示从文件起始位置;SEEK_CUR表示从当前位置开始;SEEK_END表示从文件尾段开始。
    定位文本文件必须使用SEEK_SET且offset只能是 0 或 ftell(fp)
  • rewind将流设置到文件起始位置。

(2)

#include <stdio.h>
off_t ftello(FILE *fp);  //若成功,返回当前文件位置指示;否则返回-1
int fseeko(FILE *fp, long offset, int whence);//若成功,返回0;否则返回-1

除了偏移量是off_t以外,均与(1)相同

(3)

#include <stdio.h>
int fgetpos(FILE *restrict fp, fpos_t *restrict pos);  
int fsetpos(FILE *fp, const fpos_t *pos);
//若成功,返回0;否则返回-1

这两个函数使用一个抽象数据类型fpos_t记录文件位置。
fgetpos将文件位置指示器的当前值存入pos指向的对象中,在以后调用fsetpos时,可以用这个值重新将流定位至该位置。