
C语言文件操作核心概念——“文件流"的旅程 C语言的文件操作本质上是在管理一个从内存到外部存储设备(如磁盘)的"数据流”,以及管理这个流的"交通管制员"——文件指针。
程序在运行时的数据都存储在内存中,具有"临时性"。一旦程序退出,内存就会被清空,数据就"消失"了。文件就是给数据一个永久的"家"(磁盘),实现数据的持久化保存。
“流"是C语言为了统一操作各种输入/输出设备而抽象出来的一个概念。你可以把流想象成一个"水管”:
C语言程序启动默认有三条打开的"水管":stdin(键盘输入)、stdout(屏幕输出)和 stderr(错误信息输出)。
FILE*(水管的控制阀门)你不能直接操作磁盘上的文件,你需要一个在内存中的"管理员"来间接操作它。这个管理员就是文件指针 FILE*。
FILE结构体关键成员(不同编译器略有差异):
typedef struct _iobuf {
int cnt; // 缓冲区剩余字符数
char *ptr; // 下一个要读/写的位置
char *base; // 缓冲区基地址
int flag; // 状态标志(读/写/错误等)
int fd; // 文件描述符
// 其他成员
} FILE;FILE 结构体:系统在内存中为每个打开的文件创建的一个"信息登记表",记录了文件名、状态、当前读写位置等所有细节。FILE*:是一个指针,指向这个"信息登记表"。所有的文件操作函数(如 fputc、fread)都是通过这个指针找到正确的文件并执行操作。fopen(打开):就像在程序和文件之间建立连接,同时生成上面提到的 FILE 结构体和文件指针 FILE*。你需要告诉它文件名和模式(你想对文件做什么,比如"只读"、“只写”、"二进制写"等等)。
//打开文件
FILE* pf = fopen("文件名","w");
if (pf == NULL)
{
perror("fopen");//打印错误信息
return 1;
}fclose(关闭):断开连接,释放内存中的 FILE 结构体资源。
//关闭文件
fclose(pf);
//防止野指针!!! 千万别忘记这一步,养成好习惯
pf = NULL;文件打开模式(mode)
文件使用方式 | 含义 | 如果指定文件不存在 |
|---|---|---|
r (只读) | 为了输入数据,打开一个已有的文本文件 | 出错 |
w (只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
a (追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
rb (只读) | 为了输入数据,打开一个二进制文件 | 出错 |
wb (只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
ab (追加) | 向一个二进制文件尾添加数据 | 建立一个新的文件 |
r+ (读写) | 为了读和写,打开一个文本文件 | 出错 |
w+ (读写) | 为了读和写,新建一个新的文件 | 建立一个新的文件 |
a+ (读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
rb+ (读写) | 为了读和写打开一个二进制文件 | 出错 |
wb+ (读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
ab+ (读写) | 打开一个二进制文件,在文件尾进行读写 | 建立一个新的文件 |
在C语言编程中,程序本身和它处理的数据在运行时是明确分开的:
程序相关文件:
.c 文件,包含程序员编写的原始代码.obj 文件,编译后的中间文件.exe 文件,最终可以直接运行的程序数据文件:
文件作为程序与外部环境之间的桥梁,主要发挥以下作用:
类型 | 数据存储方式 | 人类可读性 | 适用场景 | 示例函数 |
|---|---|---|---|---|
文本文件 | 以字符编码(如 ASCII, UTF-8)存储,每个字符对应一个或多个字节。 | ✅ 高(可用记事本直接查看) | 配置文件、源代码、日志文件等。 | fprintf, fscanf, fgetc, fputs |
二进制文件 | 以数据在内存中的原始二进制形式直接存储。 | ❌ 低(用记事本打开是乱码) | 图片、音频、视频、程序数据备份等。 | fwrite, fread |
'1'、'0'、'0'、'0'、'0' 五个字节来存。它牺牲了存储效率,但提高了可读性。int 的大小)存下它的二进制表示。它提高了存储效率和读写速度,但直接打开看就是乱码。这里给个例子也许直观一点:

fgetc/fputc、fgets/fputs 都是顺序操作。fseek:让你任意跳转到文件中的某个位置。ftell:告诉你当前光标在哪里(相对于文件开头有多少字节偏移量)。rewind:把光标一键重置回文件开头。函数名 | 功能 | 适用于 | 光标影响 |
|---|---|---|---|
fgetc | 字符输入函数 | 所有输入流 | 读取一个字符后,光标向后移动一个字节 |
fputc | 字符输出函数 | 所有输出流 | 写入一个字符后,光标向后移动一个字节 |
fgets | 文本行输入函数 | 所有输入流 | 读取一行内容(包括换行符,但不超过指定大小),光标移动到读取内容末尾的下一个位置 |
fputs | 文本行输出函数 | 所有输出流 | 写入一个字符串,光标移动到写入内容末尾的下一个位置 |
fscanf | 格式化输入函数 | 所有输入流 | 根据格式符读取数据,光标移动到读取操作停止的位置(可能是下一个未读取字符) |
fprintf | 格式化输出函数 | 所有输出流 | 根据格式符输出数据,光标移动到写入内容末尾的下一个位置 |
fread | 二进制输入 | 文件输入流 | 读取指定数量的字节,光标移动相应的字节数 |
fwrite | 二进制输出 | 文件输出流 | 写入指定数量的字节,光标移动相应的字节数 |
这里可能有人跟我一样就有疑惑了,类似fgetc这类函数,不是拿出字符或者字符串吗,不应该是字符输出函数吗,为何这里写的是输入函数,其实在 C 语言的文件操作中,“输入”和“输出”是相对于程序内存而言的:
1. 为什么是“输入”函数?
stdin)读入到程序的内存变量中 。fgetc 的作用: fgetc 从指定的文件流中读取一个字符,并将其作为返回值返回给程序 。2. 对应的“输出”函数是谁?
与 fgetc 功能相反,用于字符输出的函数是 fputc:
stdout)中 。fputc 的作用: fputc 将程序内存中的一个字符写入到指定的文件流中 。其他函数也是类似一样的理解。
fputc、fgetc这里给出函数原型和描述

这里写入和读出,既可以一个一个输入,也可以循环输入,写入比较简单就直接给出示例:
//写文件
fputc('E', pf);
......
fputc('\n', pf);
char c = 'a';
for (c = 'a'; c <= 'z'; c++)
{
fputc(c, pf);
}这里需要注意的就是,如果你不手动输入换行符,那你输入文件的就一直是一行内容。
当你第二次打开文件的时候,重新写入,那么将会覆盖掉你之前写的内容。无论内容多长,都会全部覆盖掉。

读字符操作,也没那么难,无非就是看清楚你给的权限是什么,如果你读操作还给的写权限,那么你将会收到这样的输出示例:

这里需要提示的是,如果你图方便在原来写操作函数的代码上进行修改的话,一定
首先保存代码,不然有可能你上次写的那些字符丢失掉,我已经踩过坑了。
读操作同样也可以单个读和循环读,无非就是看清你刚才写入的那个控制符(换行'\n')。
//读文件 -- 单字符
char c = 0;
c = fgetc(pf);
printf("%c", c);
puts(" ");
//读文件 -- 循环读
while ((c = fgetc(pf)) != EOF)
{
printf("%c", c);
}
这里解释一下为什么循环读的时候,没有打印E,因为在循环读之前,已经进行了一次单独读字符,此时光标已经移到下一个字符了。
fputs、fgets1. fputs:文本行输出(写入)
fputs 是 C 语言中用于将字符串写入到文件流的函数。它从程序内存中获取一个 C 字符串(以 \0 结尾),并将其内容复制到指定的文件流中。
\0)。puts 函数不同,fputs 不会在写入的字符串末尾自动追加换行符(\n)。如果需要换行,必须手动写入 \n。2. fgets:文本行输入(读取)
fgets 是 C 语言中用于从文件流中读取一行文本的函数。它将读取的字符串存储到指定的缓冲区 str 中。
num 参数,防止缓冲区溢出。\n),fgets 会将其包含在存储到缓冲区的结果字符串中。读取结束后,它会自动在字符串末尾追加 \0。NULL 来确定文件是否读取结束或发生错误。
这里进行写入就直接按照函数调用格式进行写入就好:

但是读操作需要细看一下了,他的参数有一个个数限制,如果你的个数比你接收字符串的长度要大,这里是会出现问题的。 思考这么一个问题,如果文件中内容很长,但你的接收数组很小,这时候会报错吗? 先来看结果,很明显文件里面是有部分内容长度超出了我的数组长度,但结果还是正确打印在了屏幕上:

这是为什么呢,确实内容很长,但你每次只读五个放在数组里,打印也是打印读入的字符串,就相当于你把文件里面的内容5个5个往出挪,只不过次数多一点而已。
fscanf、fprintffprintf 和 fscanf 被称为 printf 和 scanf 的文件版本,它们唯一的、也是最关键的区别在于第一个参数:FILE * stream。

FILE * stream 参数是文件 I/O 函数的核心。它使得这些函数具有了通用性,能够操作 C 语言抽象出的所有流:
stream 是一个由 fopen 返回的文件指针时,操作的是磁盘文件。stream 是 stdout 时,操作的是标准输出(屏幕)。stream 是 stdin 时,操作的是标准输入(键盘)。fprintf 是 C 语言中负责格式化输出(写入)的函数,其本质功能与 printf 相同,但它通过在函数原型中增加一个 FILE * stream 参数,获得了将格式化数据流向任何指定流(包括磁盘文件)的能力。它允许程序员按照预定的格式字符串(%d, %s 等)将程序内存中的变量值结构化、清晰地写入到文件中,是实现可读性高的数据文件存储的关键工具。
fscanf 是 C 语言中负责格式化输入(读取)的函数,它与标准输入函数 scanf 相对应,核心区别同样是增加了 FILE * stream 参数来指定数据来源。它能够从指定的文件流中读取数据,并根据格式控制符进行匹配和解析,将结果存储到对应的变量地址处,返回成功读取并赋值的项数,是读取结构化文件数据和进行复杂文本解析的强大工具。
因此, fprintf 和 fscanf 是标准库函数中功能最强大的格式化 I/O 版本。


以上就是部分顺序读写函数的使用方法,下来了解一下随机读写吧。
fseek:定位文件光标,是用于定位文件指针(文件光标)的函数。
int fseek ( FILE * stream, long int offset, int origin );
属性 | 描述 |
|---|---|
功能 | 根据文件指针的当前位置或起始位置,加上指定的偏移量,重新定位文件光标。 |
函数原型 | int fseek (FILE * stream, long int offset, int origin); |
光标影响 | 将光标设置到 origin + offset 的新位置。 |
参数 origin(起始位置)宏定义:
宏定义 | 含义 |
|---|---|
SEEK_SET | 从文件开头开始计算偏移量。 |
SEEK_CUR | 从文件当前位置开始计算偏移量。 |
SEEK_END | 从文件末尾开始计算偏移量。 |
说是随机读写,看着也不是很随机,只不过对比顺序读写,倒是随机了一些。

注意,它是移动光标,没有读写功能,要读写的话还是按照前文所涉及到的函数来读写。
ftell 用于报告文件指针相对于文件起始位置的偏移量。

属性 | 描述 |
|---|---|
功能 | 返回文件指针当前所处的位置,这个值是相对于文件开头的字节数。 |
函数原型 | long int ftell (FILE * stream); |
光标影响 | 不改变文件光标的位置。 |
那我们是不是可以结合上面的fseek函数求出字符串长度呢?尝试一下:首先使用 fseek(pf, 0, SEEK_END) 将文件指针移动到文件末尾,然后通过 ftell(pf) 获取当前文件位置(即文件总字节数)。输出的文件大小与字符串 "Extreme 20 65.500000" 的长度一致,表明测量准确。程序最终正常退出(代码为0),验证了文件操作的成功执行。


rewind 是一个快速将文件指针重置到文件开头的函数。
属性 | 描述 |
|---|---|
功能 | 让文件指针的位置回到文件的起始位置(即偏移量 0)。 |
函数原型 | void rewind (FILE * stream); |
光标影响 | 将光标重置到文件流的起始位置。 |
等价操作 | 功能上等价于 fseek(stream, 0, SEEK_SET)。 |

结果也确实跟我们预期的一样。
feof:文件结束标志检测,feof 函数用于检查文件流是否已经到达文件尾 (End-Of-File)。

属性 | 描述 |
|---|---|
功能 | 检查在先前对流进行的输入操作中,是否遇到了文件尾指示器。 |
函数原型 | int feof (FILE * stream); |
返回值 | 如果已设置文件结束指示器,返回一个非零值;否则返回 0。 |
关键注意事项(防止误用)
牢记: 在文件读取的循环过程中,不能用 feof 的返回值直接来判断文件是否结束。
feof 的正确作用:当文件读取已经结束时,判断读取结束的原因是否是遇到文件尾结束。fgetc 或 fread 这样的读取函数,当读取操作失败(例如,遇到文件尾或发生 I/O 错误)时,会返回特殊值(如 EOF 或小于请求的个数)来终止循环。只有当循环结束后,才能使用 feof 来区分是正常读到文件末尾还是发生了读取错误。正确的文件读取结果判断应该依赖读取函数的返回值:
fgetc 的返回值是否为 EOF,或 fgets 的返回值是否为 NULL。fread 的返回值是否小于实际要读的个数。很多人用 feof 来判断文件是否读完,这是错误的。
feof 并不判断"是否读完了"。它只在文件已经被读取成功结束后,判断失败的原因是不是"遇到了文件尾"。fgetc 或 fread)的返回值: EOF 或 NULL。ferror:错误指示器检测,ferror 函数用于检查文件流的错误状态。

属性 | 描述 |
|---|---|
功能 | 检查流的错误指示器是否被设置,以确定输入或输出操作是否发生了错误。 |
函数原型 | int ferror (FILE * stream); |
返回值 | 如果错误指示器已设置,返回一个非零值;否则返回 0。 |
关键作用:ferror 通常与 feof 结合使用,在文件读取循环终止后,用于诊断失败的原因:
fread 返回值不完整),并且 feof 返回 0,则说明文件没有到达末尾,此时极有可能是发生了 I/O 错误。ferror(fp) 的返回值是否为非零,可以确认是否是由于 I/O 错误导致了读取操作的终止。示例:
int main()
{
//打开文件
FILE* pf = fopen("Extreme.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写
char ch = 0;
for (ch = 'a'; ch <= 'z'; ch++)
fputc(ch, pf);
//判断是什么原因导致读取结束的
if (feof(pf))
printf("遇到文件末尾,读取正常结束\n");
else if (ferror(pf))
perror("fputc");
//关闭文件
fclose(pf);
//防止野指针
pf = NULL;
return 0;
}我写的这段 C 语言代码核心逻辑存在严重错误:它尝试使用 只读模式(“r”)打开文件 Extreme.txt,但随后却在循环中执行 写入操作(fputc)。由于权限冲突,fputc 调用将立即失败,并设置文件流的错误指示器。代码最后尝试用 feof 和 ferror 诊断状态,由于没有成功的读取操作,故而没有到文件末尾,feof 返回假;而 ferror 会返回真,正确地指出因写入权限不足导致了 I/O 错误,程序将输出系统错误信息(如Bad file descriptor),这确认了文件打开模式与操作行为之间的矛盾。

C 语言标准采用 缓冲文件系统 来处理数据文件。
直接对磁盘(外存)进行读写操作是非常慢的。频繁的小量 I/O 操作(比如每写一个字节就访问一次磁盘)效率极低。 缓冲区的目标: 通过在内存中集中处理数据,将多次小的操作合并为一次大的、高效的磁盘操作。
文件缓冲区的机制分为写入(输出)和读取(输入)两个方向:
fputc 或 fprintf 等输出函数时,数据不会立即写入磁盘,而是先被送到内存中的文件缓冲区。fgetc 或 fscanf 等输入函数时,如果缓冲区为空,系统会从磁盘文件中读取一整块数据(充满缓冲区)输入到内存缓冲区中。由于数据暂时停留在内存中,如果不进行特殊操作,程序崩溃或异常退出时,缓冲区中的数据可能会丢失。因此,我们必须进行刷新操作,将数据从内存推送到磁盘。
函数 | 功能 | 描述 |
|---|---|---|
fflush(fp) | 强制刷新 | 手动将文件缓冲区中的所有数据立即写入磁盘。常用于确保关键数据及时保存。 |
fclose(fp) | 关闭文件 | 在关闭文件之前,系统会自动执行一次刷新缓冲区的操作,然后再释放所有相关资源。 |
#include <stdio.h>
#include <windows.h> // 包含 Sleep 函数
int main() {
FILE* pf = fopen("test.txt", "w");
if (pf == NULL) return 1;
fputs("abcdef", pf); // 数据先放在输出缓冲区
printf("睡眠10秒-此时打开test.txt文件,文件可能没有内容...\n");
Sleep(10000); // 休眠10秒,数据仍停留在内存缓冲区
printf("刷新缓冲区\n");
fflush(pf); // 强制刷新,数据写入磁盘
printf("再睡眠10秒-此时打开test.txt文件,文件就有内容了。\n");
Sleep(10000);
// fclose 在关闭文件时也会刷新缓冲区
fclose(pf);
pf = NULL;
return 0;
}