第二十六课:文件操作——文件指针、打开关闭与读写定位
文件操作,说白了就是让程序和磁盘上的文件打交道。你写的数据要存下来,下次启动还能用,就得靠文件。我刚开始学C语言时,觉得文件操作挺神秘的,后来发现其实就几个核心动作:打开、读写、关闭、定位。今天咱们就把这些事彻底讲清楚。
26.1 文件指针——操作文件的“遥控器”
在C语言里,所有文件操作都离不开一个东西——FILE指针。它不是指向文件内容,而是指向一个结构体,这个结构体里存着文件的各种状态信息,比如当前读写位置、缓冲区状态、错误标志等。
核心概念:文件指针是操作文件的“遥控器”,你通过它告诉系统“我要读哪个文件”、“读到哪了”、“有没有出错”。
// 声明一个文件指针
FILE *fp;
这个fp就是你的遥控器。没有它,你什么都干不了。我个人习惯在声明时就初始化为NULL,避免野指针问题。
FILE *fp = NULL; // 好习惯
26.2 文件的打开与关闭——有开必有合
打开文件用fopen(),关闭用fclose()。这俩必须成对出现,就像malloc和free一样。我曾经见过一个同事,打开文件后忘了关,程序跑了一整天,最后磁盘写满了……嗯,从那以后我每次写fopen,都会立刻把fclose也写上。
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
printf("文件打开失败!\n");
return -1;
}
// ... 读写操作 ...
fclose(fp);
fopen()的第二个参数是模式字符串,决定了你怎么操作这个文件。常用的模式有这些:
| 模式 | 含义 | 文件不存在时 | 文件存在时 |
|---|---|---|---|
| "r" | 只读 | 返回NULL | 正常打开 |
| "w" | 只写 | 创建新文件 | 清空内容 |
| "a" | 追加 | 创建新文件 | 保留内容,从末尾写 |
| "r+" | 读写 | 返回NULL | 正常打开 |
| "w+" | 读写 | 创建新文件 | 清空内容 |
| "a+" | 读写追加 | 创建新文件 | 保留内容,从末尾写 |
注意:用"w"模式打开文件,如果文件已存在,内容会被直接清空!我曾经在项目中误操作,把一个配置文件用"w"打开了,结果配置全没了……从那以后,我每次用"w"模式都会再三确认。
26.3 文件的读写——字符、字符串、格式化
读写文件有好几种方式,咱们一个一个来看。
26.3.1 字符读写:fgetc / fputc
这两个函数一次读写一个字符。你想想看,如果文件很大,一个一个字符读效率确实低,但有时候我们需要精细控制,比如逐字符解析。
// 写一个字符到文件
fputc('A', fp);
// 从文件读一个字符
int ch = fgetc(fp);
if (ch == EOF) {
printf("读到文件末尾了\n");
}
这里要注意,fgetc()返回的是int而不是char,因为除了正常字符,它还可能返回EOF(-1)。如果用char接收,在某些系统上会出问题。
26.3.2 字符串读写:fgets / fputs
按行读写文件,这是最常用的方式。我个人在写日志文件时特别喜欢用fputs,因为它不会自动加换行符,我可以自己控制格式。
// 写字符串
fputs("Hello, World!\n", fp);
// 读字符串
char buffer[256];
if (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("读到:%s", buffer);
}
fgets()会读取一整行(包括换行符),最多读n-1个字符,然后自动加'\0'。这个设计很安全,不会造成缓冲区溢出。
小技巧:用fgets()时,缓冲区大小一定要给够。我一般给256或1024,根据实际行长度来定。如果行太长,fgets()会分多次读取,需要自己处理拼接。
26.3.3 格式化读写:fscanf / fprintf
这两个函数和scanf/printf用法几乎一样,只是多了一个文件指针参数。适合读写结构化的数据。
// 写入格式化数据
int id = 1001;
char name[] = "张三";
float score = 95.5;
fprintf(fp, "%d %s %.1f\n", id, name, score);
// 读取格式化数据
int rid;
char rname[50];
float rscore;
fscanf(fp, "%d %s %f", &rid, rname, &rscore);
用fscanf时要注意,它遇到空格或换行就会停止读取。如果数据里有空格,最好用fgets自己解析。
26.4 文件定位——想去哪就去哪
文件内部有一个位置指针,指向当前读写的位置。默认打开文件时,位置指针在开头(追加模式在末尾)。我们可以用fseek()和ftell()来移动和查询这个指针。
// 移动到文件开头
fseek(fp, 0, SEEK_SET);
// 移动到文件末尾
fseek(fp, 0, SEEK_END);
// 从当前位置向后移动10个字节
fseek(fp, 10, SEEK_CUR);
// 获取当前位置(相对于开头的偏移量)
long pos = ftell(fp);
fseek()的第三个参数有三个选项:
SEEK_SET:从文件开头算起SEEK_CUR:从当前位置算起SEEK_END:从文件末尾算起
实用场景:用fseek(fp, 0, SEEK_END)配合ftell(fp)可以获取文件大小。我在项目中经常用这个方法来预分配缓冲区。
// 获取文件大小
fseek(fp, 0, SEEK_END);
long fileSize = ftell(fp);
rewind(fp); // 回到文件开头,等价于 fseek(fp, 0, SEEK_SET)
26.5 知识体系总览
下面这张图把文件操作的核心知识点串起来了,你可以对照着复习:
26.6 避坑指南与实战经验
文件操作看着简单,但坑不少。我把自己踩过的坑总结一下:
- 忘记检查fopen返回值:文件可能因为权限、路径不对等原因打不开。不检查就直接用,程序会崩溃。
- 忘记关闭文件:文件描述符是有限资源,不关闭会导致泄漏。严重时程序无法再打开新文件。
- 用"w"模式误清空文件:这个我前面提过,一定要确认模式是否正确。
- fscanf格式不匹配:如果文件内容和格式字符串对不上,读取会失败,而且位置指针会停在错误的地方。
- 二进制和文本模式混用:在Windows上,文本模式会做换行符转换(\n变成\r\n),二进制模式不会。跨平台时要注意。
我的习惯:每次写完文件操作代码,我都会用valgrind检查内存泄漏,用strace跟踪系统调用。这样能确保文件操作万无一失。
好了,文件操作的核心内容就这些。记住:打开→读写→关闭,三步走,缺一不可。多写多练,慢慢就会形成肌肉记忆。