一、回顾与本篇目标
上一篇你学会了动态内存分配——用 malloc 在堆上申请内存,用 free 释放内存。你的程序现在可以在运行时根据需要动态地创建和销毁数据了。
但还有一个问题没有解决:程序退出后,所有数据都没了。变量存在内存里,内存是临时存储——关机、程序退出,内存中的数据就消失了。如果你写了一个学生管理系统,录入了 50 个学生的信息,程序一关,这些数据就全丢了。下次再打开,还得重新输入。
数据需要持久化——存到硬盘上。硬盘上的数据不会因为关机而消失。这就是本篇要学的文件操作:让程序能够读写硬盘上的文件。
如果你之前跟过《Python 零基础入门》的文件读写篇,或者《后端零基础入门》中 Node.js 的 fs 模块,这一篇的概念你会觉得很熟悉——打开文件、读写内容、关闭文件,核心流程完全一样,只是 C 语言的语法更底层、更需要你手动管理细节。
本篇的目标:
- 理解 C 语言中文件操作的基本流程
- 学会用
fopen打开文件和fclose关闭文件 - 掌握文本文件的读写:
fprintf、fscanf、fgets、fputs - 学会二进制文件的读写:
fread、fwrite - 能把结构体数据保存到文件并读取出来
二、文件操作的基本流程
无论什么编程语言,文件操作都遵循同一个流程:
打开文件 → 读写文件 → 关闭文件
C 语言通过一个叫 FILE 的结构体来操作文件。FILE 是 C 标准库中定义的类型,包含了文件的所有信息——当前读写位置、缓冲区、文件状态等。你不需要知道 FILE 内部长什么样,只需要通过文件指针 FILE* 来操作它。
#include <stdio.h>
int main() {
FILE *file; // 文件指针
// 1. 打开文件
file = fopen("data.txt", "w");
// 2. 检查是否打开成功
if (file == NULL) {
printf("无法打开文件!\n");
return 1;
}
// 3. 写入数据
fprintf(file, "你好,文件!\n");
// 4. 关闭文件
fclose(file);
printf("数据已写入文件。\n");
return 0;
}
逐行解释:
FILE *file:声明一个文件指针。它用来指向fopen返回的FILE对象。fopen("data.txt", "w"):打开(或创建)一个名为data.txt的文件。"w"表示“写入模式”。fopen返回一个FILE*,如果打开失败(比如没有权限、磁盘满了),返回NULL。if (file == NULL):必须检查 fopen 的返回值。不检查就直接使用file,如果打开失败会导致程序崩溃。fprintf(file, "你好,文件!\n"):向文件写入内容。fprintf的用法和printf几乎一样,只是多了一个FILE*参数指定写到哪个文件。fclose(file):关闭文件。文件用完后必须关闭。关闭会做两件事:把缓冲区中还没写入硬盘的数据真正写进去,释放系统资源。不关闭可能导致数据丢失或资源泄漏。
三、fopen 的打开模式
fopen 的第二个参数决定了你以什么方式打开文件:
| 模式 | 含义 | 文件不存在时 | 文件存在时 |
|---|---|---|---|
"r" |
只读 | 返回 NULL(报错) | 从文件开头读取 |
"w" |
写入(覆盖) | 创建新文件 | 清空原内容 |
"a" |
追加 | 创建新文件 | 在文件末尾追加 |
"r+" |
读写 | 返回 NULL(报错) | 从文件开头读写 |
"w+" |
读写(覆盖) | 创建新文件 | 清空原内容 |
"a+" |
读取+追加 | 创建新文件 | 读取从头开始,写入在末尾 |
"rb", "wb", "ab" |
二进制模式 | 和对应的文本模式相同,但以二进制方式操作 | |
最常用的三个模式:"r" 读、"w" 写、"a" 追加。"w" 模式需要特别小心——它会清空文件原有内容。如果只是想添加内容,用 "a"。
文本模式和二进制模式的区别:
- 文本模式(不带
b):会对换行符做转换。Windows 上\n会被转成\r\n写入,读取时再转回来。 - 二进制模式(带
b):不做任何转换,原样写入、原样读出。适合处理图片、音频、视频等非文本数据。
四、文本文件的读写
4.1 写入文本:fprintf
fprintf 的用法和 printf 完全一样,只是第一个参数是 FILE*:
FILE *file = fopen("data.txt", "w");
if (file == NULL) return 1;
int age = 28;
float score = 92.5;
char name[] = "张三";
// 像使用 printf 一样向文件写入
fprintf(file, "姓名:%s\n", name);
fprintf(file, "年龄:%d\n", age);
fprintf(file, "成绩:%.1f\n", score);
fclose(file);
生成的 data.txt 文件内容:
姓名:张三
年龄:28
成绩:92.5
4.2 读取文本:fscanf
fscanf 的用法和 scanf 完全一样,只是第一个参数是 FILE*:
FILE *file = fopen("data.txt", "r");
if (file == NULL) return 1;
char name[50];
int age;
float score;
// 从文件读取格式化的数据
fscanf(file, "姓名:%s\n", name);
fscanf(file, "年龄:%d\n", &age);
fscanf(file, "成绩:%f\n", &score);
printf("读取到:%s,%d 岁,%.1f 分\n", name, age, score);
fclose(file);
fscanf 的局限:它对格式要求很严格。如果文件内容的格式和你写的格式串不完全一致,读取就会失败。对于结构不固定的文本文件,通常用 fgets 更灵活。
4.3 逐行读取:fgets
fgets 从文件中读取一行(包括换行符),存入字符串:
FILE *file = fopen("data.txt", "r");
if (file == NULL) return 1;
char line[256];
// 逐行读取,直到文件结束
while (fgets(line, sizeof(line), file) != NULL) {
printf("读到一行:%s", line); // line 本身包含换行符,所以不加 \n
}
fclose(file);
fgets 的参数:
line:存储读取内容的字符数组。sizeof(line):最多读取的字符数(包括\0)。file:文件指针。- 返回值:成功返回
line,读到文件末尾或出错返回NULL。
4.4 写入字符串:fputs
FILE *file = fopen("output.txt", "w");
if (file == NULL) return 1;
fputs("第一行\n", file);
fputs("第二行\n", file);
fputs("第三行\n", file);
fclose(file);
fputs 写入一个字符串到文件,不会自动加换行符(需要手动加 \n)。
五、二进制文件的读写
文本文件适合人类直接阅读的数据。但对于结构化的数据(比如结构体数组),用二进制方式存储更高效——不需要在文本和二进制之间转换,直接按内存中的原始格式读写。
5.1 写入二进制:fwrite
#include <stdio.h>
typedef struct {
char name[50];
int age;
float score;
} Student;
int main() {
Student students[3] = {
{"张三", 20, 85.5},
{"李四", 21, 92.0},
{"王五", 19, 78.5}
};
FILE *file = fopen("students.bin", "wb");
if (file == NULL) return 1;
// fwrite(数据地址, 每个元素的大小, 元素个数, 文件指针)
size_t written = fwrite(students, sizeof(Student), 3, file);
printf("写入了 %zu 个学生记录\n", written);
fclose(file);
return 0;
}
fwrite 的参数:
- 第一个参数:要写入的数据的内存地址。
- 第二个参数:每个元素的大小(字节)。
- 第三个参数:要写入的元素个数。
- 第四个参数:文件指针。
- 返回值:实际成功写入的元素个数。
用 "wb" 模式打开——w 表示写入,b 表示二进制模式。
5.2 读取二进制:fread
#include <stdio.h>
typedef struct {
char name[50];
int age;
float score;
} Student;
int main() {
Student students[3];
FILE *file = fopen("students.bin", "rb");
if (file == NULL) return 1;
// fread(存储位置, 每个元素的大小, 要读取的元素个数, 文件指针)
size_t read_count = fread(students, sizeof(Student), 3, file);
printf("读入了 %zu 个学生记录\n", read_count);
fclose(file);
// 输出读取到的数据
for (size_t i = 0; i < read_count; i++) {
printf("%s,%d 岁,%.1f 分\n",
students[i].name, students[i].age, students[i].score);
}
return 0;
}
fread 的参数和 fwrite 完全对称,只是数据方向相反——fread 从文件读到内存,fwrite 从内存写到文件。
注意:用文本编辑器打开 students.bin 会看到乱码——因为它是二进制格式,不是给人读的。但程序可以精确地读写它。
六、其他文件操作函数
6.1 feof:判断是否读到文件末尾
在循环读取文件时,可以用 feof 判断是否读到了文件末尾:
FILE *file = fopen("data.txt", "r");
char ch;
while ((ch = fgetc(file)) != EOF) {
putchar(ch);
}
// 或者用 feof
// while (!feof(file)) { ... }
不过,更推荐的做法是检查读取函数的返回值(如 fgets 返回 NULL 表示结束),而不是依赖 feof。feof 只在尝试读取之后才会被设置,有时会导致多读一次。
6.2 fseek 和 ftell:在文件中定位
fseek 移动文件内部的“读写位置指针”,ftell 获取当前位置:
// 把位置移到文件末尾
fseek(file, 0, SEEK_END);
long file_size = ftell(file);
printf("文件大小:%ld 字节\n", file_size);
// 把位置移回文件开头
fseek(file, 0, SEEK_SET);
fseek 的第三个参数:
SEEK_SET:从文件开头开始计算偏移。SEEK_CUR:从当前位置开始计算偏移。SEEK_END:从文件末尾开始计算偏移。
6.3 remove:删除文件
if (remove("temp.txt") == 0) {
printf("文件删除成功\n");
} else {
printf("文件删除失败\n");
}
6.4 rename:重命名文件
if (rename("old_name.txt", "new_name.txt") == 0) {
printf("重命名成功\n");
}
七、和 Python/Node.js 文件操作的对比
如果你之前学过 Python 或 Node.js 的文件操作,这张表能帮你快速迁移:
| 操作 | Python | Node.js | C |
|---|---|---|---|
| 打开文件 | open("file.txt", "w") |
fs.openSync("file.txt", "w") |
fopen("file.txt", "w") |
| 写入文本 | file.write("hello") |
fs.writeFileSync(...) |
fprintf(file, "hello") |
| 读取一行 | file.readline() |
readline 模块 |
fgets(buffer, size, file) |
| 关闭文件 | file.close() 或 with |
fs.closeSync(fd) |
fclose(file) |
| 自动关闭 | with open(...) as f: |
回调/Promise 自动管理 | 没有!必须手动 fclose |
| 删除文件 | os.remove("file.txt") |
fs.unlinkSync(...) |
remove("file.txt") |
C 语言的文件操作最“原始”,没有 Python 的 with 语句(自动关闭文件),没有 Node.js 的异步回调。你必须自己管理文件指针,自己保证 fclose 被调用。
八、综合演示:学生信息的持久化存储
下面这个示例把结构体和文件操作结合起来,实现一个简单的学生信息管理系统,数据保存到硬盘上:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_STUDENTS 100
#define FILENAME "students.dat"
typedef struct {
char name[50];
int age;
float score;
} Student;
// 保存学生数据到文件
int save_students(const Student *students, int count) {
FILE *file = fopen(FILENAME, "wb");
if (file == NULL) {
printf("无法打开文件进行写入!\n");
return 0;
}
// 先写入学生数量
fwrite(&count, sizeof(int), 1, file);
// 再写入所有学生数据
fwrite(students, sizeof(Student), count, file);
fclose(file);
return 1;
}
// 从文件读取学生数据
int load_students(Student *students) {
FILE *file = fopen(FILENAME, "rb");
if (file == NULL) {
// 文件不存在,返回 0 表示没有数据
return 0;
}
int count;
// 先读取学生数量
if (fread(&count, sizeof(int), 1, file) != 1) {
fclose(file);
return 0;
}
if (count > MAX_STUDENTS) count = MAX_STUDENTS;
// 再读取所有学生数据
size_t read_count = fread(students, sizeof(Student), count, file);
fclose(file);
return (int)read_count;
}
int main() {
Student students[MAX_STUDENTS];
int count = 0;
// 尝试从文件加载已有数据
count = load_students(students);
if (count > 0) {
printf("从文件加载了 %d 个学生的数据\n\n", count);
for (int i = 0; i < count; i++) {
printf("%d. %s,%d 岁,%.1f 分\n",
i + 1, students[i].name, students[i].age, students[i].score);
}
} else {
printf("没有找到已有数据,开始录入新数据\n\n");
}
// 添加新学生
printf("\n===== 添加学生 =====\n");
printf("输入 'quit' 结束录入\n");
while (count < MAX_STUDENTS) {
char input[50];
printf("\n姓名:");
scanf("%s", input);
if (strcmp(input, "quit") == 0) break;
strcpy(students[count].name, input);
printf("年龄:");
scanf("%d", &students[count].age);
printf("成绩:");
scanf("%f", &students[count].score);
count++;
}
// 保存到文件
if (save_students(students, count)) {
printf("\n数据已保存到 %s(共 %d 条记录)\n", FILENAME, count);
} else {
printf("\n数据保存失败!\n");
}
return 0;
}
代码设计要点:
- 二进制存储:用
fwrite和fread直接存储结构体数组。这比用文本格式更简洁——不需要为每个字段写格式化代码。 - 先存数量:文件开头存储了学生数量,读取时先读数量,再根据数量读取对应条记录。这样不需要遍历文件或猜测有多少数据。
- 持久化:程序退出后,数据保存在
students.dat文件中。再次运行程序,load_students会自动加载已有数据,之前的录入不会丢失。 - 文件名用宏定义:
#define FILENAME "students.dat",修改文件名时只需改一处。
九、本篇动手练习
练习 1:文本文件复制
新建 practice10-1.c,实现文本文件复制功能。从 source.txt 逐字符读取,写入 destination.txt。提示:用 fgetc 和 fputc,或者用 fgets 和 fputs。
练习 2:统计文件行数
新建 practice10-2.c,读取一个文本文件,统计文件中有多少行、多少个字符(不包括换行符)。
练习 3:图书信息持久化
新建 practice10-3.c,定义 Book 结构体(书名、作者、价格、页数)。实现添加图书、显示所有图书、保存到二进制文件、从文件加载的功能。
练习 4:日志记录器
新建 practice10-4.c,写一个 log_message 函数,每次调用时把当前时间和一条消息追加到 app.log 文件中。提示:获取当前时间用 time() 和 ctime()(需要 time.h),打开模式用 "a"。
十、本篇小结
这一篇你学会了 C 语言中文件操作的核心知识:
- 文件操作流程:
fopen打开 → 读写 →fclose关闭。文件用完后必须关闭。 fopen的打开模式:"r"只读、"w"覆盖写入、"a"追加写入。"b"后缀表示二进制模式。- 文本读写:
fprintf/fscanf格式化读写,fgets/fputs逐行读写。 - 二进制读写:
fwrite/fread直接按内存格式读写,适合结构体等结构化数据。 - 文件定位:
fseek移动读写位置,ftell获取当前位置。 - 文件管理:
remove删除文件,rename重命名文件。
文件操作让你的程序有了“记忆力”——数据可以持久化到硬盘上,下次运行时还能读出来。这是从“玩具程序”到“实用工具”的关键一步。下一篇,我们学习 C 语言的预处理指令——#include、#define、条件编译等。这些以 # 开头的指令在编译之前执行,是 C 语言独特的“元编程”能力。
下一篇预告
下一篇——《预处理指令——编译之前的代码处理》:#include 的本质(复制粘贴)、#define 宏定义和宏函数、条件编译(#ifdef、#ifndef)、头文件保护(防止重复包含)。理解这些能让你看懂大型 C 项目的代码组织方式。
C/C++ 零基础入门,每周更新。











暂无评论内容