C语言中的文件
文章目录
- 文件
- 1. 流
- 1.1 文件缓冲
- 1.2 标准流
- 1.3 文本文件和二进制文件
- 2. 打开/关闭文件
- 2.1 fopen
- 2.2 fclose
- 3. 读写文件
- 3.1 fgetc & fputc
- 3.2 fgets & futs
- 3.3 fscanf & fprintf
- 3.4 fread & fwrite
- 4. 文件定位
- 5. 错误处理
- 5.1 errno
文件
1. 流
在 C 语言中,流 (stream) 表示任意输入的源或任意输出的目的地。流是一个抽象的概念,它既可以表示存储硬盘上的文件,也可以表示网络端口或者打印设备。流这个概念可以很好地屏蔽硬件设备之间的差异,使得 C 语言可以像读写文件一样读写任意的设备。
Linux哲学:一切皆文件。
1.1 文件缓冲
仅仅了解抽象的概念是不够的,有时候我们还需要了解事物运行的机理。由于内存和硬件设备之间存在读写性能上的"鸿沟",所以操作系统会在内存上为流设置缓冲区。
缓冲区是以先进先出的方式管理数据的。缓冲区分为三种类型:
- 满缓冲。当缓冲区空时,从输入流中读取数据;当缓冲区满时,向输出流中写入数据。
- 行缓冲。每次从输入流中读取一行数据;每次向输出流中写入一行数据(stdin、stdout)。
- 无缓冲。顾名思义,就是没有缓冲区(stderr)。
1.2 标准流
C 语言对流的访问是通过文件指针实现的,它的类型为 FILE*
。并且在<stdio.h>头文件中提供了 3 个标准流。这 3 个标准流可以直接使用——我们不需要对其进行声明,也不用打开或者关闭它们。
文件指针 | 流 | 默认含义 |
---|---|---|
stdin | 标准输入 | 键盘 |
stdout | 标准输出 | 屏幕 |
stderr | 标准错误 | 屏幕 |
1.3 文本文件和二进制文件
C 语言支持两种类型的文件:文本文件和二进制文件。文本文件中存储的是字符数据,人类是可以看懂的;二进制文件中的数据,人类是看不懂的。
[!TIP]
二进制文件的存储的基本单位是字节
文本文件的基本单位是字符(字符 = 字节 + 编码(比如GBK UTF-8))
文本文件具有两个独特的性质:
-
文本文件有行的概念。文本文件被划分为若干行,并且每一行的结尾都以特殊字符进行标记。在 Windows 系统中,是以回车符和换行符 (\r\n) 进行标记的;在 Unix 和 Macintosh 系统中是以换行符 (\n) 标记的。
-
文本文件可能包含一个特殊的“文本末尾”标记。一些操作系统允许在文本文件的末尾使用一个特殊的字节作为标记。在 Windows 系统中,这个标记为 ‘\x1a’ (Ctrl+Z)。Ctrl+Z不是必需的,但如果存在,它就标志着文件的结束,其后的所有字节都会被忽略。大多数其他操作系统 (包括 UNIX) 是没有文件末尾字符。
使用 Ctrl+Z 的这一习惯继承自 DOS,而 DOS 中的这一习惯又是从 CP/M (早期用于个人电脑的一种操作系统) 来的。
在写入数据时,我们需要考虑是以文本形式存储还是以二进制的形式存储。比如,存储整数 32767,一种选择是写入字符 ‘3’, ‘2’, ‘7’, ‘6’, ‘7’,需要 5 个字节。
另一个选择是以二进制形式存储这个数,这种方法只需要两个字节。
文本形式可以方便人类阅读和编辑;二进制形式可以节省空间,并且转换效率高。
2. 打开/关闭文件
2.1 fopen
读写文件之前,我们需要使用 fopen
函数打开文件。
FILE* fopen(const char* filename, const char* mode);
第一个参数是文件的路径,用来定位文件的;第二个参数表示是以何种模式打开文件的。如果无法打开文件, fopen
返回空指针。
文件路径
文件路径分为两种,一种是绝对路径:从根目录 (或者盘符) 开始,一直到文件所在的位置,比如:“c:/project/test.dat”。另一种是相对路径:从当前工作目录开始,一直到文件所在的位置,比如:“in.dat”。
在实际工作中,我们一般使用相对路径 (Why? 简单高效)。
模式
模式的选择不仅依赖于后续对文件的操作,还依赖于文件是文本形式还是二进制形式。打开一个文本文件,可以使用下面一些模式:
模式字符串 | 含义 |
---|---|
“r” | 打开文件用于读 |
“w” | 打开文件用于写(文件不存在则创建) |
“a” | 打开文件用于追加(文件不存在则创建) |
“r+” | 打开文件用于读和写,从文件头开始 |
“w+” | 打开文件用于读和写(文件不存在则创建) |
“a+” | 打开文件用于读和写(文件不存在则创建) |
当使用 fopen
打开二进制文件时,需要在模式字符串中包含字母 b。
模式字符串 | 含义 |
---|---|
“rb” | 打开文件用于读 |
“wb” | 打开文件用于写(文件不存在则创建) |
“ab” | 打开文件用于追加(文件不存在则创建) |
“r+b"或"rb+” | 打开文件用于读和写,从文件头开始 |
“w+b"或"wb+” | 打开文件用于读和写(文件不存在则创建) |
“a+b"或"ab+” | 打开文件用于读和写(文件不存在则创建) |
写模式和追加模式是不一样的。如果文件存在,写模式会清空原有的数据,而追加模式会在原有数据的后面写入新的内容。
2.2 fclose
fclose
可以关闭程序不再使用的文件。
int fclose(FILE* stream);
如果成功关闭,fclose
返回零;否则返回 EOF。
[!TIP]
注意:当不再使用某个文件时,一定要及时关闭该文件。
下面给出了一个程序框架,展示了在实际工作中是如何打开和关闭文件的:
FILE* fp = fopen(filename, mode);
if (fp == NULL) {
// error handling
}
...
fclose(fp);
3. 读写文件
前面介绍了如何打开和关闭文件,接下来我们来学习下如何读写文件。
其中 fgetc/fputc
, fgets/fputs
和 fscanf/fprintf
是用来读写文本文件的;
fread/fwrite
是用来读写二进制文件的。
3.1 fgetc & fputc
fgetc
fgetc
可以从输入流中读取一个字符,如果读取成功,返回读取的字符;如果读到文件末尾,或者读取失败,返回 EOF。
int fgetc(FILE* stream);
fgetc
和 getchar
类似。不同的是 getchar
只能从标准输入流(stdin)中读取字 符,而 fgetc
可以从任意一个输入流中读取字符。
fputc
fputc
可以向输出流中写入一个字符,如果写入成功,返回写入的字符;如果写入失败,返回EOF。
int fputc(int c, FILE* stream);
fputc
和putchar
类似。不同的是 putchar
只能向标准输出流(stdout)中写入字符,而fputc
可以向任意一个流中写入字符。
示例
#include <stdio.h>
#include <cstdlib>
int main(int argc, char** argv) {
if (argc != 3) {
printf("Error: invalid arguments\n");
exit(EXIT_FAILURE);
}
// 打开文件流
FILE* source_fp = fopen(argv[1], "r");
if (source_fp == NULL) {
printf("Can not open %s\n", argv[1]);
exit(EXIT_FAILURE);
}
FILE* dest_fp = fopen(argv[2], "w");
if (dest_fp == NULL) {
printf("Can not open %s\n", argv[2]);
fclose(source_fp);
exit(EXIT_FAILURE);
}
// 复制文件
int c = 0;
while ((c = fgetc(source_fp)) != EOF) {
fputc(c, dest_fp);
}
// 关闭文件流
fclose(source_fp);
fclose(dest_fp);
return 0;
}
3.2 fgets & futs
一个字符一个字符地读写文本文件,效率太慢了。C 语言提供了一次性可以读写一行的函数 fgets
和 fputs
。
fgets
从输入流 stream 中,最多读取 count - 1 个字符,并把读取的字符存入 str 指向的字符数组中。 fgets
遇到换行符’\n’,或者文件的末尾就会终止(也就是说,读取的字符数可能不足 count - 1 个),并且会存储换行符’\n’。 fgets
会在最后添加空字符’\0’。
char* fgets(char* str, int count, FILE* stream);
参数:
str: 指向一个字符数组
count: 能够写入的最大字符数量(通常是str指向字符数组的长度)
stream: 输入流
返回值:
成功:返回str
失败:NULL
fgets
是 gets
的通用版本,它可以从任意输入流中读取数据,而 gets
只能从 stdin 中读取数据。
fgets
也比 gets
更为安全,因为它限制了读取字符的最大数目 (count - 1)。此外,如果 fgets
是因为读取了换行符而终止,那么它会存储换行符’\n’,而 gets
函数从来不会存储换行符。
fputs
将 str 指向的字符串,写入输出流 stream 中。
int fputs(const char* str, FILE* stream);
参数:
str: 要写的字符串(以'\0'结尾的字符串)
stream: 输出流
返回值:
成功:返回一个非负值。
失败:返回EOF,并设置errno。
fputs
是 puts
的通用版本,它可以将字符串写入到任意的输出流中,而 puts
只能 写入到 stdout 中。此外, fputs
是原样输出字符串,而 puts
会在字符串后面而外 输出一个换行符’\n’。
示例
#include <stdio.h>
#include <cstdlib>
int main(int argc, char** argv) {
if (argc != 3) {
printf("Error: invalid arguments\n");
exit(EXIT_FAILURE);
}
// 打开文件流
FILE* source_fp = fopen(argv[1], "r");
if (source_fp == NULL) {
printf("Can not open %s\n", argv[1]);
exit(EXIT_FAILURE);
}
FILE* dest_fp = fopen(argv[2], "w");
if (dest_fp == NULL) {
printf("Can not open %s\n", argv[2]);
fclose(source_fp);
exit(EXIT_FAILURE);
}
// 复制文件(fgets & fputs)
char str[1024];
while (fgets(str, sizeof(str), source_fp) != NULL) {
fputs(str, dest_fp);
}
// 关闭文件流
fclose(source_fp);
fclose(dest_fp);
return 0;
}
3.3 fscanf & fprintf
fscanf
fscanf
和 scanf
类似,是用来进行格式化输入的。
int fscanf(FILE* stream, const char* format, ...);
不同的是, scanf
是从标准输入(stdin)中读取数据,而 fscanf
可以从任何一个流中读取数据。也就是说,当 fscanf
的第一个参数为 stdin 时,它的效果等价于scanf
。
顺便提一下,sscanf
可以从字符串中读取数据。
fprintf
fprintf
和 printf
类似,是用来进行格式化输出的。
int fprintf(FILE* stream, const char* format, ...);
不同的是, printf
始终是向标准输出(stdout)写入内容的,而 fprintf
可以向任何一个输出流中写入内容。也就是说,当 fprintf
的第一个参数为 stdout 时,它的效果等价于 printf
。
顺便提一下, sprintf
可以将内容写入到一个字符数组中。
格式化输入输出,可以用于序列化和反序列化过程中。所谓序列化,就是将程序中的对象转换成一种可以保存的格式(二进制或文本),从而方便存储(存储到文件或数据库中)或传输(通过网络传输给另一台机器)。反序列化则是序列化的逆过程,它将按一定格式存储的数据转换成程序中的对象。
示例
#include <stdio.h>
#include <cstdlib>
typedef struct {
char name[25];
int age;
char gender;
} Student_t;
int main(int argc, char** argv) {
if (argc != 2) {
printf("Error: invalid arguments\n");
exit(EXIT_FAILURE);
}
Student_t stu1 = {"tom", 18, 'f'};
FILE* fp = fopen(argv[1], "w");
if (fp == NULL) {
printf("Can not open %s\n", argv[1]);
exit(EXIT_FAILURE);
}
// 序列化
fprintf(fp, "%s %d %c", stu1.name, stu1.age, stu1.gender);
fclose(fp);
fp = fopen(argv[1], "r");
if (fp == NULL) {
printf("Can not open %s\n", argv[1]);
exit(EXIT_FAILURE);
}
// 反序列化
Student_t stu2 = {"jack", 19, 'm'};
fscanf(fp, "%s %d %c", stu2.name, &stu2.age, &stu2.gender);
fclose(fp);
printf("Stu2.name: %s Stu2.age: %d Stu2.gender: %c\n", stu2.name, stu2.age, stu2.gender);
return 0;
}
3.4 fread & fwrite
fread
和 fwrite
主要是用来处理二进制文件的。 fread
可以每次读取一大块数据, fwrite
可以每次写入一大块数据。
fread
从输入流 stream 中,最多读取 count 个元素,并把它们依次存放到 buffer 指向的数组中。
size_t fread(void* buffer, size_t size, size_t count, FILE* stream);
参数:
buffer: 指向存放数据的数组
size: 每个元素的大小(以字节为单位)
count: 最多可以读取的元素个数
stream: 输入流
返回值:
成功读取元素的个数。当读到文件末尾,或者发生错误时,返回值可能小于count。
我们可以通过feof和ferror函数来判断,到底是读到了文件末尾,还是发生了错误。
fwrite
将存放在 buffer 指向的数组中的 count 个元素写入到输出流 stream 中。
size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream);
参数:
buffer: 指向存放数据的数组。
size: 每个元素的大小(以字节为单位)
count: 要写入元素的个数
stream: 输出流
返回值:
成功写入元素的个数。当发生错误时,这个值可能小于count。
fread/fwrite
不仅可以用于读写二进制文件,还可以用于序列化和反序列化过程中。
示例1(复制二进制文件)
int main(int argc, char** argv) {
if (argc != 3) {
printf("Error: invalid arguments\n");
exit(EXIT_FAILURE);
}
// 打开文件流
FILE* source_fp = fopen(argv[1], "r");
if (source_fp == NULL) {
printf("Can not open %s\n", argv[1]);
exit(EXIT_FAILURE);
}
FILE* dest_fp = fopen(argv[2], "w");
if (dest_fp == NULL) {
printf("Can not open %s\n", argv[2]);
fclose(source_fp);
exit(EXIT_FAILURE);
}
// 复制文件(fread & fwrite)
char str[1024];
int n = 0;
while ((n = fread(str, 1, sizeof(str), source_fp)) != 0) {
fwrite(str, 1, n, dest_fp);
}
// 关闭文件流
fclose(source_fp);
fclose(dest_fp);
return 0;
}
示例2(序列化与反序列化)
#include <stdio.h>
#include <cstdlib>
typedef struct {
char name[25];
int age;
char gender;
} Student_t;
int main(int argc, char** argv) {
if (argc != 2) {
printf("Error: invalid arguments\n");
exit(EXIT_FAILURE);
}
Student_t stu1 = {"jerry", 20, 'm'};
FILE* fp = fopen(argv[1], "w");
if (fp == NULL) {
printf("Can not open %s\n", argv[1]);
exit(EXIT_FAILURE);
}
/* ********** 使用fscanf & fprintf ********** */
// 序列化
fwrite(&stu1, sizeof(Student_t), 1, fp);
fclose(fp);
fp = fopen(argv[1], "r");
if (fp == NULL) {
printf("Can not open %s\n", argv[1]);
exit(EXIT_FAILURE);
}
Student_t stu2 = {"jack", 19, 'm'};
// 反序列化
fread(&stu2, sizeof(Student_t), 1, fp);
fclose(fp);
printf("Stu2.name: %s Stu2.age: %d Stu2.gender: %c\n", stu2.name, stu2.age, stu2.gender);
return 0;
}
4. 文件定位
每个流都有相关联的文件位置。在执行读写操作时,文件位置会自动推进,并按照顺序访问文件。顺序访问是很好的,但是有时候,我们可能需要跳跃地访问文件。为此, 提供了几个函数来支持这种能力:
int fseek(FILE* stream, long int offset, int whence);
long int ftell(FILE* stream);
void rewind(FILE* stream);
fseek
fseek
可以改变与 stream 相关联的文件位置。其中 whence 表示参照点,参照点有 3 个选择:
- SEEK_SET:文件的起始位置
- SEEK_CUR:文件的当前位置
- SEEK_END:文件的末尾位置
offset 表示偏移量 (可能为负),它是以字节进行计数的。比如:移动到文件的起始位置,可以这样写:
fseek(fp, 0L, SEEK_SET);
移动到文件的末尾,可以这样写:
fseek(fp, 0L, SEEK_END);
往回移动10个字节,可以这样写:
fseek(fp, -10L, SEEK_CUR);
通常情况下, fseek
会返回 0;如果发生错误 (比如,请求的位置不存在),那么 fseek
会返回非 0 值。
ftell
ftell
以长整数形式返回当前文件位置;如果发生错误,ftell
返回-1L。ftell
一般的用法是:记录当前位置,方便以后返回。
long int filePos = ftell(fp);
...
fseek(fp, filePos, SEEK_SET);
rewind
rewind
会将文件位置设置为起始位置,类似于调用:
fseek(fp, 0L, SEEK_SET);
练习
用户输入文件名,将整个文件的内容读入到字符数组中,并在后面添加空字符’\0’。
char* readFile(const char* path);
char* readFile(const char* path) {
// 打开文件
FILE* fp = fopen(path, "rb");
if (fp == NULL) {
printf("Can not open %s\n", path);
exit(EXIT_FAILURE);
}
// 文件结尾位置
fseek(fp, 0L, SEEK_END);
long int file_len = ftell(fp);
// 将文件内容复制到buffer中
rewind(fp); // 回到文件开头,因为要从开头开始复制
char* buffer = (char*)malloc(file_len + 1);
fread(buffer, 1, file_len, fp);
// 在数组末尾添加空字符
buffer[file_len] = '\0';
fclose(fp);
return buffer;
}
int main(int argc, char* argv[]) {
if (argc != 2) {
printf("Error: invalid arguments\n");
exit(EXIT_FAILURE);
}
char* buffer = readFile(argv[1]);
printf("buffer content: \n%s\n", buffer);
free(buffer);
return 0;
}
5. 错误处理
错误的检测和处理并不是 C 语言的强项,C 语言没有其它高级语言 (C++, Java, C#等) 所具有的异常处理机制。
C 语言往往是通过函数的返回值,或者是测试 errno 变量来检测错误的;并且需要程序员自己编写代码来处理错误。
5.1 errno
errno
是一个 int 类型的全局变量 (C11 修改为线程本地变量,即每个线程都有一个独有的 errno
变量),它定义在 <errno.h> 头文件中。
标准库中有些函数 (比如与文件相关的一些函数),如果在调用过程中发生了错误,它会设置 errno 的值,以表明发生 了何种类型的错误。
程序启动时,会将 errno 的值设为 0,表示没有错误发生。其它非 0 值都表示发生了某种类型的错误。
我们可以通过 perror
和 strerror
来显示错误信息。其中, perror
定义在 <stdio.h> 头文件 中, strerror
定义在 <string.h> 头文件中。
示例
int main(void) {
printf("%d\n", errno); // errno == 0
FILE* fp = fopen("not_exist.txt", "r");
printf("%d\n", errno); // errno == 2
printf("%s\n", strerror(errno)); // No such file or directory
perror("not_exist.txt"); // not_exist.txt: No such file or directory
return 0;
}