本节重点
- 理解文件的形式与基本概念
- 二进制文件与文本文件
- 文件的打开与关闭
- 文件读写函数、
- 文件缓冲区
正文开始---------------------------------------------------------------------------------------------------------------------
一、为什么使用文件
程序运行时数据存储在内存中(如变量、数组),但程序结束后内存会被操作系统回收,数据随之丢失,用户配置、日志记录、程序状态等数据需跨会话保存,文件是操作系统提供的标准持久化方案,通过文件存储,C 程序能够突破内存限制,实现数据的长期保存和跨会话共享,是构建健壮应用程序的基础能力。
也就是说数据保存在内存之中容易丢失,我们可以通过文件操作将数据保存在文件也就是磁盘或硬盘之中。
二、什么是文件
在计算机领域,文件是存储数据的核心单位,用于将信息以特定格式保存在物理存储设备(如硬盘、SSD)中。
在Windows中通过可视化操作界面,文件的存在形式是这样的:

而在Linux操作系统中,通过命令行操作文件的存在形式是这样的:

2.1 程序文件
程序文件是包含机器语言指令(二进制代码)或解释型语言代码的文件,可被操作系统识别并执行从而实现特定的功能。
程序文件包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行文件(Windows环境后缀为.exe)。

2.2 数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。

在这里,我们主要讨论的是数据文件。
在之前我们所处理的数据的输入输出都是以终端为对象,即从终端的键盘输入数据(主要通过scanf 函数),运行结果显示到显示器上(主要通过 printf 函数)。
其实有时候我们可以把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上的文件。
2.3、文件名
文件名要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件主干+文件后缀,例如:
D:\code\new_code\code.exe
为了方便起见,文件标识符常被称为文件名。
四、二进制文件与文本文件
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换就输出到外存的文件中,就是二进制文件。如果要求在外存上以ASCLL码的形式存储,则需要在存储前转换,以ASCLL码的形式存储的文件就是文本文件。
一个数据在文件中是怎么存储的呢?
字符一律按ASCLL码的形式存储,数值型数据既可以用ASCLL码形式存储也可以以二进制形式存储。
例如,一个数值型数据10000以ASCLL码存储时一共有5个字符分别为1、0、0、0、0共占用5个字节,而以二进制形式存储时占4个字节。

四、文件的打开与关闭
4.1 流和标准流
4.1.1 流
在编程中,"流"(Stream)是一种抽象的数据处理模型,用于逐步传输或处理数据,而无需一次性将整个数据加载到内存中。它类似于现实世界中的"水流",数据像水流一样按顺序流动,程序可以按需读取或写入数据片段。
C程序针对文件、画面、键盘等的数据的输入输出操作都是通过流实现的。
4.1.2 标准流
在编程中,"标准流"(Standard Streams)是操作系统或运行时环境预定义的三个默认数据流,用于程序与外界(如终端、文件或其他程序)进行交互。它们是:
- stdin -标准输入流,在大多数情况下从键盘输入,scanf函数就是从标准输入流中读取数据
- stdout-标准输出流,大多数环境中输出到显示器界面,printf函数就是将信息输出到标准输出流中
- stderror-标准错误流,大多数环境中输出到显示器界面。
当我们的程序启动,或者说进程创建时,操作系统会为我们的程序自动分配三个标准流并默认连接到终端设备(显示器、键盘)。
stdin、stdout、stderror三个流的类型都是FILE*,通常称为文件指针,在C语言中就是通过文件指针来维护流的各种操作的。
4.2 文件指针
当我们打开一个文件时,为了方便我们对被打开文件的进一步管理和维护,内存中就会开辟一个相应的"文件信息区"用来存放被打开文件的相关信息(如文件的名字,状态以及文件当前的位置等等文件属性)。这些信息是保存在一个结构体变量中的,也就是说这个"文件信息区"就是一个结构体,该结构体的类型是由系统声明的,取名为FILE。
例如,vs2013编译环境提供的stdio.h头文件中有以下的文件类型声明:
cpp
struct _iobuf {
char *_ptr;
int _cnt;
char *_base;
int _flag;
int file;
int _charbuf;
int bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
每当打开一个文件时,系统就会开辟内存空间创建一个FILE结构体来管理该文件的相关属性和信息,使用者不必关心其细节。
为了将每个文件与所对应的"文件休信息区"(FILE)关联起来,就引出了文件指针(FILE*)的概念。通过使文件指针指向某个文件的"文件信息区"(本质是一个结构体变量),通过该文件信息区中的信息就能找到并管理该文件。

也就是说,通过文件指针变量就能间接找到与它关联的文件。
4.3 文件的打开与关闭
文件在读写之前首先应该打开文件,在使用结束后关闭文件。本质上就是保证打开文件后系统就会创建对应的"文件信息区"来方便用户对文件进行管理与维护,关闭文件目的是释放相应的内存空间,不然过多无效的"文件信息区"会占用内存,严重时会导致内存泄漏。
在编写程序的时候,在打开文件的同时,都会返回一个FILE* 类型的指针变量指向该文件,也相当于建立了指针与文件的关系。
ANSIC规定使用fopen函数来打开文件,fclose来关闭文件:
cpp
//打开文件
FILE* fopen(const char* filename,const char* mode);
//关闭文件
int fclose(FILE* stream);
mode参数表示文件的打开方式,下面都是文件的打开模式:
模式 | 描述 | 读权限 | 写权限 | 追加模式 | 文件不存在时 | 文件存在时 | 初始位置 |
---|---|---|---|---|---|---|---|
r |
只读模式(文本文件) | ✔️ | ❌ | ❌ | 失败 (返回 NULL ) |
保留内容,从文件开头读取 | 文件开头 |
w |
只写模式(文本文件,覆盖原有内容) | ❌ | ✔️ | ❌ | 创建新文件 | 清空文件内容 | 文件开头 |
a |
追加模式(文本文件,写入内容到文件末尾) | ❌ | ✔️ | ✔️ | 创建新文件 | 保留内容,写入位置在文件末尾 | 文件末尾 |
r+ |
读写模式(文本文件,不截断文件) | ✔️ | ✔️ | ❌ | 失败 | 保留内容,从文件开头读写 | 文件开头 |
w+ |
读写模式(文本文件,覆盖原有内容) | ✔️ | ✔️ | ❌ | 创建新文件 | 清空文件内容 | 文件开头 |
a+ |
读写追加模式(文本文件,写入到文件末尾,可读取整个文件) | ✔️ | ✔️ | ✔️ | 创建新文件 | 保留内容,写入位置在文件末尾 | 文件末尾(读从开头) |
rb |
只读模式(二进制文件) | ✔️ | ❌ | ❌ | 失败 | 保留内容,从文件开头读取 | 文件开头 |
wb |
只写模式(二进制文件,覆盖原有内容) | ❌ | ✔️ | ❌ | 创建新文件 | 清空文件内容 | 文件开头 |
ab |
追加模式(二进制文件,写入内容到文件末尾) | ❌ | ✔️ | ✔️ | 创建新文件 | 保留内容,写入位置在文件末尾 | 文件末尾 |
r+b |
读写模式(二进制文件,不截断文件) | ✔️ | ✔️ | ❌ | 失败 | 保留内容,从文件开头读写 | 文件开头 |
w+b |
读写模式(二进制文件,覆盖原有内容) | ✔️ | ✔️ | ❌ | 创建新文件 | 清空文件内容 | 文件开头 |
a+b |
读写追加模式(二进制文件,写入到文件末尾,可读取整个文件) | ✔️ | ✔️ | ✔️ | 创建新文件 | 保留内容,写入位置在文件末尾 | 文件末尾(读从开头) |
x |
独占创建模式(仅当文件不存在时创建,失败返回 NULL ) |
✔️/❌ | ✔️ | ❌ | 创建新文件 | 失败(文件已存在) | 文件开头 |
实例代码:
cpp
#include<stdio.h>
int main()
{
FILE* pf=fopen("text.txt","w");
if(pf==NULL)
{
//文件打卡失败
perror("fopen");
return 0;
}
const char* txt="hello world!!";
fputs(txt,pf);
fclose(pf);
return 0;
}
五、文件的顺序读写
5.1 字符读写
fgetc

- 功能:从文件中读取单个字符
- 参数:FILE* stream(文件指针)
- 返回值:成功返回字符的int值,失败或文件结束返回EOF
fputc

- 功能:向文件中写入单个字符
- 参数:int c(字符),FILE* stream(文件指针)
- 返回值: 成功返回写入的字符,失败返回EOF
5.2 字符串读写
fgets

- 功能:从文件中读取一行字符串(直到遇到换行符或指定长度)
- 参数:char* s(自定义缓冲区),int size(最大长度),FILE* stream(文件指针)
- 返回值:成功返回缓冲区的指针,失败或文件结束返回NULL
fputs

- 功能:向指定文件中写入一行字符串
- 参数:const char* s(字符串),FILE* stream
- 返回值:成功返回非负值,失败返回EOF
5.3 格式化读写
fscanf

- 功能:按指定格式从文件中读取数据
- 参数:FILE* stream,格式化字符串,变量地址列表
- 返回值:成功匹配并赋值的参数个数,失败返回EOF
fprintf

- 功能:按指定格式向文件中写入数据
- 参数:FILE* stream,格式化字符串,变量列表
- 返回值:成功返回写入的字符数,失败返回负数
5.4 块读写(二进制模式)
fread

-
功能:从文件中读取二进制数据块
-
参数:void* ptr(缓冲区),size_t size(每个元素大小),size_t nmemb(要读取的元素数量),FILE* stream。
-
返回值:成功读取的元素数量。
fwrite

-
功能:向文件写入二进制数据块。
-
参数:void* ptr(要写入的数据的指针),
size_t size
(每个元素大小),size_t nmemb(要写入的元素的数量)
,FILE *stream
。 -
返回值:成功写入的元素数量
六、文件的随机读写
文件的随机读写允许程序直接跳转到文件的任意位置进行读写操作,无需按顺序访问。
fseek

- 功能:移动文件指针到指定位置
- 参数:FILE* stream(文件指针),long offset(偏移量字节) ,int origin(基准位置)
- 返回值:成功返回0,失败返回非0值
ftell

- 功能:返回文件指针相对于起始位置的偏移量
- 参数:FILE* stream
- 返回值: 成功返回long类型的偏移量
rewind

- 功能:将文件指针重置到文件开头
- 参数:FILE* stream
- 返回值: 无
七、文件缓冲区
在之后的学习中我们会了解到,其实C语言中文件的各类读写函数其实底层都封装了系统调用接口read,write来向操作系统发出各类操作指令 ,而系统调用是一个复杂的概念它涉及到操作系统对内存空间的各类调度与管理,如果C语言中的文件函数直接访问文件或磁盘中的数据,频繁地进行读取和写入操作,就意味着系统调用接口的频繁调用加重CPU的负担,减低I/O的效率。
为了避免这个问题,我们引入了文件缓冲区的概念,它通过先将程序数据先放入缓冲区中,通过刷新机制将多次小数据量的读写合并为少量大块操作,减少直接访问磁盘的次数,显著提升I/O效率,避免频繁的系统调用。

写个代码验证一下:
cpp
#include<stdio.h>
#include<windows.h>
int main()
{
FILE*pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒已经写数据了,打开test.txt⽂件,发现⽂件没有内容\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到⽂件(磁盘)
//注:fflush 在⾼版本的VS上不能使⽤了
printf("再睡眠10秒-此时,再次打开test.txt⽂件,⽂件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭⽂件的时候,也会刷新缓冲区
pf = NULL;
return 0
}