C语⾔中的各种⽂件读写⽅法⼩结
前⾔
找⼯作的时候,曾经⽤C语⾔练习过⼀段时间的算法题⽬,也在⼏个还算出名的OJ平台有过还算靠谱的排名。之前以为C语⾔只限于练习⼀下算法,但是⼯作中的⼀个问题解决让我意识到C语⾔的⽤处还是⾮常⼴泛的。下⾯介绍⼀下,如果⽤C语⾔来操作⽂件保存⼀个字符串,和读取⼀个字符串。算法中往往都是printf来打印出结果,但是真实⼯作中往往通过⽂件来进⾏⼀些持久化的存储⼯作。
C-File I/O
⽂件的I/O操作是每⼀门语⾔的重点,因此这⾥我先来介绍⼀下如何⽤C语⾔去进⾏⽂件的I/O操作。⽂件和流
就C语⾔程序⽽⾔,所有的I/O操作只是简单地从程序移进或移出字节的事情。因此,这种字节流便被称为流(stream)。程序只需要关⼼创建正确的输出字节数据,以及正确地解释从输⼊读取的字节数据。特定I/O设备的细节对程序员是隐藏的。绝⼤多数流是完全缓冲的(fully buffered),这意味着“读取”和“写⼊”实际上是从⼀块被称为缓冲区(buffer)的内存区域来回复制数据。从内存中来回复制数据是⾮常快速的。⽤于输出流的缓冲区只有当它写满时才会被刷新(flush,物理写⼊)到设备或⽂件中。⼀次性把写满的缓冲区写⼊和逐⽚把程序产⽣的输出分别写⼊相⽐效率更⾼。输⼊缓冲区也是类似的原理。 流分为两种类型,分别是⽂本流和⼆进制流。打开流和关闭流
fopen函数打开⼀个特定的⽂件,并把⼀个流和这个⽂件相关联。它的原型如下所⽰:[cpp] view plaincopyprint?在CODE上查看代码⽚派⽣到我的代码⽚
FILE* open(const char* name, const char* mode);
name参数是你希望打开的⽂件或设备的名字。mode参数标识流⽤于只读、只写还是既读⼜写,以及它是⽂本流还是⼆进制流。下⾯表格⾥列出了⼀些常⽤的模式:
如果fopen函数执⾏成功,它将返回⼀个指向FILE结构的指针,该结构代表这个新创建的流。如果函数执⾏失败,它将返回⼀个NULL指针,error会提⽰问题的性质。 流是⽤函数fclose关闭的,它的原型如下:
[cpp] view plaincopyprint?在CODE上查看代码⽚派⽣到我的代码⽚
int fclose(FILE *f);
对于输出流,fclose函数在⽂件关闭之前刷新缓冲区。如果它执⾏成功,fclose返回零值,否则返回EOF。
由于fopen和fclose打开和关闭的都是FILE结构体指针,⽽在stdio.h头⽂件中,包含了对⽂件结构体FILE的描述。这⾥介绍⼀下FILE结构体定义:
struct _iobuf {
char *_ptr; // 下⼀个要被读取的字符的地址 int _cnr; // 剩余的字符
char *base; // 缓冲区基地址 int _flag; // 读写⽂件标志位 int _file; // ⽂件号
int _charbuf; // 检查缓冲区的状况 int _bufsiz; // ⽂件的⼤⼩
char *_tmpfname; // 临时⽂件名 };
typedef struct _iobuf FILE;
字符I/O
当⼀个流被打开之后,它可以⽤于输⼊和输出。它最简单的形式是字符I/O。字符输⼊是由getchar函数家族执⾏的,它们的原型如下所⽰:
int fgetc(FILE *stream); int getc(FILE *stream); int getchar(void);
需要操作的流作为参数传递给getc和fgetc,但是getchar始终是从标准输⼊读取。每个函数从流中读取下⼀个字符,并把它作为函数的返回值返回。如果流中不存在更多的字符,函数就返回常量值EOF(-1)。 为了把单个字符写⼊到流中,可以使⽤putchar函数家族。它的原型如下:
[cpp] view plaincopyprint?在CODE上查看代码⽚派⽣到我的代码⽚
int fputc(int character, FILE* stream); int putc(int character, FILE* stream); int putchar(int character);
⾏I/O
⾏I/O其实可以⽤两种⽅式执⾏——未格式化的或者格式化的。这两种形式都⽤于操纵字符串。区别在于未格式化的I/O只是通过fgets和fputs简单读取或写⼊字符串,⽽格式化的I/O则执⾏数字和其他变量的内部或外部表⽰形式之间的转换。由于⽇常⼯作中操作的⼀般都是格式化I/O,因此这⾥不讲fgets和fputs这种⾮格式化I/O操作了。(当然,还有⼀个重要的原因,fgets⽆法判断缓冲区长度,容易导致溢出等情况)scanf家族
scanf函数家族的原型如下所⽰。每个原型中的省略号表⽰⼀个可变长度的指针列表。从输⼊转换⽽来的值逐个存储到这些指针参数所指向的内存位置。
int fscanf(FILE* stream, const char* format, ...); int scanf(const char* format, ...);
int sscanf(const char* string, const char* format, ...);
这些函数都从输⼊源读取字符并根据format字符串给出的格式化代码对它们进⾏转换。当格式化字符串到达末尾或者读取的输⼊不再匹配格式字符串所指定的类型时,输⼊就停⽌。在任何⼀种情况下,被转换的输⼊值的数⽬作为函数的返回值返回。如果在任何输⼊值被转换之前⽂件就已经到达尾部,函数就返回常量值EOF。printf家族
printf函数家族⽤于创建格式化的输出。它们的函数原型如下:
int fprintf(FILE *stream, const char* format, ...); int printf(const char* format, ...);
int sprintf(char* buffer, const char* format, ...);
⼆进制I/O
把数据写到⽂件⾥效率最⾼的⽅法是⽤⼆进制形式写⼊,⽽且Android系统⾥也有很有⽤⼆进制⽂件通过位来存储数据的应⽤场景。介绍⼀下操纵⼆进制I/O的函数原型。
fread函数⽤于读取⼆进制数据,fwrite函数⽤于写⼊⼆进制数据。它们的原型如下所⽰:[cpp] view plaincopyprint?在CODE上查看代码⽚派⽣到我的代码⽚
size_t fread(void* buffer, size_t size, size_t count, FILE* stream); size_t fwrite(void* buffer, size_t size, size_t count, FILE* stream);
buffer是⼀个指向⽤于保存数据的内存位置的指针,size是缓冲区中每个元素的字节数,count是读取或写⼊的元素数,stream是数据读取或写⼊的流。刷新和定位函数
在处理流时,另外还有⼀些函数也较为有⽤。⾸先,是fflush,它迫使⼀个输出流的缓冲区内的数据进⾏物理写⼊,不管它是不是已经写满。它的原型如下所⽰:
int fflush(FILE* stream);
当我们需要⽴即把输出缓冲区的数据进⾏物理写⼊时,应该使⽤这个函数。
在正常的情况下,数据以线性的⽅式写⼊,这意味着后⾯写⼊的数据在⽂件中的位置是在以前所有写⼊数据的后⾯。C同时⽀持随机访问I/O,也就是以任意顺序访问⽂件的不同位置。随机访问是通过在读取或写⼊前先定位到⽂件中需要的位置来实现的。⼀般使⽤fseek函数来实现,函数原型如下:
int fseek(FILE* stream, long offset, int from);
fseek函数允许你在⼀个流中定位。这个操作将改变下⼀个读取或写⼊的位置。它的第⼀个参数是需要改变的流,它的第⼆个和第三个参数标识⽂件中需要定位的位置。下表描述了fseek参数的使⽤⽅法。