FILE结构
C语言将每个文件简单地作为顺序字节流。每个文件用文件结束符结束,或者在特定字节数的地方结束,这个特定的字节数可以存储在系统维护的管理数据结构中。当打开文件时,就建立了和文件的关系。
在开始执行程序的时候,将自动打开3个文件和相关的流:标准输入流、标准输出流和标准错误。流提供了文件和程序的通信通道。例如,标准输入流使得程序可以从键盘读取数据,而标准输出流使得程序可以在屏幕上输出数据。
打开一个文件将返回指向FILE结构(在stdio.h中定义)的指针,它包含用于处理文件的信息,也就是说,这个结构包含文件描述符。文件描述符是操作系统数组(打开文件列表的索引)。每个数组元素包含一个文件控制块(FCB, File Control Block),操作系统用它来管理特定的文件。
C语言的stdio.h头文件中,定义了用于文件操作的结构体FILE。这样,我们通过fopen返回一个文件指针(指向FILE结构体的指针)来进行文件操作。可以在stdio.h(位于visual studio安装目录下的include文件夹下)头文件中查看FILE结构体的定义。
#ifndef _FILE_DEFINED
struct _iobuf {
char *_ptr; //文件输入的下一个位置
int _cnt; //当前缓冲区的相对位置
char *_base; //文件的起始位置
int _flag; //文件标志
int _file; //文件的有效性验证
int _charbuf;//检查缓冲区状况,若无缓冲区则不读取
int _bufsiz; //文件的大小
char *_tmpfname;//临时文件名
};
typedef struct _iobuf FILE;
#define _FILE_DEFINED
#endif /* _FILE_DEFINED */
实际上,FILE结构是间接地操作系统的文件控制块(FCB)来实现对文件的操作的。具体的应用程序不需要知道FILE结构的细节。
文件是存放在物理磁盘上的,包括文件控制块(FCB)和数据块。文件控制块通常包括文件权限、日期(创建、读取、修改)、拥有者、文件大小、数据块信息。数据块用来存储实际的内容。
对于打开的文件,系统维护了两张表,一张是系统级打开文件表,一张是进程级打开文件表(每个进程有一个)。
系统级打开文件表复制了文件控制块的信息等,系统级文件表每一项都保存一个计数器,即该文件打开的次数。我们初次打开一个文件时,系统首先查看该文件是否已在系统级文件表中,如果不在,则创建该项信息,否则,计数器加1。当我们关闭一个文件时,相应的计数也会减1,当减到0时,系统将系统级文件表中的项删除。
进程级打开文件表保存了指向系统级文件表的指针及其他信息,进程打开一个文件时,会在进程级文件表中添加一项。每项的信息包括当前文件偏移量(读写文件的位置)、存取权限、和一个指向系统级文件表中对应文件项的指针。系统级文件表中的每一项通过 文件描述符(一个非负整数) 来标识。
FILE结构体中的_file成员应该是指向进程级打开文件表,然后,通过进程级打开文件表可以找到系统级打开文件表,进而可以通过FCB操作物理磁盘上面的文件。FILE中包含fd的信息,而且还包含IO缓冲,所以可以理解为FILE是对fd的封装,是C的标准形式,所以FILE比fd更适合跨平台。
流和FILE对象
对于标准IO库,其操作是围绕流进行的。当使用标准IO库打开或者创建一个文件时,使用一个流与文件进行关联。
流的定向决定了所读写的字符是单字节还是多字节。当一个流被创建时,并没有定向。函数fwide可以用于设置流的定向。
#include <stdio.h>
#include <wchar.h>
int fwide(FILE *fp, int mode);
/*若流是宽定向,返回正值,若流是字节定向,返回负值,若流是未定向,返回0*/
根据mode参数的不同值,函数fwide执行不同操作:
若mode为负值,fwide试图使指定的流为字节定向。
若mode为正值,fwide试图使指定的流为宽定向。
若mode为0,fwide不设置流的定向,返回标识该流定向的值。
fwide并不改变已经定向的流。
标准输入、标准输出与标准错误
在头文件stdio.h中预定义了文件指针stdin、stdout和stderr,分别表示标准输入、标准输出与标准错误。
注意与文件描述符的区别:STDIN_FILENO、STDOUT_FILENO与STDERR_FILENO。
/* Standard streams. */
extern struct _IO_FILE *stdin; /* Standard input stream. */
extern struct _IO_FILE *stdout; /* Standard output stream. */
extern struct _IO_FILE *stderr; /* Standard error output stream. */
/* C89/C99 say they're macros. Make them happy. */
#define stdin stdin
#define stdout stdout
#define stderr stderr
缓冲
标准IO提供以下3种类型的缓冲:
全缓冲:在全缓冲模式下,填满标准IO缓冲区后才进行实际的IO操作。术语“冲洗”说明标准IO缓冲区的写操作,即将缓冲区的内容写到磁盘上。
行缓冲:当在输入与输出中遇到换行符时,标准IO库才执行IO操作。当流涉及一个终端时,通常使用行缓冲。
不带缓冲:标准IO库不对字符进行缓冲存储。
很多UNIX系统使用如下的缓冲规则:
- 标准错误是不带缓冲的;
- 若是指向终端设备,则是行缓冲;
- 其他的是全缓冲。
更改缓冲的类型,可以使用以下的函数:
#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*/
mode参数的可取值为:
/* The possibilities for the third argument to `setvbuf'. */
#define _IOFBF 0 /* Fully buffered. */
#define _IOLBF 1 /* Line buffered. */
#define _IONBF 2 /* No buffering. */
这两个函数的选项与动作为:
一般而言,应该由系统选择缓冲区的长度,并自动分配缓冲区。在这种情况下关闭流时,标准IO库将自动释放缓冲区。
强制冲洗一个流,调用函数:
#include <stdio.h>
int fflush(FILE *fp);
/*若成功,返回0,若出错,返回EOF*/
如果fp是NULL,将冲洗所有的输出流。
打开流与关闭流
打开一个标准IO流的函数为:
#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*/
fopen函数打开路径名为pathname的一个指定的文件。
freopen函数在一个指定的流上打开一个指定的文件,若该流已经打开,则先关闭该流,若该流已经定向,则清除该定向。
fdopen取一个已有的文件描述符,使得一个标准IO流与该文件描述符相结合。
type参数指定对IO流的读写方式,具体如下:
调用函数fclose关闭一个打开的流:
#include <stdio.h>
int fclose(FILE *fp);
/*若成功,返回0,若出错,返回EOF*/
在该文件被关闭之前,冲洗缓冲区的输出数据。缓冲区的任何输入数据被丢弃。如果标准IO为流自动分配了缓冲区,则释放该缓冲区。
读和写流
如果打开了流,则对流的读写操作可有3种操作类型:
- 每次一个字符的IO,一次读或者写一个字符;
- 每次一行的IO,每行都以一个换行符终止;
- 直接IO,每次IO操作读写某种数量的对象,每个对象具有指定的长度。
读写流:每次一个字符的IO
以下3个函数可用于一次读取一个字符:
#include <stdio.h>
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);
/*若成功,返回下一个字符,若已到达文件尾端或出错,返回EOF*/
函数getchar等同于getc(stdin),即从标准输入读取一个字符。
函数getc与fgetc的区别:getc可以被实现为一个宏,而fgetc不能实现为宏。
以上三个函数在返回下一个字符时,将unsigned char数据类型转换为int数据类型,要求int类型的返回值的理由是,这样可以返回所有可能的字符值,再加上一个已出错或者到达文件尾端的EOF指示值。在stdio.h中EOF被要求是一个负值,通常为-1。
对于以上的3个函数,不管是已经到达文件尾端,还是调用出错,返回值均为EOF,为了区分这两种情况,需调用函数ferror或者feof确认:
#include <stdio.h>
int ferror(FILE *fp);
int feof(FILE *fp);
/*若条件为真,返回非0,若条件为假,返回0*/
void clearerr(FILE *fp);
在大多数的实现者,为每个流在FILE对象中维护了2个标志:出错标志、文件结束标志。调用clearerr可以清除这2个标志。
从流读取字符数据后,可以调用函数ungetc将字符再压回流中。
#include <stdio.h>
int ungetc(int c, FILE *fp);
/*若成功,返回c,若出错,返回EOF*/
用ungetc压送回字符时,并没有将他们写到底层的文件或者设备中,而是将他们写到标准IO库的流缓冲区中。
对于以上3个读函数,都有对应的写函数,具体如下:
#include <stdio.h>
int putc(int c, FILE *fp);
int fputc(int c, FILE *fp);
int putchar(int c);
/*若成功,返回c,若出错,返回EOF*/
其中函数putchar©等同于putc(c,stdout)。putc可实现为宏,fputc不能实现为宏。
读写流:每次一行的IO
下面的2个函数提供每次读取一行IO的功能:
#include <stdio.h>
char *fgets(char *restrict buf, int n, FILE *restrict fp);
char *gets(char *buf);
/*若成功,返回buf,若已经到达文件尾端或出错,返回NULL*/
函数gets从标准输入读取,而fgets从指定的流读取。
对于函数fgets,必须指定缓冲区的长度n,此函数一直读到下一个换行符为止,但是不超过n-1个字符。读入的字符被送入buf指向的缓冲区,该缓冲区以null结尾。fgets将换行符也存入缓冲区中。
如果该行包括最后一个换行符超过n-1个字符,则fgets只返回一个不完整的行,缓冲区还是以null结尾。对fgets的下一次调度会继续读取该行字符。
gets函数是一个不推荐使用的函数。
对每次一行的IO的写操作如下:
#include <stdio.h>
int fputs(const char *restrict str, FILE *restrict fp);
int puts(const char *char);
/*若成功,返回非负值,若出错,返回EOF*/
函数fputs将一个以null字节终止的字符串写到指定的流,尾端的null字节不写出。函数puts将一个以null字节终止的字符串写到标准输出,null字节不写出,随后puts将一个换行符写到标准输出。
如果总是使用fgets和fputs,那么就会熟知在每行的终止处,我们必须自己处理换行符。
读写流:二进制IO
如果一次读或者写一个完整的结构,以上介绍的函数均不能满足要求,提供以下函数执行二进制IO操作:
#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);
/*返回读或者写的对象数量*/
例如,读或者写一个结构,可以编写如下程序:
struct{
short count;
long total;
char name[NAMESIZE];
}item;
if(fwrite(&item, sizeof(item), 1, fp) != 1)
err_sys("fwrite error");
对于fread函数,如果出错,或者到达文件尾端,则返回的数字可能少于nobj,此时应该调用ferror或者feof判断具体是哪一种情况。
对于fwrite函数,如果返回值小于要求的nobj,则出错。
定位流
可以使用函数fgetops与fsetops来定位流。
#include <stdio.h>
int fgetops(FILE *restrict fp, fpos_t *restrict pos);
int fsetops(FILE *fp, const fpos_t *pos);
/*若成功,返回0,若出错,返回非0*/
函数fgetops将文件位置指示器的当前值存入由pos指向的对象中。调用fsetops可以将pos指向的对象中的文件位置值设置到流中。
格式化IO
格式化输出IO函数是由printf系列函数组成:
#include <stdio.h>
int printf(const char *restrict format, ...);
int fprintf(FILE *restrict fp, const char *restrict format, ...);
int dprintf(int fd, 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将格式化的数据写到标准输出stdout。
函数fprintf将格式化的数据写至指定的流。
函数dprintf将格式化的数据写至指定的文件描述符。
函数sprintf将格式化的数据写入数组buf中,并在该数组buf的尾端自动添加一个null字节,但是该字节不包括在返回值中。
需要注意,sprintf函数可能会造成buf指向的缓冲区溢出,调用者有责任保证缓冲区足够大。
为了解决缓冲区溢出的问题,函数snprintf将缓冲区长度作为一个显式的参数。超过缓冲区尾端写的所有字符将被丢弃。
参数使用了arg的printf系列函数的变体如下:
#include <stdio.h>
#include <stdarg.h>
int vprintf(const char *restrict format, va_list arg);
int vfprintf(FILE *restrict fp, const char *restrict format, va_list arg);
int vdprintf(int fd, const char *restrict format, va_list arg);
/*若成功,返回输出字符数,若出错,返回负值*/
int vsprintf(char *restrict buf, const char *restrict format, va_list arg);
/*若成功,返回存入数组的字符数,若出错,返回负值*/
int vsnprintf(char *restrict buf, size_t n, const char *restrict format, va_list arg);
/*若缓冲区足够大,返回将要存入数组的字符数,若出错,返回负值*/
格式化输入处理函数是scanf系列函数,如下:
#include <stdio.h>
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, ...);
/*返回赋值的输入项数,若输入出错或者在任一转换前已到达文件尾端,返回EOF*/
对于scanf系列函数也有对应的可变长度参数变体:
#include <stdio.h>
#include <stdarg.h>
int vscanf(const char *restrict format, va_list arg);
int vfscanf(FILE *restrict fp, const char *restrict format, va_list arg);
int vsscanf(const char *restrict buf, const char *restrict format, va_list arg);
/*返回赋值的输入项数,若输入出错或者在任一转换前已到达文件尾端,返回EOF*/