程序关闭数据就丢失?学会 C 语言文件操作,摆脱一次性数据!
什么是文件?
文件就是存储在磁盘、u盘等外存设备上,以文件名标识的数据集合,由操作系统统一管理。
文件分类:
(1) 按存储内容(文件功能)
- 程序文件:能让电脑/软件直接运行、执行指令的文件,后缀有固定格式 。eg:源代码(.c)、目标文件(.obj)、可执行程序(.exe)
- 数据文件:只存数据不存代码的文件,不能双击运行只打开能读取。eg:文字、表格、图、音频、视频、配置、备份等等。
(2)按用户视角
- 文本文件(ASCII文件):数据以数据以ASCII 字符形式存放,每个字符占 1 字节,可以直接用记事本打开阅读。eg:数字 123 保存为字符 '1'、'2'、'3',占用 3 字节。
- 二进制文件:数据按照内存中的原始二进制形式直接存储,不转换为 ASCII 码。eg:整数 123 以二进制形式存储,占用 4 字节(int),记事本打开显示乱码。
文件名:
用于唯一标识磁盘上文件的名称,操作系统依靠文件名查找、访问文件。
文件名包含3部分:文件路径+文件主干名+文件后缀

我们为什么要学习文件操作(使用文件·)?
在我们写C语言程序时,所有数据都放在内存中。内存有一个致命的特点:断电、程序一旦退出,内存就会被系统释放,数据彻底丢失。而文件是存储在硬盘(磁盘)上的,程序关了、电脑重启了,数据依然还在。下次打开程序,可以直接读取上次保存的数据。
注意!
源文件中的数据≠程序运行时输入的数据,.c源文件保存的是文本文件,里面写的数据属于常量。键盘通过
scanf()录入的数据,仅加载进内存 (RAM)供程序临时使用,不会写入.c源代码文件,内存是临时存储空间,程序退出数据就销毁。
解决数据丢失的核心,就是掌握 C 语言的文件操作。无论是游戏存档、学生管理系统,还是各种软件的配置文件,底层全靠它来实现。
一、核心概念:流与文件指针
1.1 流
1.1.1 什么是流
C 语言把键盘、显示器、磁盘文件等所有外部 I/O 设备统一抽象成字符流(字节流)
流也被定义为有序字节序列
- 有序:数据是一个接着一个存取,不是随机的
- 字节:现代操作系统以字节作为最小IO传输单位(系统不区分一张图片、一段字符,只认字节)
为什么需要流?
外部 I/O 设备就是输入输出外部设备,是计算机主机(cpu、内存)意外能够与计算机进行数据交换的硬件。每种设备有不同的读写函数,程序员想切换数据源(eg:从读键盘改成读文件),要大面积改写代码。
如果引入‘流’程序只和“流”打交道,代码不用区分底层是键盘、屏幕还是磁盘文件。C 标准库(stdio)封装一层,形成流接口,程序员负责:打开流、读写流、关闭流。硬件交互由 OS 和库完成。大幅度的简化程序开发,降低代码复杂度,程序可以灵活切换输入输出目标。
1.1.2 标准流
C 程序启动时默认打开三个流:
- stdin (标准输入流) :通常对应键盘。eg:scanf
- stdout(标准输出流 ): 通常对应屏幕。eg:printf
- stderr(标准错误流):通常对应屏幕,用来输出错误信息。
1.2 文件指针(FILE*)
操作文件时,系统会根据文件情况在内存中创建一个FILE结构体并填充文件的信息(文件名、当前读写位置、文件打开模式、缓冲区信息、错误标记、EOF等信息)
- FILE 是结构体类型
- FILE*是文件指针,代表文件流
- 不同C编译器的FILE类型包含的内容不完全相同但大同小异
- 核心作用:
(1) 标识正在操作的文件
eg:需要打开多个文件时
FILE* f1=fopen("a.txt","w");
FILE* f2=fopen("b.txt","w");
(2) 记录当前读写位置
eg:每次文件操作后,文件指针的位置自动向后移动
fgetc(对文件进行读取)

(3)连接内存和磁盘文件
简单来说,内存是工作台(空间小,速度快,程序运行时把药用的数据临时拿到这里处理,断电后消失),磁盘是仓库(空间大,速度慢。长期存放文件、软件,断电数据仍然保存)。
FILE 结构体、缓冲区全部位于内存,不在硬盘。硬盘只存最终的文件二进制内容。
在C语言中,程序员无法直接操作磁盘,这就需要FILE * 指针和缓冲区(Buffer)连接内存和磁盘

注 意 !缓 冲 区 机 制 !
- 写文件:数据先放到缓冲区,缓冲区满 / 主动调用
fflush()/fclose()关闭文件时,数据才真正写入磁盘; - 如果不关闭文件,程序异常退出,缓冲区未落地的数据直接丢失。
二、核心操作:文件的打开和关闭
文件操作必须要对文件进行打开和关闭
- 打开文件 fopen:CPU告诉操作系统,我要访问硬盘上的某个文件,在内存用户空间创建FILE结构体。分配用户缓冲区,把缓冲区地址、硬盘文件位置、读写书签位置保存进 FILE,返回文件指针
FILE* fp,程序后续所有读写都依靠这个指针。不打开文件就没有结构体、没有缓冲区、不能进行文件读写 - 关闭文件 fclose:自动调用fflush把用户缓冲区剩余的数据刷新送到内核缓冲区。释放内存资源,销毁FILE结构体、释放用户缓冲区占用的内存。如果不写fclose,缓冲区还没满的数据留在内存用户缓冲区,不会写入磁盘、断电数据丢失,打开太多文件不关闭,后续打不开新文件
2.1 fopen 打开文件
FILE *fopen(const char *filename, const char *mode);
-
filename:文件名,可以是相对路径,也可以是绝对路径。
-
mode:打开模式,控制读写。
-
返回值:打开成功返回FILE*指针,打开失败返回NULL。
-
作用:打开一个外部磁盘文件,建立程序和磁盘文件之间的通道,返回文件指针。
注 意 !
打开文件必须判断返回值会否为空,防止文件打开失败,影响后续代码的运行。
fopen常用读写模式
| 模式 | 含义 | 文件不存在 | 文件存在 |
|---|---|---|---|
"r" | 只读 ,打开已有文件读 | 打开失败,返回 NULL | 从头读取 |
"w" | 只写 ,创建写入文件 | 新建文件 | 清空原有全部内容,从头覆盖 |
"a" | 追加 ,末尾写 | 新建文件 | 在文件末尾追加内容,原有内容保留 |
"r+" | 可读可写 | 打开失败 | 从头读写,可覆盖原有数据 |
"w+" | 可读可写 | 新建文件 | 清空文件,先写后读 |
"a+" | 可读 + 追加写 | 新建文件 | 读任意位置;写永远加到末尾 |
二进制文件(加上字母b表示二进制文件)
| 模式 | 读写权限 | 文件不存在 | 文件存在 |
|---|---|---|---|
"rb" | 二进制只读 | 打开失败 | 正常打开 |
"wb" | 二进制只写 | 创建新文件 | 清空原有内容 |
"ab" | 二进制追加写 | 创建新文件 | 保留原有内容 |
"rb+" | 二进制可读可写 | 打开失败 | 正常打开 |
"wb+" | 二进制可读可写 | 创建新文件 | 清空原有内容 |
"ab+" | 二进制可读、追加写 | 创建新文件 | 保留原有内容 |
2.2 fclose 关闭文件
int fclose(FILE *stream);
- FILE*stream:文件指针—fopen返回的指针。
- 返回值:关闭成功返回0,关闭失败返回EOF(一般为-1)。
- 作用:刷新缓冲区,关闭文件,释放操作系统资源。
注 意 !
打开的文件一定要使用fclose关闭,不能对NULL(打开失败的文件指针)进行fclose,关闭后将文件指针置为空,消除野指针、防止误访问,空文件指针变成野指针不能再次进行读写,一个文件指针只能关闭一次。
eg:

三、文本文件读写
| 函数名 | 作用 | 适用范围 | 读 / 写 | 流的范围 |
|---|---|---|---|---|
| fgetc | 从文件流读取1 个字符 | 单个字符读取;遍历整个文件 | 读 | 任意输入流:文件 (stdin 也可以) |
| fputc | 向文件流写入1 个字符 | 单个字符写入 | 写 | 任意输出流:文件 (stdout/stderr 也可以) |
| fgets | 从流读取一行字符串,最多 n‑1 个字符,自动补\0 | 按行读取文本;读取字符串 | 读 | 任意输入流:文件 (stdin 也可以) |
| fputs | 向流写入一串字符串,不会自动添加换行 | 写入整段文字 / 一行内容 | 写 | 任意输出流:文件 (stdout/stderr 也可以) |
3.1 fputc :写入单个字符
int fputc(int ch, FILE* stream);
- int ch : 要写入的字符,底层存的是ASCII。
- FIILE* stream : 目标流指针(fopen打开的文件)(文件指针/stdout)。
- 返回值 : 写入成功-返回该字符的ASCII码值(int),写入失败-返回EOF(-1)。
⭐注 意 事 项 !
fputc函数一次只能写一个字符且不会自动换行
3.2 fgetc :读取单个字符
int fgetc(FILE* stream);
- FILE*stream:输入流指针(文件指针/stdin)。
- 返回值:读取成功-返回字符的ASCII码值,读取失败-返回FOF(-1).
在这里想跟大家一起讨论一个我学习过程中的疑惑点
字符的读取为什么返回int而不是char?
核心原因是为了能区分“普通字符”和“结束标志”
char是1字节,signed char的范围是-128-127,unsigned char的范围是0~255;int是4字节,范围很大;
假设文件中正好有一个字符255,当把255赋给signed char时会变成-1,这时程序就会把正常字符 255,误判成文件结束 EOF。
将返回值设为int可以避免二者值冲突,顺利进行文件结束的判断。
⭐注 意 事 项 !
读完一个字符后,文件指针自动向后移一位,依次直接读取下一个字符
eg:

3.3 fputs :写入字符串
int fputs(const char* str, FILE* stream);
- const char* str:要输入的字符串(字符串必须以'\0'结尾,'
\0'不会写入文件,作为停止标记) - FILE* strem:目标指针流(文件指针/stdout)。
- 返回值:写入成功-返回非负值,写入失败返回EOF(-1);
⭐注 意 事 项 !
不会自动追加换行符,只能输出字符串不能单独输出字符,输入字符串必须以'\0'结尾。
3.4 fgets :读取一行字符串
char* fgets(char* str, int n, FILE* stream);
- char *str:字符数组/需要放入字符串的地址
- int n:读取字符串的长度(有效字符个数),最多读取 n‑1 个有效字符,最后一个位置自动放 '
\0'。 - 返回值:读取成功-返回str的地址,读取到文件末尾/读取出错-返回NULL。
⭐注 意 事 项 !
读到换行符 \n,会一并存入缓冲区,读取结束自动补字符串结束符 \0

四、格式化读写
4.1 fprintf:格式化写入文件
int fprintf(FILE* stream, const char* format, ...);
- FILE* stream:文件流指针(要写入的目标位置)。
- const char* formt:格式控制字符串。
- ... : 可变参数,用来匹配format里面每一个%占位符。
- 返回值:写入成功-返回成功输出的字符总数,写入失败-返回负数。
⭐注 意 事 项 !
format 字符串不会自动换行,换行要手动加 \n
fprintf(stdout,"xxx")效果等同于printf("xxx")
4.2 fscanf:格式化读取文件
int fscanf(FILE* stream, const char* format, ...);
- FILE* stream:文件流指针(读取数据的目标位置)。
- const char* formt :格式控制字符串。
- ... :可变参数,变量的地址,需要传入&变量名。
- 返回值:读取成功-返回成功读到并赋值的数据项个数(成功匹配到几个数据),读取到文件末尾-返回EOF(-1),读取失败-返回小于预期数据项的数量。
⭐注 意 事 项 !
可变参数必须传地址,漏掉&会造成内存错误。读取时会自动跳过空格、换行、Tab 空白符。
fscanf(stdin,"%d",&x)等价于scanf("%d",&x)。
eg:
五、二进制文件读写
5.1 fwrite:将数据以二进制形式写入文件
size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream);
- const void* ptr:被写入文件数据的地址。
- size_t size:单个元素所占字节数(一般用sizeof())。
- size_t count:打算写入多少个size大小的元素。
- FILE* stream:文件流指针。
- 返回值:写入成功-返回写入的元素个数(不是字节数),写入失败-返回值小于写入元素个数
⭐注 意 事 项 !
ptr参数填的是地址!size_t 是无符号类型二进制文件不能用 EOF 判断结束!
5.2 fread : 从二进制文件中读取数据
size_t fread(void* ptr, size_t size, size_t count, FILE* stream);
- void* ptr:接收数据的内存起始地址。
- size_t size : 单个元素所占字节数(一般用sizeof())。
- size_t count : 即将读取的元素个数。
- FILE*stream : 文件指针流。
- 返回值:读取成功-返回成功读取到的个数,读取失败-返回0,读取到文件末尾/数据不够-返回小于读取个数的值。
⭐注 意 事 项 !
二进制文件不能用 EOF 判断读取结束!
eg:

六、文件指针位置控制
C语言默认读写顺序只能从头到尾。控制指针就可以实现随机读写,控制指针移动到文件任意位置读/改数据。
区分两个容易混淆东西
1. FILE* pf:文件指针变量--读写数据的文件地址。
2. 文件指针位置:隐藏在FILE结构体内部,标记下次开始读/写的位置。(对文件修改时的光标)
6.1 fseek:移动文件指针
int fseek(FILE *stream, long int offset, int whence);
- stream:文件指针(正在进行文件操作的文件地址)。
- offset:偏移量,前->后—正数,后->前—负数,单位为字节。
- whence:参考起点(3种选择)(相对偏移量起始位置)。
| 常量 | 起点位置 |
|---|---|
SEEK_SET | 文件开头(偏移 0 位置) |
SEEK_CUR | 光标当前所在位置 |
SEEK_END | 文件末尾(最后一个字节的后面) |
- 返回值:成功返回0,失败返回1。


6.2 ftell:返回文件指针的当前位置
long int ftell(FILE *stream);
- stream:文件指针流(当前操作文件地址)。
- 返回值:ftell返回的是当前文件指针的位置距离文件开头多少字节。
eg:

6.3 rewind:光标直接回到文件开头
void rewind(FILE *stream);
- stream:文件指针流(当前操作文件地址)。
- 无返回值

七、文件读取结束的判定
文件读取过程中读取结束有两种可能,文件结束/读取错误 ,很多储蓄者容易混淆这两个函数,这两个函数在使用过程中也有需要注意的易错点。
7.1 feof:判断是否达到文件末尾
int feof(FILE* stream);
- 返回值:达到文件末尾-返回非0值(文件流内部被标记“末尾标记”),未达到文件末尾-返回0。
注意!!
feof是用来判断、检测文件读取后是否已经达到文件末尾

7.2 ferror:判断读取过程是否发生错误
int ferror(FILE *stream);
- 返回值:出现错误(读写错误,硬件 / 权限等异常)-返回非0,读取正常-返回0。
注意!!
ferror 只检测错误标志位(C 语言 IO 读写函数内部自动设置),不会清除错误;错误标志一旦置位会一直保留,直到调用 clearerr() 或 rewind() 才重置。
eg:feof与ferror的正确使用

注 意 !
fgetc() 返回值是 int,读到文件末尾返回 EOF(-1)。
// ❌ 错误写法
while(fgetc(pf) != EOF)
//✅️ 正确写法!
int ch; //必须用int,不能用char
while( (ch = fgetc(pf)) != EOF )
{
putchar(ch);
}
此篇文章到这里就结束了,谢谢大家的观看,我们下期再见!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐




所有评论(0)