在编程时,你是否遇到过这样的困扰:程序运行时计算出的数据,关掉程序就不见了?下次想继续用这些数据,只能重新运行程序重新计算?其实解决这个问题很简单,核心就是学会 "文件操作"------ 把数据存到硬盘上,需要时再读出来。
一、先搞懂:为什么需要文件?
我们写程序时,数据默认存在内存 里。但内存有个特点:程序退出后,内存就会被系统回收,里面的数据也会跟着消失(比如你用计算器算完结果,关掉计算器就找不到之前的数字了)。
而文件 是存在硬盘上的,属于 "持久化存储"------ 哪怕电脑关机,数据也不会丢。所以文件操作的核心目的就是:让程序的数据能长期保存,下次运行时直接使用。
二、什么是文件?C 语言里的文件分两种
平时我们说的 "文件",比如电脑里的文档、图片,都是硬盘上的文件。但在 C 语言中,从功能角度主要分为两类:
- 程序文件 :就是我们写的代码相关文件,比如后缀为
.c的源文件、.obj的目标文件、.exe的可执行文件(这些是程序运行的 "基础",不是我们要操作的数据); - 数据文件 :专门用来存储程序运行时需要读写的数据的文件(比如程序计算的结果、需要读取的配置信息),这也是我们今天重点讨论的对象。
文件名的小知识
一个文件要有唯一的 "身份证"------ 文件名,它由 3 部分组成:文件路径 + 文件名主干 + 文件后缀 。比如c:\code\test.txt:
- 路径:
c:\code(文件存在哪个文件夹里); - 主干:
test(文件的名字); - 后缀:
.txt(文件类型,文本文件)。
三、文本文件 vs 二进制文件:数据怎么存?
根据数据的存储形式,数据文件又分为两种,核心区别在于 "是否转换数据格式":
- 二进制文件 :数据在内存中是二进制形式存储的,直接输出到硬盘,不做任何转换。优点是存储高效(占空间小),缺点是肉眼看不懂(打开是乱码);

(obj文件是二进制方式存储,用文本编辑器是乱码)
- 文本文件 :数据存储前会转换成 ASCII 码形式。优点是肉眼能直接看懂(比如打开是 "1234"),缺点是占空间更大。

(.c文件是文本文件)
举个直观的例子:存储整数10000
- 文本文件:每个数字是一个 ASCII 字符,
1、0、0、0、0共 5 个字符,占 5 个字节; - 二进制文件:直接存
10000的二进制值,只占 4 个字节(效率更高)。
四、关键概念:流和文件指针
在学文件操作前,先记住两个核心概念,否则后面的函数会看不懂:
1. 什么是 "流"?
程序要向硬盘写数据、从硬盘读数据,中间需要一个 "桥梁"------ 因为不同设备(键盘、显示器、硬盘)的输入输出规则不一样,为了方便程序员操作,C 语言抽象出了 "流" 的概念。
你可以把 "流" 想象成一条 "字符河":程序写数据就是往河里 "放水",读数据就是从河里 "取水"。不管是操作键盘、显示器还是文件,都通过 "流" 来统一处理,不用关心底层设备的差异。
2. 标准流:程序启动时默认打开的 3 个 "河"
为什么我们用scanf能直接从键盘输入,用printf能直接在屏幕输出?因为 C 语言程序启动时,会默认打开 3 个标准流:
stdin:标准输入流(对应键盘),scanf就是从这里读数据;stdout:标准输出流(对应显示器),printf就是往这里写数据;stderr:标准错误流(也对应显示器),用来输出程序的错误信息。
这 3 个流默认打开,所以我们不用手动操作就能用scanf和printf。
3. 文件指针:操作文件的 "工具手"
要操作文件,我们需要一个 "工具" 来管理文件的信息(比如文件名、文件当前的读写位置、文件状态)------ 这就是**FILE*类型的文件指针**。
可以这么理解:每个打开的文件,系统都会在内存中创建一个 "文件信息区 "(类似文件的 "说明书"),而文件指针就是指向这个 "说明书" 的指针。通过这个指针,我们就能间接操作对应的文件。
定义文件指针很简单:FILE* pf;(pf 就是一个文件指针变量)。
五、核心操作:文件的打开和关闭
就像我们用记事本一样:用之前要先打开,用完要关闭。C 语言操作文件也遵循这个逻辑,而且必须养成 "打开就关闭" 的习惯(否则可能导致数据丢失、文件损坏)。
1. 打开文件:用 fopen 函数
函数原型:FILE * fopen (const char * filename, const char * mode);
- 第一个参数:文件名(比如
"test.txt",如果文件不在当前文件夹,要写全路径,比如"c:\code\test.txt"); - 第二个参数:打开模式(关键!决定了文件是只读、只写还是读写,是文本文件还是二进制文件);
- 返回值:打开成功返回文件指针(指向文件信息区),打开失败返回
NULL(比如文件不存在却要 "只读" 打开)。
例子(要打开的的文件不存在):
cpp
#include<stdio.h>
int main()
{
FILE *fp=fopen("test.txt","r");
if (fp == NULL)
{
perror("Error opening file");
return 1;
}
return 0;
}

例子(以只读方式创建打开一个文件):
cpp
#include<stdio.h>
int main()
{
FILE *fp=fopen("test.txt","w");
if (fp == NULL)
{
perror("Error opening file");
return 1;
}
fclose(fp);
fp = NULL;
return 0;
}
程序运行后,打开创建的文件,向其中随便输入文字,在运行这个程序会把里面的内容清空。



再次运行后:

2. 关闭文件:用 fclose 函数
函数原型:int fclose (FILE * stream);
- 参数:要关闭的文件指针(比如之前定义的
pf); - 注意:关闭后要把文件指针设为
NULL(比如pf = NULL;),避免变成 "野指针"(指向无效地址,导致程序出错)。
常用的文件打开模式(重点记这几个)
| 打开模式 | 含义 | 文件不存在时 |
|---|---|---|
| "r" | 只读(文本文件) | 出错(打开失败) |
| "w" | 只写(文本文件) | 新建一个文件 |
| "a" | 追加(文本文件,往文件末尾写) | 新建一个文件 |
| "rb" | 只读(二进制文件) | 出错 |
| "wb" | 只写(二进制文件) | 新建一个文件 |
| "r+" | 读写(文本文件) | 出错 |
| "w+" | 读写(文本文件) | 新建一个文件 |
六、文件的读写操作:顺序读写和随机读写
打开文件后,核心就是 "读"(从文件拿数据到程序)和 "写"(把程序数据存到文件)。C 语言提供了一套专门的函数,分为 "顺序读写" 和 "随机读写" 两类。
1. 顺序读写:按文件内容的顺序 "从头读到尾"
顺序读写就是从文件开头开始,读 / 写完一个数据后,指针自动移到下一个位置,不能跳着来。常用函数如下(记熟这些就够日常用了):

写一个字符:
cpp
#include<stdio.h>
int main()
{
FILE *fp=fopen("test.txt","w");
if (fp == NULL)
{
perror("Error opening file");
return 1;
}
//写文件
fputc('a', fp);//(往 pFile 指向的文件写字符 'a');
fputc('b', fp);
fputc('c', fp);
fclose(fp);
fp = NULL;
return 0;
}
运行后查看文件:

循环写26个字符:


读一个字符:
cpp
#include<stdio.h>
int main()
{
FILE* p = fopen("test.txt", "r");
if (p == NULL)
{
perror("Error opening file");
return 1;
}
//// 依次读取前三个字符并打印
//char a = fgetc(p);
//printf("%c", a);
//char b = fgetc(p);
//printf("%c", b);
//char c = fgetc(p);
//printf("%c", c);
int ch = 0;
while ((ch = fgetc(p)) != EOF)
{
printf("%c ",ch);
}
fclose(p);
p = NULL;
return 0;
}

例子2:(fscanf,fprintf的使用)
cpp
#include<stdio.h>
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = {"djsj", 33, 99.9};
// "w+"模式:可读写的文本模式,文件不存在则创建,存在则清空
FILE *pf = fopen("test.txt", "w+");
if (pf == NULL)
{
perror("Error opening file");
return 1;
}
// 写文件:格式化写入结构体数据
int write_ret = fprintf(pf, "%s %d %.1f", s.name, s.age, s.score);
if (write_ret < 0) { // fprintf返回写入的字符数,失败返回负数
perror("Error writing to file");
fclose(pf);
return 1;
}
// 关键:将文件指针重置到文件开头,否则读不到内容
fseek(pf, 0, SEEK_SET);
// 读文件:格式化读取到新的结构体变量(避免覆盖原数据更易验证)
struct S s_read = {0}; // 初始化读取用的结构体
int read_ret = fscanf(pf, "%s %d %f", s_read.name, &s_read.age, &s_read.score);
if (read_ret != 3) { // fscanf返回成功读取的变量个数,这里应读取3个
perror("Error reading from file");
fclose(pf);
return 1;
}
// 打印读取的内容(方式1)
printf("方式1:%s %d %.1f\n", s_read.name, s_read.age, s_read.score);
// 打印读取的内容(方式2:stdout就是屏幕,效果和printf一致)
fprintf(stdout, "方式2:%s %d %.1f\n", s_read.name, s_read.age, s_read.score);
// 关闭文件,避免资源泄漏
fclose(pf);
pf = NULL;
return 0;
}
2. 随机读写:想读哪里就读哪里
有时候我们不想按顺序来,比如想直接修改文件中间的内容,这就需要 "随机读写"------ 通过调整文件指针的位置来实现。核心函数有 3 个:
(1)fseek:移动文件指针
函数原型:int fseek (FILE * stream, long int offset, int origin);
- 作用:根据 "起始位置" 和 "偏移量",把文件指针移到指定位置;
- 参数说明:
origin:起始位置(3 个可选值):SEEK_SET:文件开头;SEEK_CUR:文件指针当前位置;SEEK_END:文件末尾;
offset:偏移量(正数往后移,负数往前移)。
例子:
cpp
#include<stdio.h>
int main()
{
FILE* p = fopen("test.txt", "w");
if (p == NULL)
{
perror("Error opening file");
return 1;
}
fputs("This is an apple.", p);
//fseek(p, 9, SEEK_SET);// 从文件开头(SEEK_SET)往后移9个位置(第10个字符)
//fputs(" sam", p);// 修改后文件内容变成"This is sam apple."
fclose(p);
p = NULL;
return 0;
}

去掉代码的注释再运行的结果:

例子(fread于fwrite):
cpp
#include<stdio.h>
int main()
{
int arr[] = {1,2,3,4,5};
int arr1[] = { 0 };
FILE* pf = fopen("test.txt", "wb");
if (pf == NULL)
{
perror("Error opening file");
return 1;
}
//写数据
int sz=sizeof(arr)/sizeof(arr[0]);
fwrite(arr, sizeof(arr[0]), sz, pf);//把arr的内容以二进制的形式写入文件
// 将文件指针重置到文件开头
fseek(pf, 0, SEEK_SET);
//读数据
fread(arr1, sizeof(arr[0]), sz, pf);//把文件内容以二进制的形式读入arr
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
fclose(pf);
pf = NULL;
return 0;
}
(2)ftell:获取指针偏移量
函数原型:long int ftell (FILE * stream);
- 作用:返回文件指针相对于 "文件开头" 的偏移量(比如指针在第 10 个字符,返回 9,因为从 0 开始计数);
- 常用场景:计算文件大小(把指针移到文件末尾,获取偏移量就是文件字节数)。
(3)rewind:指针回到文件开头
函数原型:void rewind (FILE * stream);
- 作用:直接把文件指针移回文件开头(比如读了一半文件,想重新读,就用这个函数)。
七、避坑重点:文件读取结束怎么判断?
很多新手会用feof函数来判断文件是否读完,这是错误的!
正确的判断逻辑:
feof的作用不是 "判断文件是否结束",而是 "判断文件结束的原因"------ 是读到文件尾了,还是读的时候出错了(比如文件损坏)。
正确的判断方法分两种:
- 文本文件 :判断读写函数的返回值:
fgetc:读失败或到文件尾,返回EOF;fgets:到文件尾,返回NULL;
- 二进制文件 :判断
fread的返回值是否小于 "实际要读的个数"(比如想读 5 个数据,返回 3,说明只读到 3 个,文件结束了)。
cpp
FILE* fp = fopen("test.txt", "r");
if (!fp) {
printf("文件打开失败");
return 1;
}
int c; // 注意用int,因为EOF是-1,char存不下
// 循环读每个字符,直到返回EOF
while ((c = fgetc(fp)) != EOF) {
putchar(c); // 输出到屏幕
}
// 读完后判断原因
if (ferror(fp)) {
printf("读取文件出错");
} else if (feof(fp)) {
printf("文件正常读完");
}
fclose(fp);
fp = NULL;
八、文件缓冲区:为什么写了数据,文件里看不到?
有时候我们用函数写了数据到文件,但打开文件却发现没有内容,这是因为 C 语言有 "文件缓冲区" 的机制。
什么是缓冲区?
系统会在内存中为每个打开的文件开辟一块 "临时存储区"------ 写数据时,不会直接写到硬盘,而是先存到缓冲区;只有当缓冲区满了、调用fflush函数刷新,或者调用fclose关闭文件时,缓冲区的数据才会真正写到硬盘上。
关键结论:
- 操作文件后,一定要
fclose关闭文件(关闭时会自动刷新缓冲区); - 如果不想等缓冲区满,也可以用
fflush(pf)手动刷新(注意:高版本 VS 可能不支持); - 不关闭文件也不刷新缓冲区,可能导致数据丢失(比如程序异常退出,缓冲区的数据没写到硬盘)。
最后:文件操作的核心步骤总结
不管是读还是写文件,都遵循以下 4 个步骤,按这个来就不会出错:
- 打开文件(
fopen)→ 检查是否打开成功; - 进行读写操作(顺序读写或随机读写);
- 关闭文件(
fclose)→ 指针置空(避免野指针); - (可选)判断读写结果(比如是否读完、是否出错)。
C 语言文件操作看似知识点多,但核心逻辑很简单:就是 "打开 - 操作 - 关闭" 的流程,再记住几个常用函数和避坑点,就能轻松实现数据的持久化存储。