深入理解 C 语言:文件操作从打开到缓冲区实战
文件是程序与外部世界交换数据的桥梁,也是数据持久化的核心手段。C 语言通过标准 I/O 库提供了一套完整、高效的文件操作接口。本文将从"为什么使用文件"出发,一路深入到文件打开关闭、顺序读写、随机读写、结束判定以及缓冲区机制。所有代码均保证可以直接编译运行,带你彻底吃透文件操作在内存与磁盘之间的处理逻辑。

目录
一、为什么使用文件?
程序运行时数据存放在内存中,一旦程序结束,内存回收,数据随之丢失。若想长期保存数据,就必须将数据写入文件(磁盘)。文件可以让数据独立于程序存在,实现持久化存储。
典型场景:通讯录保存联系人、游戏存档、日志记录、配置读取等。
代码示例:将用户输入的一句话保存到文件,再读取出来。
c
#include <stdio.h>
#include <string.h>
int main(void) {
// 写入文件
FILE *fp = fopen("message.txt", "w");
if (fp == NULL) {
perror("fopen");
return 1;
}
const char *msg = "Hello, file!";
fputs(msg, fp);
fclose(fp);
// 读取文件
char buf[100] = {0};
fp = fopen("message.txt", "r");
if (fp == NULL) {
perror("fopen");
return 1;
}
fgets(buf, sizeof(buf), fp);
printf("读取内容: %s\n", buf);
fclose(fp);
return 0;
}

"要点总结:"
-
文件是数据持久化的基本单位
内存数据程序退出即丢失,文件可以把数据保存到磁盘,供下次运行读取。
-
打开文件必须检查返回值
fopen返回FILE *,失败返回NULL。用perror可以打印失败原因,便于调试。 -
打开模式决定文件行为
"w":只写,文件不存在则创建,存在则清空。"r":只读,文件必须存在,否则打开失败。- 还有
"a"追加、"rb"/"wb"二进制等模式。
-
写入后必须关闭文件
fclose会刷新缓冲区,把尚未写入磁盘的数据真正落盘,并释放文件相关资源。若只写入不关闭,程序异常结束时可能丢失数据。
-
读取时用返回值判断结束
fgets返回NULL表示读取结束或出错。结束后用
feof判断是否正常到达文件尾,用ferror判断是否发生读写错误。
避坑提示:
- 用
"w"打开已存在的文件会清空原内容,操作重要文件前务必确认。 fgets会保留换行符'\n',打印时通常不需要额外加换行。- 缓冲区
buf要足够大,否则一行过长会被截断,剩余内容留在流中。 fclose后不要再次使用同一个fp,否则行为未定义。- 文本文件在不同平台换行符可能不同,跨平台时建议用二进制模式或统一换行处理。
二、什么是文件?
从操作系统角度看,文件是磁盘上的数据集合;从 C 程序角度看,文件是通过 FILE 结构管理的字节流。C 语言中,文件分为程序文件 (源文件、目标文件、可执行文件)和数据文件(程序运行时读写的数据)。本章讨论的是数据文件。
文件名 :通常包含路径、主干和后缀,如 C:\data\test.txt。不写路径则默认当前目录。
C 标准库在程序启动时默认打开三个标准流:
stdin:标准输入,通常对应键盘。stdout:标准输出,通常对应显示器。stderr:标准错误,通常对应显示器。
它们的类型都是 FILE *。我们通过文件指针来操作文件。
代码示例:打印标准流指针,并演示文件指针的基本使用。
c
#include <stdio.h>
int main(void) {
printf("stdin = %p\n", (void*)stdin);
printf("stdout = %p\n", (void*)stdout);
printf("stderr = %p\n", (void*)stderr);
FILE *fp = fopen("test.txt", "w");
if (fp == NULL) {
perror("fopen");
return 1;
}
printf("文件打开成功,FILE* = %p\n", (void*)fp);
fclose(fp);
return 0;
}

" 要点总结:"
-
FILE是描述文件的结构体类型它定义在
<stdio.h>中,内部保存了文件描述符、缓冲区位置、读写状态、错误标志等信息。不同编译器实现细节不同,但用法统一。我们不需要关心其内部成员,只需通过FILE *来操作文件。 -
文件操作围绕
FILE *进行fopen返回FILE *,失败返回NULL。所有文件读写函数(fgetc、fputs、fread、fwrite、fprintf、fscanf等)都需要传入FILE *。关闭文件用fclose。 -
标准流让常用输入输出无需显式打开
C 程序启动时自动打开三个标准流:
stdin:标准输入,通常连接键盘,scanf从它读取。stdout:标准输出,通常连接显示器,printf向它写入。stderr:标准错误,通常也连接显示器,但不会被重定向到普通输出文件,用于打印错误信息。
它们的类型都是FILE *,可以直接用于fprintf、fputs等函数。
-
标准流与普通文件流没有本质区别
它们都是
FILE *,都可以用相同的函数操作。例如fprintf(stdout, "...")等价于printf("...")。区别在于标准流由系统自动管理,普通文件流需要手动fopen/fclose。 -
文件操作的基本流程
打开文件 → 检查是否成功 → 读写 → 检查读写结果 → 关闭文件 → 指针置空。每一步都不能省略,否则可能引发资源泄漏、数据丢失或程序崩溃。
" 避坑提示:"
-
打开文件后务必检查
fp是否为NULL文件不存在、路径错误、权限不足、磁盘满等都会导致
fopen失败。不检查就直接使用,程序会崩溃。cFILE *fp = fopen("test.txt", "w"); if (fp == NULL) { perror("fopen"); return EXIT_FAILURE; } -
文件路径中的反斜杠
\在 C 字符串中需转义为\\Windows 路径
C:\code\test.txt中的\t会被当成制表符。正确写法是"C:\\code\\test.txt",或者直接使用正斜杠"C:/code/test.txt",C 标准库通常也能识别。 -
用
"w"打开已存在的文件会清空原内容如果不想清空,应使用
"a"追加模式,或先判断文件是否存在。操作重要文件前务必备份。 -
fclose后必须把fp置为NULL关闭后文件流已释放,继续使用
fp是未定义行为,可能崩溃或数据错乱。置空可避免野指针。 -
不要对
stdin、stdout、stderr调用fclose它们由系统自动管理,手动关闭会导致后续输入输出异常。标准流只在程序结束时由系统自动关闭。
-
建议检查
fclose的返回值fclose失败可能意味着数据未完整落盘。工程代码应检查:cif (fclose(fp) != 0) { perror("fclose"); return EXIT_FAILURE; } fp = NULL;
三、二进制文件和文本文件
根据数据的存储形式,文件分为:
- 文本文件:以 ASCII 字符形式存储,每个字符占一个字节,便于人阅读。
- 二进制文件:数据按内存中的二进制形式直接存储,不进行字符转换,节省空间但不可直接阅读。
示例 :整数 10000
- 文本文件存储为字符
'1' '0' '0' '0' '0',占 5 字节。 - 二进制文件存储为 4 字节的 int(小端序:0x10 0x27 0x00 0x00),占 4 字节。
代码示例:分别以文本和二进制写入整数,观察文件大小。
c
#include <stdio.h>
int main(void) {
int num = 10000;
// 文本方式写入
FILE *fp = fopen("text.txt", "w");
if (fp == NULL) { perror("fopen"); return 1; }
fprintf(fp, "%d", num);
fclose(fp);
// 二进制方式写入
fp = fopen("binary.bin", "wb");
if (fp == NULL) { perror("fopen"); return 1; }
fwrite(&num, sizeof(int), 1, fp);
fclose(fp);
// 查看文件大小
fp = fopen("text.txt", "rb");
fseek(fp, 0, SEEK_END);
long text_size = ftell(fp);
fclose(fp);
fp = fopen("binary.bin", "rb");
fseek(fp, 0, SEEK_END);
long bin_size = ftell(fp);
fclose(fp);
printf("text.txt 大小: %ld 字节\n", text_size);
printf("binary.bin 大小: %ld 字节\n", bin_size);
return 0;
}

" 要点总结:"
-
文本写入 vs 二进制写入的本质区别
fprintf(fp, "%d", num)写入的是字符'1','0','0','0','0',通常占 5 字节,没有换行符。fwrite(&num, sizeof(int), 1, fp)写入的是num在内存中的原始字节表示,大小是sizeof(int)。常见平台int为 4 字节,所以binary.bin通常是 4 字节。- 二进制内容依赖字节序:小端机器上
10000可能写成10 27 00 00,大端机器上是00 00 27 10,直接用文本编辑器打开会乱码。
-
文件打开与错误处理不完整
- 写入时检查了
fopen是否返回NULL,但后面读取text.txt和binary.bin求大小时没有检查fp == NULL。 - 如果读取时文件打开失败,
fseek(NULL, ...)是未定义行为,通常会导致程序崩溃。 fseek、ftell、fprintf、fwrite、fclose的返回值都没有检查。尤其ftell失败会返回-1L。
- 写入时检查了
-
模式与平台差异
"w"是文本写,"wb"是二进制写;"w"/"wb"都会截断已有文件或创建新文件。- Windows 下文本模式可能把
\n转换成\r\n,但这里没写换行,所以影响不大。 - 求文件大小时用
"rb"更接近真实字节大小;不过 C 标准中文本流的ftell不保证一定是字节偏移,二进制流才是。跨平台求大小更推荐stat、fseeko、ftello。
-
其他注意点
fclose后数据才确保刷新到文件,再读取大小才准确。- 文件路径是相对当前工作目录,受权限、目录是否存在影响。
printf("... %ld ...", text_size)与long类型匹配,格式正确。- 常见运行结果:
text.txt为 5 字节,binary.bin为 4 字节(假设sizeof(int) == 4)。
最关键的一个坑是:读取文件大小时没有判断 fopen 是否成功 。建议在每次 fopen 后都加:
c
if (fp == NULL) {
perror("fopen");
return 1;
}
四、文件的打开和关闭
打开文件 :fopen(文件名, 打开模式)
关闭文件 :fclose(文件指针)
常用打开模式:
| 模式 | 含义 |
|---|---|
"r" |
只读,文件必须存在 |
"w" |
只写,文件不存在则创建,存在则清空 |
"a" |
追加,文件不存在则创建,写入在末尾 |
"r+" |
读写,文件必须存在 |
"w+" |
读写,文件不存在则创建,存在则清空 |
"a+" |
读写,追加 |
加 b |
二进制模式,如 "rb"、"wb" |
代码示例:打开文件并写入,再关闭。
c
#include <stdio.h>
int main(void) {
// 以只写方式打开,文件不存在则创建
FILE *fp = fopen("demo.txt", "w");
if (fp == NULL) {
perror("fopen");
return 1;
}
fputs("Hello, world!\n", fp);
fclose(fp); // 关闭文件,刷新缓冲区
// 以只读方式打开,验证内容
fp = fopen("demo.txt", "r");
if (fp == NULL) {
perror("fopen");
return 1;
}
char buf[100];
fgets(buf, sizeof(buf), fp);
printf("文件内容: %s", buf);
fclose(fp);
return 0;
}

要点总结
-
fopen模式的含义"w":只写,文件不存在则创建,存在则清空/截断。原内容会丢失。"r":只读,文件必须存在,否则fopen返回NULL。- 代码对两次
fopen都做了NULL检查,这点是好的。
-
fputs的返回值没有检查fputs失败会返回EOF,但代码没有判断。- 写入后调用
fclose很重要,它会刷新缓冲区,确保数据真正写入文件。 fclose的返回值也建议检查,因为磁盘满、写入错误可能在关闭时才暴露。
-
fgets的行为fgets(buf, sizeof(buf), fp)最多读sizeof(buf)-1个字符,并自动在末尾加'\0'。- 遇到换行符
'\n'或文件结束EOF会停止。 - 如果读到换行符,换行符会保留在
buf中 ,所以后面printf("文件内容: %s", buf)会原样输出换行。 buf大小是 100,这里内容很短,安全;但如果一行超过 99 个字符,会被截断,剩余内容留在文件流中。
-
没有检查
fgets是否读取成功-
如果
fgets返回NULL(例如文件为空或读取出错),buf的内容是未定义的。 -
此时继续
printf("%s", buf)会使用未初始化数据,属于潜在 bug。 -
更安全写法:
cif (fgets(buf, sizeof(buf), fp) != NULL) { printf("文件内容: %s", buf); } else { printf("读取失败或文件为空\n"); }
-
-
文本模式与平台差异
"w"和"r"是文本模式。在 Windows 上,写入\n可能被转换为\r\n,读取时再转换回\n。- 因此文件实际字节数可能和写入字符数不完全一致。
五、文件的顺序读写
顺序读写指从文件开头依次读写,主要函数:
| 函数 | 功能 | 适用于 |
|---|---|---|
fgetc / fputc |
读写一个字符 | 所有文件 |
fgets / fputs |
读写一行字符串 | 文本文件 |
fscanf / fprintf |
格式化读写 | 文本文件 |
fread / fwrite |
二进制块读写 | 二进制文件 |
代码示例:综合演示字符、字符串、格式化、二进制块读写。
c
#include <stdio.h>
typedef struct {
int id;
char name[20];
float score;
} Student;
int main(void) {
// 1. 字符写入
FILE *fp = fopen("chars.txt", "w");
if (fp == NULL) { perror("fopen"); return 1; }
fputc('A', fp);
fputc('B', fp);
fputc('C', fp);
fclose(fp);
// 2. 字符串写入
fp = fopen("strings.txt", "w");
fputs("Hello\n", fp);
fputs("World\n", fp);
fclose(fp);
// 3. 格式化写入
fp = fopen("format.txt", "w");
fprintf(fp, "%d %s %.1f\n", 1, "Alice", 92.5);
fprintf(fp, "%d %s %.1f\n", 2, "Bob", 88.0);
fclose(fp);
// 4. 二进制块写入
Student s1 = {1, "Alice", 92.5f};
Student s2 = {2, "Bob", 88.0f};
fp = fopen("students.bin", "wb");
fwrite(&s1, sizeof(Student), 1, fp);
fwrite(&s2, sizeof(Student), 1, fp);
fclose(fp);
// 5. 读取验证
printf("--- 字符文件 ---\n");
fp = fopen("chars.txt", "r");
int ch;
while ((ch = fgetc(fp)) != EOF) {
putchar(ch);
}
putchar('\n');
fclose(fp);
printf("--- 字符串文件 ---\n");
fp = fopen("strings.txt", "r");
char line[100];
while (fgets(line, sizeof(line), fp) != NULL) {
printf("%s", line);
}
fclose(fp);
printf("--- 格式化文件 ---\n");
fp = fopen("format.txt", "r");
int id;
char name[20];
float score;
while (fscanf(fp, "%d %s %f", &id, name, &score) == 3) {
printf("%d %s %.1f\n", id, name, score);
}
fclose(fp);
printf("--- 二进制文件 ---\n");
fp = fopen("students.bin", "rb");
Student stu;
while (fread(&stu, sizeof(Student), 1, fp) == 1) {
printf("%d %s %.1f\n", stu.id, stu.name, stu.score);
}
fclose(fp);
return 0;
}

要点总结
- 文件打开与错误检查
-
只有第一次
fopen("chars.txt", "w")检查了fp == NULL。 -
后面所有
fopen都没有检查。如果打开失败,fp为NULL,继续调用fputs、fwrite、fgetc、fgets、fscanf、fread等都是未定义行为,通常直接崩溃。 -
每次
fopen后都应加:cif (fp == NULL) { perror("fopen"); return 1; } -
fclose的返回值也没检查。磁盘满、写入失败可能在fclose时才暴露。
- 写入操作的返回值
fputc、fputs、fprintf、fwrite的返回值都没有检查。- 例如:
fputc失败返回EOF;fputs失败返回EOF;fprintf返回实际写入字符数,负值表示错误;fwrite返回成功写入的元素个数,应该判断是否等于预期值。
- 生产代码中应检查这些返回值,避免"以为写成功了,其实没写全"。
- 文本模式 vs 二进制模式
- 写字符、字符串、格式化文本用
"w"/"r",属于文本模式。 - 写结构体用
"wb"/"rb",属于二进制模式,这是正确的。 - 在 Windows 上,文本模式会把
\n转换成\r\n,读取时再转换回来;二进制模式不会转换。 "w"会截断已有文件,文件不存在则创建。所以每次运行都会覆盖原文件。
- 读取操作的注意点
fgetc返回int,用int ch接收是正确的,这样才能区分EOF和普通字符0xFF。fgets会保留换行符\n,所以printf("%s", line)输出时通常会自带换行。- 如果一行超过
sizeof(line)-1,fgets会截断,下一次继续读剩余部分,输出可能没有换行。 fscanf(fp, "%d %s %f", &id, name, &score) == 3的循环条件是对的,但:%s没有指定宽度,name[20]可能被超长字符串溢出。应写成%19s。- 如果文件格式不对,
fscanf会提前结束,无法区分是正常 EOF 还是格式错误。
- 循环结束后可以检查
feof/ferror来确认原因。
- 结构体二进制读写的可移植性
c
fwrite(&s1, sizeof(Student), 1, fp);
fread(&stu, sizeof(Student), 1, fp);
这种直接读写结构体的方式有几个问题:
- 平台相关 :
int大小、float表示、字节序、结构体对齐和填充都可能不同。 - 填充字节 :
Student中可能有编译器插入的填充字节,内容不确定,写入文件后可能包含垃圾数据。 - 跨平台不可读:小端机器写的文件,大端机器直接读会出错。
- 安全/版本问题:结构体定义一变,旧文件就可能读不出来。
- 如果只是本机临时保存,可以用;如果要跨平台或长期存储,建议逐字段序列化,例如固定宽度整数、固定长度字符串,并明确字节序。
六、文件的随机读写
随机读写指可以定位到文件的任意位置进行读写,主要函数:
fseek(fp, offset, origin):移动文件指针。
origin:SEEK_SET(文件头)、SEEK_CUR(当前位置)、SEEK_END(文件尾)。ftell(fp):返回当前偏移量。rewind(fp):将指针移回文件开头。
代码示例:写入若干数据后,随机读取第三个结构体。
c
#include <stdio.h>
typedef struct {
int id;
char name[20];
float score;
} Student;
int main(void) {
Student stu[3] = {
{1, "Alice", 92.5f},
{2, "Bob", 88.0f},
{3, "Cindy", 95.5f}
};
// 写入三个学生到二进制文件
FILE *fp = fopen("students.bin", "wb");
if (fp == NULL) { perror("fopen"); return 1; }
fwrite(stu, sizeof(Student), 3, fp);
fclose(fp);
// 随机读取第二个学生(下标1)
fp = fopen("students.bin", "rb+");
if (fp == NULL) { perror("fopen"); return 1; }
// 定位到第二个学生:偏移 1 * sizeof(Student)
fseek(fp, 1 * sizeof(Student), SEEK_SET);
Student s;
fread(&s, sizeof(Student), 1, fp);
printf("第二个学生: %d %s %.1f\n", s.id, s.name, s.score);
// 修改第三个学生
fseek(fp, 2 * sizeof(Student), SEEK_SET);
Student new_s = {3, "Cindy", 99.0f};
fwrite(&new_s, sizeof(Student), 1, fp);
// 定位到文件末尾,获取文件大小
fseek(fp, 0, SEEK_END);
long size = ftell(fp);
printf("文件大小: %ld 字节\n", size);
// 回到开头读取
rewind(fp);
while (fread(&s, sizeof(Student), 1, fp) == 1) {
printf("记录: %d %s %.1f\n", s.id, s.name, s.score);
}
fclose(fp);
return 0;
}

要点总结
fseek可移动文件指针到任意位置。ftell获取当前偏移,常用于计算文件大小。rewind快速回到文件开头。- 随机读写常用于二进制文件,配合
fread/fwrite。
避坑提示
fseek偏移量必须是long类型。- 文本文件中
fseek行为可能受换行符转换影响,建议二进制模式使用。 - 移动指针后读写前需确保位置有效。
一、核心函数:
1. fseek ------ 移动文件指针
c
int fseek(FILE *stream, long offset, int whence);
offset:偏移量,单位是字节。whence:基准位置,可取:SEEK_SET:文件开头SEEK_CUR:当前位置SEEK_END:文件末尾
- 返回值:成功返回
0,失败返回非0。 - 用途:随机读写时定位到任意记录。
2. ftell ------ 获取当前偏移
c
long ftell(FILE *stream);
-
返回当前文件指针相对于文件开头的字节偏移。
-
失败返回
-1L,需检查。 -
常配合
fseek(fp, 0, SEEK_END)计算文件大小:cfseek(fp, 0, SEEK_END); long size = ftell(fp); if (size == -1L) { perror("ftell"); }
3. rewind ------ 回到文件开头
c
void rewind(FILE *stream);
- 等价于
fseek(stream, 0, SEEK_SET),同时清除错误指示器。 - 无返回值,不能直接判断是否成功。
4. fread / fwrite ------ 二进制块读写
c
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
-
返回值是成功读/写的元素个数,不是字节数。
-
必须检查是否等于预期个数。
-
随机读写常配合
fseek定位到第i条记录:clong offset = i * (long)sizeof(Record); fseek(fp, offset, SEEK_SET); fread(&rec, sizeof(Record), 1, fp);
二、随机读写要点:
-
按记录定长存储
每条记录大小固定(如
sizeof(Student)),才能用i * sizeof(Record)直接定位。 -
偏移量计算
clong offset = index * (long)sizeof(Student); fseek(fp, offset, SEEK_SET);sizeof返回size_t,fseek需要long,显式转换避免警告。- 大文件(>2GB)应使用
fseeko/ftello或平台 64 位接口。
-
读写模式选择
"rb":只读二进制"wb":只写二进制,会截断"rb+":读写二进制,文件必须存在,不截断"wb+":读写二进制,会截断- 随机读写通常用
"rb+"。
-
更新流的读写切换
对
"rb+"这类更新流,C 标准要求:- 写后要读 → 必须
fflush或调用fseek/rewind; - 读后要写 → 必须调用文件定位函数。
- 否则行为未定义。
- 写后要读 → 必须
-
读取循环
cwhile (fread(&rec, sizeof(Record), 1, fp) == 1) { // 处理记录 } if (ferror(fp)) { /* 读取出错 */ }fread返回 0 可能是 EOF,也可能是错误,用feof/ferror区分。- 文件大小不是记录大小整数倍时,末尾半条记录会被忽略。
三、避坑提示:
- 偏移量类型
-
fseek第二个参数是long,不是size_t。 -
直接写
fseek(fp, i * sizeof(Record), SEEK_SET);可能产生隐式转换警告。 -
推荐:
cfseek(fp, (long)(i * sizeof(Record)), SEEK_SET); -
大文件用
fseeko/ftello。
- 文本模式 vs 二进制模式
- 文本模式(
"r"/"w")下,fseek行为受换行符转换影响(尤其 Windows)。 - 标准只保证:
fseek(fp, 0, SEEK_SET)fseek(fp, ftell(fp), SEEK_SET)fseek(fp, 0, SEEK_END)
这些有限操作可移植。
- 随机读写务必用二进制模式 (
"rb"/"wb"/"rb+")。
- 移动指针后检查位置有效性
-
fseek可能失败(如越界、设备不支持)。 -
每次
fseek后检查返回值:cif (fseek(fp, offset, SEEK_SET) != 0) { perror("fseek"); // 处理错误 } -
读取前确认当前位置有数据,避免读到未初始化内容。
-
返回值检查不能省
| 函数 | 失败返回 | 后果 |
|---|---|---|
|
fopen|NULL| 后续操作崩溃 ||
fseek| 非 0 | 位置错误 ||
ftell|-1L| 大小错误 ||
fread| < 预期个数 | 数据不完整 ||
fwrite| < 预期个数 | 写入失败 ||
fclose|EOF| 缓冲区未刷新,数据丢失 | -
二进制结构体读写的可移植性
- 直接
fwrite(&struct, sizeof(struct), 1, fp)有以下问题:- 结构体填充字节内容不确定;
int、float大小和字节序随平台变化;- 结构体定义变更导致旧文件不可读。
- 跨平台或长期存储应逐字段序列化(固定宽度类型 + 明确字节序),或改用文本格式(CSV/JSON)。
- 字符串安全
-
从文件读出的
char name[N]不保证以'\0'结尾。 -
使用前应手动补结尾:
cs.name[sizeof(s.name) - 1] = '\0'; -
写入时确保不溢出,可用
snprintf或strncpy并补'\0'。
- 文件大小计算的正确姿势
c
if (fseek(fp, 0, SEEK_END) != 0) { perror("fseek"); }
long size = ftell(fp);
if (size == -1L) { perror("ftell"); }
- 二进制模式下结果才是准确的字节数。
- 计算完大小后若要继续读,记得
rewind或fseek回开头。
- 记录数不整时的处理
-
文件大小 /
sizeof(Record)可能有余数。 -
遍历时用
fread(...) == 1判断,多余字节会被忽略。 -
若需要严格校验,可先算记录数:
clong count = size / (long)sizeof(Record);
七、文件读取结束的判定
常见误区 :用 feof(fp) 判断文件是否结束。
正确做法 :根据读取函数的返回值判断。feof 只在读取函数因遇到文件尾而返回失败后,才返回真。
fgetc返回EOF表示结束或错误。fgets返回NULL表示结束或错误。fread返回小于请求项数表示结束或错误。feof(fp)判断是否因文件尾结束。ferror(fp)判断是否因错误结束。
代码示例:正确读取文件并区分结束与错误。
c
#include <stdio.h>
int main(void) {
FILE *fp = fopen("students.txt", "r");
if (fp == NULL) { perror("fopen"); return 1; }
int ch;
while ((ch = fgetc(fp)) != EOF) {
putchar(ch);
}
// 判断结束原因
if (feof(fp)) {
printf("\n正常读到文件末尾。\n");
} else if (ferror(fp)) {
perror("读取错误");
}
fclose(fp);
return 0;
}

二进制文件读取示例:
c
#include <stdio.h>
enum { SIZE = 5 };
int main(void) {
double a[SIZE] = {1.1, 2.2, 3.3, 4.4, 5.5};
FILE *fp = fopen("doubles.bin", "wb");
if (!fp) { perror("fopen"); return 1; }
fwrite(a, sizeof *a, SIZE, fp);
fclose(fp);
double b[SIZE];
fp = fopen("doubles.bin", "rb");
if (!fp) { perror("fopen"); return 1; }
size_t ret = fread(b, sizeof *b, SIZE, fp);
if (ret == SIZE) {
printf("读取成功: ");
for (int i = 0; i < SIZE; i++) printf("%.1f ", b[i]);
printf("\n");
} else {
if (feof(fp)) printf("意外到达文件末尾\n");
else if (ferror(fp)) perror("读取错误");
}
fclose(fp);
return 0;
}

要点总结
- 不要用
feof作为循环条件。 - 读取函数的返回值才是判断依据。
feof和ferror用于事后判断原因。
避坑提示
feof在读取失败后才置位,提前调用无意义。- 二进制文件读取时,
fread返回值小于预期可能是文件尾或错误,需结合feof/ferror。
八、文件缓冲区
C 标准 I/O 库使用缓冲区提高效率。数据先写入内存缓冲区,再批量写入磁盘。缓冲区分为:
- 全缓冲:普通文件,缓冲区满才刷新。
- 行缓冲:终端设备(如屏幕),遇换行刷新。
- 无缓冲 :标准错误
stderr,立即输出。
刷新缓冲区:fflush(fp)。程序正常结束或 fclose 也会刷新。
代码示例:演示缓冲区行为。
c
#include <stdio.h>
int main(void) {
FILE *fp = fopen("buffer.txt", "w");
if (fp == NULL) { perror("fopen"); return 1; }
fputs("Hello, buffer!", fp);
// 此时数据可能还在缓冲区,未写入磁盘
printf("数据已写入缓冲区,按回车继续...\n");
getchar();
// 刷新缓冲区,强制写入磁盘
fflush(fp);
printf("缓冲区已刷新,数据已写入磁盘。\n");
fclose(fp);
return 0;
}

运行后,打开 buffer.txt 会看到内容。若注释掉 fflush,在按回车前文件可能为空(取决于系统)。
要点总结
- 缓冲区减少系统调用,提高效率。
fflush强制刷新,fclose自动刷新。- 程序异常终止可能丢失缓冲区数据。
避坑提示
- 调试时若文件内容未出现,可能因缓冲区未刷新。
- 多线程或信号处理中注意缓冲区安全。
fflush(stdin)不是标准行为,不要使用。
九、总结
文件操作是 C 语言中连接程序与磁盘的纽带。掌握以下心法,即可游刃有余:
- 为何用文件:持久化存储,数据独立于程序。
- 文件是什么 :
FILE结构,通过指针操作。 - 文本与二进制:文本可读,二进制高效,跨平台慎用二进制。
- 打开与关闭 :
fopen选对模式,fclose必须调用。 - 顺序读写:字符、字符串、格式化、二进制块,各司其职。
- 随机读写 :
fseek定位,ftell测偏移,rewind回开头。 - 结束判定 :看返回值,
feof/ferror仅作事后分析。 - 缓冲区 :提高效率,
fflush强制刷新,fclose自动刷新。
核心心法 :打开要判空,关闭要牢记;读写看返回,缓冲区要刷新。
掌握文件操作,你的 C 程序就能与整个世界交换数据。
如果觉得有用,欢迎点赞收藏支持一下!