
个人主页 :Yue丶越
个人专栏 :C语言 、 Python基础


前言
在C语言编程中,我们常常会遇到这样的问题:程序运行时处理的数据,一旦因程序退出就会随着内存回收而丢失。我们想要让数据长期保存、重复使用,则必须掌握其文件的操作。所以本文将基于C语言文件操作的核心知识点,从基础概念到实战技巧,带你全面解锁于文件操作的奥秘。
一、为什么需要文件操作?
程序在运行时,所有数据都存储在内存中。内存的特性是"断电易失",比如我们编写一个计算程序,计算结果如果只存在变量里,程序关闭后就再也找不回了。
而文件存储在磁盘(硬盘)等外部存储设备中,数据则可长期保留。通过文件操作,我们能实现:
- 程序运行数据的持久化保存(如用户信息、配置参数)。
- 跨程序数据共享(如A程序写入的数据,B程序可读取)。
- 大规模数据处理(如读取超大日志文件、写入海量计算结果)。
简单来说,文件就是程序与外部存储设备之间的数据桥梁。
二、文件的核心概念
在C语言中,文件主要分为两类,且每个文件都有唯一标识。
2.1 文件的分类(按功能)
- 程序文件 :用于存放程序代码和编译结果,比如
.c源文件、.obj目标文件、.exe可执行文件。 - 数据文件 :用于存储程序运行时读写的数据(本文重点讨论),比如存储用户信息的
user.txt、存储二进制数据的data.bin。
2.2 文件名的组成
一个完整的文件名包含3部分,三者缺一不可:
文件路径 + 文件名主干 + 文件后缀
示例:c:\code\test.txt
- 路径:
c:\code\(指定文件所在位置)。 - 主干:
test(文件的核心标识)。 - 后缀:
.txt(标识文件类型,文本文件)。
2.3 文本文件 vs 二进制文件(按数据组织形式)
数据在内存中始终以二进制形式存储,但写入文件时,可选择两种存储方式:
| 存储类型 | 存储形式 | 特点 | 示例(存储整数10000) |
|---|---|---|---|
| 文本文件 | ASCII码形式 | 人类可读,占用空间较大 | 占用5个字节('1'/'0'/'0'/'0'/'0') |
| 二进制文件 | 原生二进制形式 | 人类不可读,占用空间小 | 占用4个字节(VS2019环境) |
代码验证二进制存储:
c
#include <stdio.h>
int main() {
int a = 10000;
// 以二进制只写模式打开文件
FILE* pf = fopen("test.bin", "wb");
if (pf != NULL) {
// 写入4字节二进制数据
fwrite(&a, 4, 1, pf);
fclose(pf);
pf = NULL;
}
return 0;
}
运行后用二进制编辑器打开test.bin,可看到存储内容为10 27 00 00(十六进制),对应10000的二进制值。
三、文件操作的核心
C语言通过"流"和"文件指针"简化对不同设备的操作。
1. 流(Stream)
不同外部设备(键盘、显示器、磁盘)的输入输出逻辑不同,C语言抽象出"流"的概念(可理解为"流淌着数据的河")。
程序所有的输入输出操作,本质都是对"流"的操作:
- 读数据:从流中"捞取"数据到程序。
- 写数据:将程序数据"注入"流中,再由流传递到设备。
2. 标准流(默认打开)
C程序启动时,会自动打开3个标准流,无需手动操作:
stdin:标准输入流(默认对应键盘),scanf就是从这里读数据。stdout:标准输出流(默认对应显示器),printf就是向这里写数据。stderr:标准错误流(默认对应显示器),用于输出程序错误信息。
这3个流的类型都是FILE*,即文件指针。
3. 文件指针(FILE*)
操作文件时,系统会在内存中创建一个"文件信息区"(结构体FILE实例),存储文件名、状态、当前读写位置等信息。
我们通过FILE*类型的指针来操作这个信息区,进而间接操作文件:
c
// 定义文件指针
FILE* pf;
// 打开文件后,pf指向该文件的信息区
pf = fopen("test.txt", "r");
可以理解为:文件指针是程序与文件之间的"遥控器"。
四、文件的操作(打开、读写、关闭)
文件操作的基本流程是:打开文件 → 读写操作 → 关闭文件,任何一步都不能马虎。
4.1 打开文件:fopen函数
c
FILE *fopen(const char *filename, const char *mode);
filename:文件路径+文件名(如"test.txt"或"c:\code\data.bin")。mode:打开模式(核心参数,决定读写权限和文件类型)。- 返回值:成功则返回文件指针,失败则返回
NULL(必须检查!)。
常用文件打开模式
| 模式 | 含义 | 文件不存在时 | 适用场景 |
|---|---|---|---|
| "r" | 文本文件只读 | 报错 | 读取已存在的文本文件 |
| "w" | 文本文件只写 | 创建新文件 | 新建或覆盖文本文件 |
| "a" | 文本文件追加 | 创建新文件 | 向文本文件末尾添加数据 |
| "rb" | 二进制文件只读 | 报错 | 读取已存在的二进制文件 |
| "wb" | 二进制文件只写 | 创建新文件 | 新建或覆盖二进制文件 |
| "r+" | 文本文件读写 | 报错 | 读写已存在的文本文件 |
| "w+" | 文本文件读写 | 创建新文件 | 新建可读写的文本文件 |
打开文件的正确形式(必须检查返回值)
c
FILE* pf = fopen("test.txt", "w");
// 检查文件是否打开成功
if (pf == NULL) {
// 打印错误信息
perror("fopen failed");
// 退出程序
return 1;
}
4.2 关闭文件:fclose函数
c
int fclose(FILE *stream);
- 功能:关闭文件,释放文件信息区和相关资源。
- 注意:关闭后必须将文件指针置为
NULL,避免野指针。 - 示例:
c
// 关闭文件
fclose(pf);
// 置空指针
pf = NULL;
4.3 顺序读写(按"从头到尾"的顺序操作)
顺序读写是最常用的文件操作方式,C语言提供了8个核心函数,覆盖字符、行、格式化、二进制四种场景:
| 函数名 | 功能 | 适用场景 | 示例 |
|---|---|---|---|
| fgetc | 读取单个字符 | 所有输入流 | char c = fgetc(pf); |
| fputc | 写入单个字符 | 所有输出流 | fputc('A', pf); |
| fgets | 读取一行文本 | 所有输入流 | char buf[1024]; fgets(buf, 1024, pf); |
| fputs | 写入一行文本 | 所有输出流 | fputs("hello", pf); |
| fscanf | 格式化读取 | 所有输入流 | fscanf(pf, "%d %s", &num, str); |
| fprintf | 格式化写入 | 所有输出流 | fprintf(pf, "name:%s age:%d", name, age); |
| fread | 二进制读取 | 文件 | fread(buf, sizeof(int), 5, pf); |
| fwrite | 二进制写入 | 文件 | fwrite(arr, sizeof(int), 5, pf); |
关键区别:fscanf/printf与scanf/printf
scanf:仅从标准输入流(键盘)读取。fscanf:可从任意输入流(文件/键盘)读取。printf:仅向标准输出流(显示器)写入。fprintf:可向任意输出流(文件/显示器)写入。
示例:用fprintf/fscanf读写文本文件
c
#include <stdio.h>
int main() {
FILE* pf = fopen("user.txt", "w+");
if (pf == NULL) {
perror("fopen failed");
return 1;
}
// 写入数据
fprintf(pf, "name:zhangsan age:20\n");
fprintf(pf, "name:lisi age:22\n");
// 回到文件起始位置(否则读不到数据)
rewind(pf);
// 读取数据
char name[20];
int age;
while (fscanf(pf, "name:%s age:%d", name, &age) != EOF) {
printf("read: name=%s, age=%d\n", name, age);
}
fclose(pf);
pf = NULL;
return 0;
}
4.4 随机读写:随心所欲定位读写位置
有时我们需要跳过部分内容直接读写(比如修改文件中间的某段数据),这就需要随机读写函数:
(1)fseek:定位文件指针
c
int fseek(FILE *stream, long int offset, int origin);
offset:偏移量(正数向后移,负数向前移)。origin:起始位置(3个可选值)。SEEK_SET:文件起始位置(常用)。SEEK_CUR:文件当前位置。SEEK_END:文件末尾位置。
示例:修改文件中间内容
c
FILE* pf = fopen("example.txt", "wb");
fputs("This is an apple.", pf);
// 从文件开头偏移9个字节(指向"apple"的'a')
fseek(pf, 9, SEEK_SET);
// 覆盖写入,结果为"This is a sam."
fputs(" sam", pf);
fclose(pf);
(2)ftell:获取指针偏移量
c
long int ftell(FILE *stream);
功能:返回文件指针相对于起始位置的偏移量(可用于获取文件大小):
c
// 定位到文件末尾
fseek(pf, 0, SEEK_END);
// 获取文件大小(字节数)
long size = ftell(pf);
(3)rewind:重置指针到起始位置
c
void rewind(FILE *stream);
功能:快速将文件指针回到文件开头,等价于fseek(pf, 0, SEEK_SET)。
五、文件读取结束的判定
很多初学者会误用feof函数判断文件是否结束,但这是一个高频坑点!
正确认知feof
feof的作用不是"判断文件是否结束",而是"判断文件结束的原因是否是遇到文件尾"。
正确的判定方式
-
文本文件:判断读写函数的返回值:
fgetc:返回EOF(-1)表示结束。fgets:返回NULL表示结束。fscanf:返回值小于格式说明符个数表示结束。
-
二进制文件 :判断
fread的返回值是否小于要读取的元素个数:
c
// 二进制文件读取示例
size_t ret = fread(buf, sizeof(double), 5, pf);
if (ret < 5) {
// 读取结束或出错
if (feof(pf)) {
printf("文件读取完毕\n");
} else if (ferror(pf)) {
printf("读取错误\n");
}
}
错误示例
c
// 错误:feof在读取到文件尾后才返回真,会多循环一次
while (!feof(pf)) {
fgetc(pf);
}
六、底层原理:文件缓冲区
C语言采用"缓冲文件系统",系统会为每个打开的文件分配一块内存缓冲区,这会影响数据写入的时机:
- 写数据:先写入缓冲区,缓冲区满了才会批量写入磁盘。
- 读数据:先从磁盘读取数据填满缓冲区,再从缓冲区供程序读取。
刷新缓冲区的3种方式
- 调用
fflush(pf)(注意:高版本VS已不支持)。 - 关闭文件(
fclose会自动刷新缓冲区)。 - 缓冲区满(系统自动刷新)。
示例:缓冲区的影响
c
FILE* pf = fopen("test.txt", "w");
// 数据先写入缓冲区,文件中暂时没有内容
fputs("abcdef", pf);
// 睡眠10秒,此时打开文件看不到内容
Sleep(10000);
// 刷新缓冲区,数据写入磁盘
fflush(pf);
// 睡眠10秒,此时打开文件能看到内容
Sleep(10000);
fclose(pf);
关键结论
操作文件后,必须关闭文件 或手动刷新缓冲区,否则数据可能留在缓冲区,导致文件内容不完整!
文件操作是C语言的核心技能之一,无论是日常开发还是面试,都是高频考点。建议结合本文的示例代码,亲手编写几个实战案例(比如读写用户信息、存储数组数据、修改文件内容),则能彻底掌握!
至此,我们已梳理完"文件操作"的全部内容了。最后我们在文末来进行一个投个票,告诉我你对哪部分内容最感兴趣、收获最大,也欢迎在评论区聊聊你的学习感受,如果在实践中遇到具体问题,也欢迎在评论区留言讨论,祝大家编程顺利!
以上就是本期博客的全部内容了,感谢各位的阅读以及关注。如有内容存在疏漏或不足之处,恳请各位技术大佬不吝赐教、多多指正,咱们下篇内容见。
以上。
祝你早安午安晚安。
盈盈赴暖,事事舒然。

