程序计算出的一切,在它退出时都会消失,除非把它写下来。C 语言的文件接口位于 <stdio.h> 中,和 printf 是同一个头文件,而且有意设计得很相似:文件是一个字节流,用于屏幕的函数都有一个接收流参数的“孪生”版本。
句柄的类型是 FILE *。你永远不会查看 FILE 的内部;它是一个不透明类型(参见 typedef),你只会持有指向它的指针。
三个步骤
每次文件操作都遵循同样的流程:打开、使用、关闭。
检查 NULL 并不是多余的谨慎。文件不存在(读取模式下)、目录不可写、路径错误、进程用完了文件句柄,这些情况下 fopen 都会失败,而且每一种都返回 NULL。使用 NULL 句柄会导致 段错误。
把错误信息发送到 stderr 而不是 stdout 是惯例:它没有缓冲,而且可以和正常输出分开重定向。
fopen 模式
第二个参数是一个简短的模式字符串。在这一部分,写错模式是破坏性最大的错误,因为 "w" 会悄无声息地清空已有文件。
| 模式 | 读 | 写 | 文件已存在时 | 文件不存在时 |
|---|---|---|---|---|
"r" | 是 | 否 | 从开头打开 | 失败,返回 NULL |
"w" | 否 | 是 | 截断为空 | 创建 |
"a" | 否 | 是 | 只在末尾写入 | 创建 |
"r+" | 是 | 是 | 从开头打开 | 失败,返回 NULL |
"w+" | 是 | 是 | 截断为空 | 创建 |
"a+" | 是 | 是 | 可在任意位置读,只在末尾写 | 创建 |
在任何模式后面加上 b("rb"、"wb"、"ab+")就是二进制模式,文末会详细介绍。
两条能防止真实数据丢失的规则:
- 想读取时就用
"r"。 文件名打错时,"r+"会安全地失败;"w+"会创建一个空文件,你损失的只是时间。而本想用"r"却对正确的文件名用了"w",数据就毁了。 - 日志用
"a"。 不管流当前位于何处,每次fprintf都会写到末尾,这正是日志需要的。
在一个程序里写入和读取
下面的编辑器运行一个完整的往返流程:创建文件,写入记录,关闭,再以读取方式重新打开,打印读到的内容。
fprintf 和 fscanf 就是把流作为第一个参数的 printf 和 scanf;它们的 格式说明符 完全相同,包括防止 name 溢出的 %31s 宽度。
循环条件是 == 2,也就是格式字符串要求读取的项数。改成和 EOF 比较是一个经典 bug:遇到格式错误的行时,fscanf 返回的是 0 而不是 EOF,循环就会在无法消耗的输入上无限空转。
用 fgets 按行读取
fscanf 适合格式严格的数据。对于文本文件,比如配置、日志、CSV,以及任何人写的内容,应该整行读取。下面这个循环就是标准写法:
fgets 之所以是合适的默认选择:
- 它接收缓冲区大小,所以不会溢出。传入
sizeof line,即使你调整数组大小,调用依然正确。 - 到达文件末尾或出错时返回
NULL,正好作为简洁的循环条件。 - 当一行能放进缓冲区时,它会保留换行符。 这很有用:如果读到的内容里没有
'\n',说明这一行比缓冲区长,剩下的部分还在等着读取。strcspn(line, "\n")找到换行符的下标(没有换行符时就是字符串长度),所以在那里赋值'\0',两种情况都能正确去掉换行符。
要区分真正的文件结束和出错,在循环结束后检查:
if (ferror(in)) {
fprintf(stderr, "read error\n");
} else if (feof(in)) {
/* normal end */
}
不要把 while (!feof(fp)) 作为循环条件。feof 只有在某次读取已经失败之后才会变为真,所以这种循环会把最后一次缓冲区的内容多处理一遍。应该检查读取函数的返回值,就像上面两个循环那样。
逐个字符:fgetc 和 fputc
对于字节级别的操作,比如统计字符、转换文件、复制文件,fgetc 和 fputc 每次调用处理一个字符。
有一个重要细节:c 声明为 int,而不是 char。fgetc 返回 int,这样它既能返回每一个可能的字节值,又能返回一个与之不同的哨兵值 EOF(即 -1)。如果存进 char,和 EOF 的比较要么永远为假,要么在遇到字节 0xFF 时误判为真,取决于你的平台上 char 是否有符号。这是 C 语言最古老的陷阱之一。
正确地检查错误
正式环境中的读取代码是这样的:
errno 保存描述上一次失败的错误码,strerror 把它转换成一句话;perror 一次调用就能打印你的消息加上那句话。它们分别需要 <errno.h> 和 <string.h>。
fclose 也可能失败:它会刷新缓冲的数据,而磁盘可能放不下这次写入。所以对重要的数据,要检查它:
if (fclose(fp) != 0) {
fprintf(stderr, "failed to flush and close\n");
}
一段话讲清二进制模式
文本模式可能会转换换行符(在 Windows 上,写入时 \n 变成 \r\n,读取时再转回来),也可能对某些字节做特殊处理。对于不是文本的数据,比如图片、原样转储的结构体、压缩数据块,应该加上 b 打开,并使用 fread/fwrite,它们搬运的是原始字节:
fwrite(ptr, size, count, fp) 写入 count 个大小为 size 字节的项,返回实际写入的项数;fread 与之对应。要注意,用这种方式写出的文件和写它的机器绑定在一起:结构体填充、整数大小和字节序都会渗入这些字节。所以它适合做缓存或临时文件,但不适合作为其他程序必须读取的格式。
常见错误
- 不检查
fopen是否返回NULL。 随后的崩溃会被归咎于读取,而不是打开。 - 本想用
"r"却用了"w"。 等你发现时,文件已经被清空了。 - 忘记
fclose。 缓冲的输出会丢失,而且每个没关闭的文件都会泄漏一个句柄。 while (!feof(fp))。 会把最后一行处理两次。应该检查读取调用的返回值。char c = fgetc(fp)。 会破坏和EOF的比较。用int。fscanf("%s", buf)不指定宽度。 和从键盘用 scanf 读取一样会缓冲区溢出。- 相对路径。
fopen("data.txt", "r")查找的是工作目录,而不是可执行文件所在的目录。如果文件“消失”了,通常就是这个原因。
常见问题
C 语言中怎么打开文件?
FILE *fp = fopen("data.txt", "r"); 以读取方式打开文件,返回一个 FILE * 句柄;如果失败则返回 NULL。使用句柄之前一定要检查是否为 NULL,用完后调用 fclose(fp)。
C 语言中 fopen 有哪些模式?
"r" 读取(文件必须存在),"w" 写入(创建文件,或者把已有文件截断为空),"a" 追加(创建文件,在末尾写入)。加上 + 后都变成可读可写:"r+"、"w+"、"a+"。加上 b("rb"、"wb")则以二进制模式打开。
C 语言中怎么按行读取文件?
在 while 循环中使用 fgets:while (fgets(line, sizeof line, fp) != NULL) { ... }。它在每个换行符处或缓冲区满时停下,到达文件末尾时返回 NULL,而且因为你传入了缓冲区大小,它不会溢出。
为什么 C 语言写入文件后文件是空的?
最常见的原因是忘了 fclose。输出是有缓冲的,所以程序异常结束时,数据可能还留在内存中。fclose 会刷新缓冲区并关闭文件;fflush(fp) 只刷新不关闭。另一个原因是第二次用 "w" 打开文件,这会把你刚写好的文件截断。