Menu

C 语言文件操作:fopen、fclose 和按行读取文件

如何在 C 语言中读写文件:fopen 模式表,检查 NULL,用 fprintf 和 fputc 写入,用 fgets、fscanf 和 fgetc 读取,标准的按行读取循环,以及什么时候使用二进制模式。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

程序计算出的一切,在它退出时都会消失,除非把它写下来。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" 打开文件,这会把你刚写好的文件截断。

Coddy 编程语言插图

和 Coddy 一起学编程

开始