C: 文件操作
文件就像一个保险柜——用之前要打开(fopen),取放东西要按规矩来(读写方式),用完必须锁好(fclose),不然东西可能丢了。
1. 文件的基本概念
C语言把所有外部设备都当作"流"来处理。文件流分为两种:
- 文本流:可见字符组成,换行符会被转换,适合人类阅读
- 二进制流:原始字节序列,原样读写,适合程序处理
操作系统通过一个 FILE 结构体来管理打开的文件,我们通过 FILE * 指针来操作它。
(1) 文件指针
C
FILE *fp;
这个指针是后续所有文件操作的"钥匙"。没有它,什么都做不了。
2. 打开与关闭文件
(1) fopen
C
FILE *fopen(const char *filename, const char *mode);
| 模式 | 含义 | 文件不存在时 | 文件已存在时 |
|---|---|---|---|
"r" |
只读 | 出错 | 从头读 |
"w" |
只写 | 创建 | 清空内容 |
"a" |
追加 | 创建 | 末尾追加 |
"r+" |
读写 | 出错 | 从头操作 |
"w+" |
读写 | 创建 | 清空内容 |
"a+" |
读追加 | 创建 | 末尾追加 |
"rb" |
二进制只读 | 出错 | 从头读 |
"wb" |
二进制只写 | 创建 | 清空内容 |
"ab" |
二进制追加 | 创建 | 末尾追加 |
💡
"w" 模式会清空已有文件!这是新手最常犯的错误——本想追加内容却用了 "w" 导致数据全丢了。
(2) fclose
C
int fclose(FILE *fp);
关闭文件会刷新缓冲区并释放资源。成功返回 0,失败返回 EOF。
C
#include <stdio.h>
int main(void) {
FILE *fp = fopen("data.txt", "w");
if (fp == NULL) {
printf("无法打开文件\n");
return 1;
}
fprintf(fp, "Hello, File!\n");
fclose(fp);
return 0;
}
⚠️ 每一个
fopen 必须对应一个 fclose。忘记关闭文件会导致内存泄漏和数据丢失。
3. 文本文件读写
(1) 字符读写:fgetc / fputc
C
int fgetc(FILE *fp);
int fputc(int ch, FILE *fp);
fgetc 读取一个字符,到达文件末尾返回 EOF。fputc 写入一个字符。
(2) 字符串读写:fgets / fputs
C
char *fgets(char *str, int n, FILE *fp);
int fputs(const char *str, FILE *fp);
fgets 最多读 n-1 个字符,遇到换行或文件末尾停止,自动添加 '\0'。
C
#include <stdio.h>
int main(void) {
FILE *fp = fopen("poem.txt", "w");
if (fp == NULL) {
return 1;
}
fputs("白日依山尽\n", fp);
fputs("黄河入海流\n", fp);
fclose(fp);
fp = fopen("poem.txt", "r");
if (fp == NULL) {
return 1;
}
char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
printf("%s", line);
}
fclose(fp);
return 0;
}
TEXT
📖 仅展示
白日依山尽
黄河入海流
4. 二进制文件读写
(1) fread / fwrite
C
size_t fread(void *ptr, size_t size, size_t count, FILE *fp);
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *fp);
参数含义:ptr 是数据缓冲区,size 是每个元素的字节数,count 是元素个数。返回值是实际读写的元素个数。
C
#include <stdio.h>
typedef struct {
int id;
char name[20];
float score;
} Student;
int main(void) {
Student stu = {1, "张三", 92.5f};
FILE *fp = fopen("student.dat", "wb");
if (fp == NULL) {
return 1;
}
fwrite(&stu, sizeof(Student), 1, fp);
fclose(fp);
Student read_stu;
fp = fopen("student.dat", "rb");
if (fp == NULL) {
return 1;
}
fread(&read_stu, sizeof(Student), 1, fp);
fclose(fp);
printf("学号: %d, 姓名: %s, 成绩: %.1f\n",
read_stu.id, read_stu.name, read_stu.score);
return 0;
}
TEXT
📖 仅展示
学号: 1, 姓名: 张三, 成绩: 92.5
💡 二进制模式适合存储结构体数据,效率高且不会改变内容。文本模式在不同操作系统间可能换行符不同,二进制模式则原样保存。
5. 格式化读写
(1) fprintf / fscanf
C
int fprintf(FILE *fp, const char *format, ...);
int fscanf(FILE *fp, const char *format, ...);
用法和 printf/scanf 完全一样,只是多了一个文件指针参数。
C
#include <stdio.h>
int main(void) {
FILE *fp = fopen("scores.txt", "w");
if (fp == NULL) {
return 1;
}
fprintf(fp, "%s %d %.1f\n", "李四", 2, 88.0);
fprintf(fp, "%s %d %.1f\n", "王五", 3, 95.5);
fclose(fp);
fp = fopen("scores.txt", "r");
if (fp == NULL) {
return 1;
}
char name[20];
int id;
float score;
while (fscanf(fp, "%s %d %f", name, &id, &score) == 3) {
printf("姓名: %s, 学号: %d, 成绩: %.1f\n", name, id, score);
}
fclose(fp);
return 0;
}
TEXT
📖 仅展示
姓名: 李四, 学号: 2, 成绩: 88.0
姓名: 王五, 学号: 3, 成绩: 95.5
6. 文件位置指针
每个打开的文件都有一个"位置指针",指示当前读写位置。就像读书时的书签——你读到哪,书签就夹在哪。
(1) fseek
C
int fseek(FILE *fp, long offset, int origin);
| origin | 含义 |
|---|---|
SEEK_SET |
文件开头 |
SEEK_CUR |
当前位置 |
SEEK_END |
文件末尾 |
(2) ftell / rewind
C
long ftell(FILE *fp);
void rewind(FILE *fp);
ftell 返回当前位置(相对文件开头的字节数)。rewind 等价于 fseek(fp, 0, SEEK_SET)。
▶ 示例
用 fseek 和 ftell 计算文件大小:
C
#include <stdio.h>
int main(void) {
FILE *fp = fopen("data.txt", "rb");
if (fp == NULL) {
printf("文件打开失败\n");
return 1;
}
fseek(fp, 0, SEEK_END);
long size = ftell(fp);
rewind(fp);
printf("文件大小: %ld 字节\n", size);
fclose(fp);
return 0;
}
▶ 示例
从二进制文件中随机读取指定记录:
C
#include <stdio.h>
typedef struct {
int id;
char name[20];
float score;
} Student;
int main(void) {
Student students[3] = {
{1, "张三", 92.5f},
{2, "李四", 88.0f},
{3, "王五", 95.5f}
};
FILE *fp = fopen("students.dat", "wb");
if (fp == NULL) {
return 1;
}
fwrite(students, sizeof(Student), 3, fp);
fclose(fp);
fp = fopen("students.dat", "rb");
if (fp == NULL) {
return 1;
}
int target = 2;
fseek(fp, (target - 1) * sizeof(Student), SEEK_SET);
Student s;
fread(&s, sizeof(Student), 1, fp);
fclose(fp);
printf("第%d条记录: 学号=%d, 姓名=%s, 成绩=%.1f\n",
target, s.id, s.name, s.score);
return 0;
}
TEXT
📖 仅展示
第2条记录: 学号=2, 姓名=李四, 成绩=88.0
🔥 这就是数据库"随机访问"的原理!通过计算偏移量直接跳到指定位置,不需要从头遍历。
7. 其他文件函数
(1) feof
C
int feof(FILE *fp);
检测是否到达文件末尾。到达返回非零值。注意:feof 是在读取操作失败后才变为真的,不是提前判断。
(2) fflush
C
int fflush(FILE *fp);
强制刷新缓冲区,把缓冲区数据写入文件。对输入流调用 fflush 的行为是未定义的。
(3) remove / rename
C
int remove(const char *filename);
int rename(const char *old, const char *new);
删除文件和重命名文件。成功返回 0。
8. 文件操作的通用模式
大部分文件操作都遵循这个模式:
- 用
fopen打开文件,检查返回值 - 循环读写数据
- 用
fclose关闭文件
C
#include <stdio.h>
int main(void) {
FILE *fp = fopen("input.txt", "r");
if (fp == NULL) {
perror("fopen");
return 1;
}
int ch;
while ((ch = fgetc(fp)) != EOF) {
putchar(ch);
}
fclose(fp);
return 0;
}
⚠️ 检查
fopen 返回值是铁律!文件可能不存在、没有权限、磁盘满了……不检查就使用空指针会直接崩溃。
❓ 常见问题
Q fopen 返回 NULL 但文件明明存在,怎么回事?
A 可能是路径问题。相对路径基于程序运行目录,不是源文件目录。在 IDE 中运行时,工作目录可能和预期不同。
Q 文本模式和二进制模式有什么区别?
A 在 Windows 上,文本模式读写时会把
\n 和 \r\n 互相转换,二进制模式不做任何转换。Linux 上两者没区别。Q fwrite 写入结构体时,不同编译器生成的文件兼容吗?
A 不一定。内存对齐方式、字节序、基本类型大小都可能不同。跨平台需要手动序列化。
Q feof 为什么不能用来判断循环结束?
A feof 只有在读取操作越过文件末尾后才变为真。如果用它做循环条件,最后一次循环会多处理一条无效数据。应该用 fread/fgets 的返回值来判断。
📖 小节
- 用
fopen打开文件,必须检查返回值是否为NULL - 文本模式用
fgets/fputs、fprintf/fscanf,二进制模式用fread/fwrite "w"模式会清空文件,"a"模式追加到末尾,不要搞混fseek/ftell/rewind控制文件位置指针,实现随机访问- 每个
fopen必须对应一个fclose,养成好习惯
📝 作业
- 编写程序,统计一个文本文件中的行数、单词数和字符数
- 实现一个简单的文件复制程序,用二进制模式读取源文件并写入目标文件
- 编写程序,将一个结构体数组写入二进制文件,再从文件中读取并打印出来