前面我们学习了:
数组
函数
指针
结构体
动态内存
这些数据默认都存在于:
内存中。
例如:
int a = 10;程序运行时:
a
↓
内存
但是程序结束以后:
程序结束
↓
内存释放
↓
a 消失
如果希望数据能够在程序关闭之后仍然存在,就需要把数据保存到:
文件。
例如:
程序运行
↓
读取文件
↓
修改数据
↓
保存文件
↓
程序结束
↓
数据仍然存在
一、什么是文件
文件就是保存在存储设备上的数据。
例如:
student.txt
data.txt
config.txt
save.dat
程序可以:
创建文件
读取文件
写入文件
修改文件
追加内容
关闭文件
C 语言主要通过:
FILE *来操作文件。
需要:
#include <stdio.h>二、FILE 是什么
C 语言使用:
FILE来表示一个文件流。
例如:
FILE *fp;这里:
fp
↓
指向一个 FILE
↓
代表打开的文件
可以简单理解成:
磁盘上的文件
↓
fopen()
↓
FILE *
↓
fp
↓
程序操作文件
三、打开文件:fopen
基本格式:
fopen("文件名", "模式");例如:
FILE *fp = fopen("test.txt", "w");这里:
test.txt
是文件名。
w
是打开模式。
四、fopen 的返回值
如果打开成功:
fopen(...)会返回一个:
FILE *如果失败:
fopen(...)会返回:
NULL所以应该检查:
FILE *fp = fopen("test.txt", "w");
if (fp == NULL) {
printf("文件打开失败\n");
return 1;
}这个思想和前面:
malloc()返回 NULL 时检查,是一样的。
五、关闭文件:fclose
打开文件以后,用完需要:
fclose(fp);例如:
FILE *fp = fopen("test.txt", "w");
if (fp == NULL) {
return 1;
}
fclose(fp);文件操作的基本流程:
fopen
↓
使用文件
↓
fclose
可以记成:
打开
↓
读/写
↓
关闭
六、文件打开模式
fopen() 第二个参数非常重要。
常见模式:
r
w
a
r+
w+
a+
还有二进制模式:
rb
wb
ab
rb+
wb+
ab+
先重点学习:
r
w
a
七、r:读取文件
FILE *fp = fopen("test.txt", "r");r:
read
表示读取。
如果文件不存在:
打开失败
例如:
FILE *fp = fopen("test.txt", "r");
if (fp == NULL) {
printf("文件不存在\n");
return 1;
}八、w:写入文件
FILE *fp = fopen("test.txt", "w");w:
write
表示写入。
如果文件不存在:
创建文件
如果文件已经存在:
原来的内容会被清空
这一点非常重要。
例如原来的:
test.txt
hello
world
执行:
fopen("test.txt", "w");之后再写入:
abc
文件会变成:
abc
而不是:
hello
world
abc
九、a:追加
FILE *fp = fopen("test.txt", "a");a:
append
表示追加。
如果文件不存在:
创建文件
如果文件已经存在:
保留原来的内容
新内容会写到文件末尾。
例如原本:
hello
world
执行:
fprintf(fp, "abc\n");变成:
hello
world
abc
十、fputc:写入一个字符
可以使用:
fputc()格式:
fputc(字符, 文件指针);例如:
FILE *fp = fopen("test.txt", "w");
if (fp == NULL) {
return 1;
}
fputc('A', fp);
fputc('B', fp);
fputc('C', fp);
fclose(fp);文件内容:
ABC
十一、fgetc:读取一个字符
对应:
fgetc()例如:
FILE *fp = fopen("test.txt", "r");
if (fp == NULL) {
return 1;
}
char c = fgetc(fp);
printf("%c\n", c);
fclose(fp);如果文件内容是:
Hello
那么第一次 fgetc() 会读取:
H
再次调用:
c = fgetc(fp);会读取:
e
因为文件有一个:
当前位置。
十二、循环读取整个文件
可以使用:
while ((c = fgetc(fp)) != EOF)例如:
#include <stdio.h>
int main() {
FILE *fp = fopen("test.txt", "r");
if (fp == NULL) {
return 1;
}
int c;
while ((c = fgetc(fp)) != EOF) {
putchar(c);
}
fclose(fp);
return 0;
}这里:
fgetc()
↓
读取一个字符
↓
文件结束?
↓
不是 → 继续
↓
是 → EOF
十三、为什么 c 要用 int
注意这里:
int c;而不是:
char c;因为:
fgetc()需要能够返回:
普通字符
以及:
EOF
所以通常使用:
int保存 fgetc() 的返回值。
这是文件操作中一个很重要的细节。
十四、fputs:写入字符串
可以使用:
fputs()例如:
FILE *fp = fopen("test.txt", "w");
if (fp == NULL) {
return 1;
}
fputs("Hello World\n", fp);
fputs("C Language\n", fp);
fclose(fp);文件:
Hello World
C Language
十五、fgets:读取一行
与 fputs() 对应:
fgets()例如:
char buffer[100];
fgets(buffer, 100, fp);意思是:
最多读取 99 个字符
并预留:
\0
结束字符串。
十六、使用 fgets 读取整个文件
#include <stdio.h>
int main() {
FILE *fp = fopen("test.txt", "r");
if (fp == NULL) {
return 1;
}
char buffer[100];
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("%s", buffer);
}
fclose(fp);
return 0;
}这样会:
一次读取一行
↓
继续读取
↓
直到文件结束
对于普通文本文件,这是非常常见的写法。
十七、fprintf:格式化写入
如果需要像:
printf()一样写格式化数据,可以使用:
fprintf()格式:
fprintf(文件指针, "格式", 数据);例如:
FILE *fp = fopen("test.txt", "w");
if (fp == NULL) {
return 1;
}
int age = 18;
double score = 95.5;
fprintf(fp, "age = %d\n", age);
fprintf(fp, "score = %.2f\n", score);
fclose(fp);文件:
age = 18
score = 95.50
十八、printf 和 fprintf
你之前:
printf("age = %d\n", age);是在:
控制台
输出。
而:
fprintf(fp, "age = %d\n", age);是在:
文件
输出。
可以简单理解:
printf
↓
标准输出
fprintf
↓
指定 FILE 输出
十九、fscanf:从文件读取格式化数据
与:
scanf()类似。
例如文件:
18 95.5
可以:
int age;
double score;
fscanf(fp, "%d %lf", &age, &score);这样:
文件
↓
fscanf
↓
变量
二十、完整的文本读写
写入:
#include <stdio.h>
int main() {
FILE *fp = fopen("student.txt", "w");
if (fp == NULL) {
return 1;
}
fprintf(fp, "Alice 18 95.5\n");
fprintf(fp, "Bob 19 88.0\n");
fclose(fp);
return 0;
}文件变成:
Alice 18 95.5
Bob 19 88.0
然后再读取:
#include <stdio.h>
int main() {
FILE *fp = fopen("student.txt", "r");
if (fp == NULL) {
return 1;
}
char name[50];
int age;
double score;
while (fscanf(fp, "%49s %d %lf",
name, &age, &score) == 3) {
printf(
"%s %d %.2f\n",
name,
age,
score
);
}
fclose(fp);
return 0;
}二十一、文件指针的位置
文件内部可以理解为:
H e l l o \n W o r l d
↑
当前位置
每读取一次:
fgetc(fp);位置就会向后移动。
例如:
第一次
↓
H e l l o
↑
第二次
↓
H e l l o
↑
这就是所谓:
文件位置指示器。
二十二、rewind
可以把文件位置重新放回开头:
rewind(fp);例如:
fgetc(fp);
fgetc(fp);
rewind(fp);
fgetc(fp);前两次读取以后位置已经向后移动。
rewind() 后:
回到文件开头
二十三、ftell
可以查看当前文件位置:
ftell(fp);例如:
long pos = ftell(fp);
printf("%ld\n", pos);返回当前文件位置。
例如:
刚打开
↓
0
读取一个字符
↓
1
再读取一个字符
↓
2
二十四、fseek
可以直接移动文件位置:
fseek()基本形式:
fseek(fp, offset, origin);常见:
SEEK_SET
SEEK_CUR
SEEK_END例如:
fseek(fp, 0, SEEK_SET);表示:
移动到文件开头
二十五、文件结尾
常见判断方式:
while ((c = fgetc(fp)) != EOF)这里:
EOF
表示:
End Of File
也就是文件结束。
注意:
EOF 不是一个真正写进文件里的普通字符。
它是读取函数用来表示:
已经没有数据可读了。
二十六、二进制文件
到目前为止,我们主要操作:
文本文件
例如:
student.txt
但是程序还可以保存:
二进制文件
例如:
student.dat
save.dat
data.bin
使用:
rb
wb
ab
等模式。
二十七、fwrite
二进制写入主要使用:
fwrite()格式:
fwrite(数据地址, 每个元素大小, 元素数量, 文件指针);例如:
int a = 100;
fwrite(&a, sizeof(int), 1, fp);表示:
把 a 的内存数据
↓
写入文件
二十八、fread
读取对应:
fread()例如:
int a;
fread(&a, sizeof(int), 1, fp);表示:
文件中的二进制数据
↓
读取到 a
二十九、fwrite + fread
完整例子:
#include <stdio.h>
int main() {
FILE *fp = fopen("data.bin", "wb");
if (fp == NULL) {
return 1;
}
int a = 100;
double b = 3.14;
fwrite(&a, sizeof(int), 1, fp);
fwrite(&b, sizeof(double), 1, fp);
fclose(fp);
return 0;
}然后读取:
#include <stdio.h>
int main() {
FILE *fp = fopen("data.bin", "rb");
if (fp == NULL) {
return 1;
}
int a;
double b;
fread(&a, sizeof(int), 1, fp);
fread(&b, sizeof(double), 1, fp);
printf("%d\n", a);
printf("%.2f\n", b);
fclose(fp);
return 0;
}三十、结构体写入文件
现在把上一章的:
结构体
和本章:
文件
结合起来。
例如:
typedef struct {
char name[50];
int age;
double score;
} Student;创建:
Student s = {"Alice", 18, 95.5};直接写入:
fwrite(&s, sizeof(Student), 1, fp);三十一、结构体读取文件
读取:
Student s;
fread(&s, sizeof(Student), 1, fp);完整程序:
#include <stdio.h>
typedef struct {
char name[50];
int age;
double score;
} Student;
int main() {
Student s = {"Alice", 18, 95.5};
FILE *fp = fopen("student.dat", "wb");
if (fp == NULL) {
return 1;
}
fwrite(&s, sizeof(Student), 1, fp);
fclose(fp);
return 0;
}读取:
#include <stdio.h>
typedef struct {
char name[50];
int age;
double score;
} Student;
int main() {
Student s;
FILE *fp = fopen("student.dat", "rb");
if (fp == NULL) {
return 1;
}
fread(&s, sizeof(Student), 1, fp);
printf("姓名: %s\n", s.name);
printf("年龄: %d\n", s.age);
printf("成绩: %.2f\n", s.score);
fclose(fp);
return 0;
}三十二、结构体数组写入文件
可以一次写多个:
Student students[3] = {
{"Alice", 18, 95.5},
{"Bob", 19, 88.0},
{"Charlie", 18, 92.5}
};然后:
fwrite(
students,
sizeof(Student),
3,
fp
);也就是:
students
↓
Student
Student
Student
↓
一次写入 3 个
三十三、结构体数组读取
例如:
Student students[3];
fread(
students,
sizeof(Student),
3,
fp
);然后:
for (int i = 0; i < 3; i++) {
printf(
"%s %d %.2f\n",
students[i].name,
students[i].age,
students[i].score
);
}三十四、动态内存 + 文件 + 结构体
现在把前三章全部连接起来。
结构体
+
动态内存
+
文件
例如:
Student *students =
malloc(sizeof(Student) * n);用户输入 n。
然后:
malloc
↓
创建 n 个 Student
↓
输入数据
↓
fwrite
↓
保存文件
↓
free
读取时:
fopen
↓
确定数据数量
↓
malloc
↓
fread
↓
使用
↓
free
↓
fclose
这已经开始接近真正的软件数据存储了。
三十五、文本文件和二进制文件
两种方式各有特点。
文本文件
例如:
student.txt
内容:
Alice 18 95.5
Bob 19 88.0
特点:
人可以直接打开查看
方便调试
跨语言处理比较方便
常用:
fprintf
fscanf
fgets
fputs二进制文件
例如:
student.dat
内容是二进制数据。
特点:
人不容易直接阅读
适合程序直接读写
可以直接保存结构体的字节数据
常用:
fread
fwrite三十六、文件操作安全问题
1. fopen 失败
一定要检查:
FILE *fp = fopen("test.txt", "r");
if (fp == NULL) {
return 1;
}2. 文件没关闭
使用完:
fclose(fp);3. 覆盖原文件
注意:
fopen("test.txt", "w");会清空原文件。
如果只想在后面添加内容:
fopen("test.txt", "a");4. scanf / fscanf 字符串长度
例如:
char name[50];
fscanf(fp, "%49s", name);预留一个位置给:
\0
这样可以避免简单的缓冲区越界问题。
三十七、文件和你的课程表项目
你之前做过课程表程序。
之前的数据可能放在:
char classname[7][6][291];程序关闭之后:
数据消失
现在可以:
课程表
↓
结构体
↓
动态内存
↓
文件
↓
永久保存
例如设计:
typedef struct {
char name[100];
char teacher[100];
char room[100];
} Course;然后:
Course courses[7][6];可以保存到文件。
甚至以后:
C 程序
↓
生成 JSON
↓
网页 / 手机 App
↓
读取 JSON
这样就和你之前做的课程表项目真正连接起来了。
三十八、文件操作的完整流程
最核心:
文件
│
┌──────┴──────┐
↓ ↓
读取 写入
│ │
fopen fopen
│ │
fread fwrite
│ │
└──────┬──────┘
↓
fclose
文本文件:
fopen
↓
fgets / fscanf
↓
处理数据
↓
fprintf / fputs
↓
fclose
二进制文件:
fopen
↓
fread / fwrite
↓
处理数据
↓
fclose
三十九、本章最重要的函数
建议先记住这几个:
fopen
fclose
fprintf
fscanf
fgets
fputs
fread
fwrite
然后再学习:
fgetc
fputc
fseek
ftell
rewind
四十、知识联系
到这里,你已经可以把前面的知识串起来:
数组
↓
可以保存多个相同类型的数据
结构体
↓
把不同类型的数据组合起来
指针
↓
保存地址
动态内存
↓
运行时申请空间
文件
↓
把数据保存到磁盘
最终形成:
结构体
+
指针
+
malloc/free
+
fread/fwrite
↓
可以保存复杂的数据
四十一、练习
练习 1
创建:
test.txt
写入:
Hello C
要求使用:
fopen
fputs
fclose练习 2
读取:
test.txt
逐行输出。
要求使用:
fgets练习 3
创建一个学生:
Student s;把:
姓名
年龄
成绩
使用 fprintf() 保存到文本文件。
练习 4
从文本文件中读取多个学生。
使用:
fscanf然后放入:
Student students[100];练习 5
使用:
fwrite把一个 Student 保存到:
student.dat
再使用:
fread读取回来。
练习 6
创建 5 个学生:
Student students[5];一次性写入:
fwrite(
students,
sizeof(Student),
5,
fp
);然后再一次性读取。
练习 7
综合练习:
让用户输入:
学生数量
然后:
输入 n
↓
malloc
↓
创建 n 个 Student
↓
输入学生信息
↓
保存到文件
↓
free
重新启动程序:
读取文件
↓
malloc
↓
读取所有学生
↓
输出
↓
free
Comments NOTHING