C 语言文件读取,核心就是先用 fopen 打开文件拿到文件指针,再用 fgets 或 fread 把内容读进内存。你第一次写读取文件的小程序时,往往会卡在「函数返回了乱码」「只读了一行就停」「中文变成问号」这些地方。本文基于 C99 标准、在 Windows 11 与 gcc 13 下验证,讲清 fopen 的打开模式、fgets 逐行读、fread 整块读两种做法,并附上对比表和踩坑清单。看完你不仅能把文件读出来,还能理解为什么文本文件和二进制文件要用不同的方式处理。今天这篇文章,编程狮就把这块讲透。
一、先搞清楚:文件是怎么被打开的
在 C 语言教程 里你已经学过变量、数组和函数,但文件是「程序外面的东西」,C 不会自动帮你管理,一切都要自己动手。
打开文件用到 fopen 这个函数,它返回一个 FILE* 类型的文件指针。你可以把文件指针理解成一根「连到文件的管子」:之后的读写都通过这根管子进行,关掉管子(调用 fclose)才算结束。
#include <stdio.h>
int main(void) {
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
printf("文件打开失败\n");
return 1;
}
// ... 读取操作 ...
fclose(fp);
return 0;
}
上面这段里,"r" 表示以只读文本模式打开。和更高级的语言相比,C 把文件细节完全暴露给你(五大语言对比 能看出它们在易用性上的定位差异)。如果文件不存在,fopen 会返回 NULL,所以永远先判断返回值是否为空,这是新手最容易漏的一步。
常用打开模式有:"r" 只读文本、"rb" 只读二进制、"w" 只写(不存在则创建,存在则清空)、"a" 追加。读取场景用 r 或 rb 就够。
实际工程里,打开失败不要只打印一句「失败」就完事。C 标准库会把具体原因写进全局变量 errno,配合 perror("fopen") 能直接打出「No such file or directory」这类可读信息,方便你快速定位是路径写错还是权限不够。一个小技巧:把文件名先用 printf 打出来,确认你以为的路径和程实际上打开的路径一致,能省掉大量排查时间。
二、做法一:用 fgets 逐行读取(最常用)
fgets 一次读一行,碰到换行符或到达缓冲区上限就停。它最适合读文本文件,比如配置文件、日志、csv。
#include <stdio.h>
int main(void) {
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) return 1;
char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
printf("读到:%s", line); // fgets 会把换行符也读进来
}
fclose(fp);
return 0;
}
fgets(line, sizeof(line), fp) 的三个参数分别是:存到哪、最多读多少字符、从哪个文件读。sizeof(line) 是 256,所以一行超过 255 字符会被截断成多段——这不是 bug,是保护你不越界。判断是否读到文件末尾,就看返回值是不是 NULL。
文本模式和二进制模式的差异,很多新手在这栽跟头(C 语言选择指南 也提到过 C 系的底层特性)。在 Windows 上,"r" 会把换行 \n 自动转成 \r\n 再写回,而 "rb" 原样读写,处理图片、压缩包时必须用 rb,否则数据会损坏。
三、做法二:用 fread 整块读取(适合二进制)
fread 不认「行」,它按「元素个数 × 元素大小」整块搬数据,适合读图片、音频、自定义结构体等二进制内容。
#include <stdio.h>
int main(void) {
FILE *fp = fopen("pic.bin", "rb");
if (fp == NULL) return 1;
unsigned char buf[1024];
size_t n = fread(buf, 1, sizeof(buf), fp);
printf("实际读到 %zu 字节\n", n);
fclose(fp);
return 0;
}
fread(buf, 1, sizeof(buf), fp) 表示:每次搬 1 字节,最多搬 sizeof(buf) 次。返回值 n 是实际读到的字节数,它可能小于你请求的数量——文件末尾或读取出错时都会这样,所以判断「读完了没」要看 n 是否小于请求值,或 feof(fp)。
fgets 会连同行尾的换行符一起读进 line,所以打印时如果又自己加了 \n,屏幕上就会多出一个空行。想要「读出来什么样、打出来什么样」,打印用 printf("%s", line) 而不是 printf("%s\n", line);想主动去掉换行,用 line[strcspn(line, "\n")] = '\0' 把换行截掉即可。另外 fgets 遇到文件结尾或出错都返回 NULL,两者要区分可以用 feof(fp) 和 ferror(fp) 进一步判断到底读到哪了。
fread 和 fgets 的关键区别:fgets 遇到换行就停、fread 不管内容一律按字节搬。读文本时若用 fread,换行符不会被特殊处理,跨平台容易出问题;读二进制时若用 fgets,遇到 0x0A 之外的结构就会读歪。一句话:文本用 fgets,二进制用 fread。
读二进制还有个常见需求:把一整个结构体原样存盘再读回。只要结构体里不含有指针(指针存的是地址,换台机器就会失效),fread(&stu, sizeof(stu), 1, fp) 就能一次读回;写入则用对称的 fwrite。这种「内存镜像」式读写简单粗暴,适合做小型数据文件的持久化,但跨平台时要留意字节序(大小端)差异,否则在另一台机器上读出来的数字会是乱的。
四、两种方案对比与怎么选
| 方案 | 适用场景 | 优点 | 代价 |
|---|---|---|---|
| fgets 逐行 | 文本、日志、配置、csv | 逻辑直观,天然按行切分 | 单行超长会被截断 |
| fread 整块 | 图片、音频、结构体 | 原样搬数据,速度快 | 需自己处理边界与结构 |
选法的口诀很简单:你读的是「给人看的字」就用 fgets;你读的是「给机器用的字节」就用 fread。不确定时,先想清楚文件里装的是不是字符串。回到 C 语言文件读取 本身,选法就这一条:文本交给 fgets,二进制交给 fread,别混用。
五、踩坑清单
把文件读出来之后,新手最常卡在下面几处:
- 现象:运行时直接崩溃。
原因:忘了判断
fopen返回值,文件不存在时拿着NULL去读。 办法:每次fopen后先if (fp == NULL) return 1;。 - 现象:中文变成乱码。
原因:文件是 UTF-8,但控制台用 GBK 解码,或读写模式混用了文本/二进制。
办法:确认文件编码,Windows 下读写文本统一用
"r"/"w",二进制统一用"rb"/"wb"。 - 现象:只读了一行程序就结束。
原因:把
fgets写在if里而不是while里,只读了一次。 办法:用while (fgets(...) != NULL)循环读取。 - 现象:程序退出后文件内容没保存。
原因:写了但忘了
fclose,缓冲区没刷盘。 办法:任何打开的文件,用完立刻fclose。
图:Windows 下用 gcc 编译并运行读取示例的命令行界面(需人工补图)
总结
C 语言文件读取的本质,就是 fopen 拿到文件指针、fgets 或 fread 把数据搬进内存、fclose 收尾。要点带走:文本用 fgets 逐行读、二进制用 fread 整块读;打开后第一件事是判断返回值是否为空;读中文乱码多半是编码或模式不对;用完一定记得关闭文件。下一步,把「读」和「写」连起来,你就能写出一个完整的文件处理小程序。
延伸学习
想把 C 语言的地基打牢,可以按这个顺序来:
- 先过一遍 C 语言入门体验课,把指针、数组、文件这些基础串成体系;
- 想系统学完 C 与 C++ 的底层能力,C语言/C++零基础软件入门 是边学边练的形式,适合巩固。
常见问题
Q:fopen 返回 NULL 一定是文件不存在吗?
A:不一定。也可能是路径写错、没有读取权限、或者文件被别的程序独占打开。先打印错误信息,再核对路径和权限。
Q:fgets 和 gets 有什么区别,能用 gets 吗?
A:不要用 gets。gets 不限制读取长度,容易造成缓冲区溢出,已被 C11 标准移除。始终用 fgets 并指定缓冲区大小。
Q:读大文件用 fread 一次全读进来好吗?
A:文件不大可以,但几百 MB 的文件一次性读入会撑爆内存。正确做法是开一个固定大小的缓冲区,用循环分块 fread,读多少处理多少。

免费 AI IDE



