29、文件定位与错误处理:fseek, ftell, rewind, feof, ferror
文件操作,说白了就是跟数据流打交道。你打开文件、读写数据,但很多时候你并不知道自己「读到哪了」。我刚开始做嵌入式项目时,就吃过这个亏——读配置文件读到一半,程序突然崩溃,查了半天才发现是文件指针跑偏了。
嗯,今天我们就来聊聊文件定位和错误处理。这几个函数虽然基础,但用不好真的会出大问题。
文件指针的「当前位置」
每个打开的文件,系统都会维护一个内部指针,叫「文件位置指示器」。你每次读写,这个指针都会自动往后移动。你可以把它想象成书签——你读到哪,书签就夹到哪。
那问题来了:怎么知道当前读到哪了?怎么跳回去重读?
ftell:告诉我你在哪
ftell 的作用很简单——返回当前文件指针相对于文件开头的偏移量(字节数)。
long ftell(FILE *stream);
返回值是 long 类型,如果出错返回 -1L。
典型用法:获取文件大小
FILE *fp = fopen("data.bin", "rb");
if (fp == NULL) {
perror("打开文件失败");
return -1;
}
fseek(fp, 0, SEEK_END); // 跳到文件末尾
long size = ftell(fp); // 获取偏移量 = 文件大小
printf("文件大小: %ld 字节\n", size);
fclose(fp);
我在项目中遇到过一个问题:用 ftell 获取文件大小后,忘了把指针复位,结果后续读取全乱了。你想想看,指针在末尾,你读什么?
fseek:想去哪就去哪
fseek 是文件定位的核心函数。它允许你把文件指针移动到任意位置。
int fseek(FILE *stream, long offset, int whence);
三个参数:
- stream:文件指针
- offset:偏移量(可正可负)
- whence:起始位置,有三个宏
| 宏 | 值 | 含义 |
|---|---|---|
| SEEK_SET | 0 | 从文件开头算起 |
| SEEK_CUR | 1 | 从当前位置算起 |
| SEEK_END | 2 | 从文件末尾算起 |
成功返回 0,失败返回非 0。
小技巧:用 fseek(fp, -10, SEEK_END) 可以跳到文件末尾前 10 个字节的位置。这在读取文件尾部数据时非常实用。
// 读取文件最后 20 个字节
FILE *fp = fopen("log.txt", "r");
if (fp == NULL) return -1;
fseek(fp, -20, SEEK_END);
char buf[21] = {0};
fread(buf, 1, 20, fp);
printf("文件尾部: %s\n", buf);
fclose(fp);
rewind:一键复位
rewind 相当于 fseek(fp, 0, SEEK_SET),但它还会清除文件的错误标志和 EOF 标志。
void rewind(FILE *stream);
没有返回值,也不会告诉你是否失败。我个人习惯用 fseek 代替 rewind,因为 fseek 能返回错误状态。
注意:rewind 不会返回错误。如果文件流已经损坏,你调用 rewind 后可能仍然无法正常读写,但你完全不知道。
feof:真的读到文件尾了吗?
feof 用来检测是否到达文件末尾。但这里有个坑——它不是在读取之前判断,而是在读取之后判断。
int feof(FILE *stream);
到达文件末尾返回非 0,否则返回 0。
我曾经犯过一个经典错误:
// ❌ 错误用法
while (!feof(fp)) {
fgets(buf, sizeof(buf), fp);
printf("%s", buf);
}
为什么会这样?因为 feof 只有在读取操作尝试越过文件末尾后才会返回真。上面的循环会多读一次,打印出重复或无效的数据。
正确的做法是:
// ✅ 正确用法
while (fgets(buf, sizeof(buf), fp) != NULL) {
printf("%s", buf);
}
核心原则:永远不要用 feof 作为循环条件。用读取函数的返回值来判断是否结束。
ferror:出错了别闷着
ferror 用来检查文件流是否发生了错误。一旦出错,这个错误标志会一直保持,直到你调用 clearerr 或 rewind。
int ferror(FILE *stream);
有错误返回非 0,否则返回 0。
FILE *fp = fopen("data.bin", "rb");
if (fp == NULL) {
perror("打开失败");
return -1;
}
int ch;
while ((ch = fgetc(fp)) != EOF) {
// 处理字符
}
if (ferror(fp)) {
printf("读取过程中发生了错误\n");
clearerr(fp); // 清除错误标志
}
fclose(fp);
嗯,这里要注意:EOF 既可能是文件结束,也可能是读取错误。所以读完数据后,一定要用 ferror 确认一下到底是哪种情况。
clearerr:重置状态
当你处理完错误或到达文件末尾后,如果想继续操作这个文件流,需要调用 clearerr 清除错误标志和 EOF 标志。
void clearerr(FILE *stream);
没有返回值。调用后,feof 和 ferror 都会返回 0。
知识体系图
下面这张图帮你理清这几个函数的关系:
综合示例:安全读写二进制文件
把上面这些函数串起来,写一个健壮的文件读取函数:
#include <stdio.h>
#include <stdlib.h>
int read_file_safe(const char *filename, unsigned char *buffer, size_t size) {
FILE *fp = fopen(filename, "rb");
if (fp == NULL) {
perror("fopen");
return -1;
}
// 获取文件大小
fseek(fp, 0, SEEK_END);
long file_size = ftell(fp);
if (file_size < 0) {
perror("ftell");
fclose(fp);
return -1;
}
// 检查缓冲区是否足够
if ((size_t)file_size > size) {
fprintf(stderr, "缓冲区太小\n");
fclose(fp);
return -1;
}
// 回到文件开头
rewind(fp);
// 读取数据
size_t bytes_read = fread(buffer, 1, file_size, fp);
if (bytes_read != (size_t)file_size) {
if (ferror(fp)) {
perror("fread");
} else if (feof(fp)) {
fprintf(stderr, "意外到达文件末尾\n");
}
fclose(fp);
return -1;
}
fclose(fp);
return file_size;
}
个人建议:每次调用 fread 或 fwrite 后,都检查一下 ferror。尤其是在嵌入式环境下,SD 卡可能突然拔出,文件系统可能出错。别等到程序崩溃了才去查。
避坑指南
- 文本模式下的 fseek:在 Windows 上,文本模式会做换行符转换(\n → \r\n),导致
ftell返回的值不可靠。我建议二进制文件用"rb"或"wb"模式。 - fseek 的 offset 不要太大:在 32 位系统上,
long最大只有 2GB。超过 2GB 的文件需要用fseeko64或平台相关函数。 - rewind 不报错:如果你需要知道操作是否成功,用
fseek(fp, 0, SEEK_SET)代替。 - feof 和 ferror 要配合使用:单独用
feof判断文件结束是不够的,因为读取错误也会导致循环退出。
好了,文件定位和错误处理就讲到这里。这几个函数看着简单,但用好了能让你的代码健壮很多。下次写文件操作时,记得加上错误检查——别像我当年那样,出了问题才回头补。