
📚 个人主页: ByteWizard
※ 专栏目录: 《C语言》
⭐ 春风得意马蹄疾,一日看尽长安花
📚 ByteWizard 的简介:

目录
- 1.为什么使用文件
- 2.什么是文件
-
- [2.1 程序文件](#2.1 程序文件)
- [2.2 数据文件](#2.2 数据文件)
- [2.3 文件名](#2.3 文件名)
- 3.二进制文件和文本文件
- [4. 文件的打开与关闭](#4. 文件的打开与关闭)
-
- [4.1 流和标准流](#4.1 流和标准流)
-
- [4.1.1 流](#4.1.1 流)
- [4.1.2 标准流](#4.1.2 标准流)
- [4.2 文件指针](#4.2 文件指针)
- [4.3 文件的打开与关闭](#4.3 文件的打开与关闭)
-
- [4.3.1 fopen](#4.3.1 fopen)
- [4.3.2 fclose](#4.3.2 fclose)
- [5. 文件的顺序读写](#5. 文件的顺序读写)
-
- [5.1 fputc](#5.1 fputc)
- [5.2 fgetc](#5.2 fgetc)
- [5.3 feof 和 ferror](#5.3 feof 和 ferror)
- [5.4 fputs](#5.4 fputs)
- [5.5 fgets](#5.5 fgets)
- [5.6 fprinf](#5.6 fprinf)
- [5.7 fscanf](#5.7 fscanf)
- [5.8 fwrite](#5.8 fwrite)
- [5.9 fread](#5.9 fread)
- [5.10 对比一组函数:](#5.10 对比一组函数:)
-
- [5.10.1 sprintf](#5.10.1 sprintf)
- [5.10.2 sscanf](#5.10.2 sscanf)
- [6. 文件的随机读写](#6. 文件的随机读写)
-
- [6.1 fseek](#6.1 fseek)
- [6.2 ftell](#6.2 ftell)
- [6.3 rewind](#6.3 rewind)
- [7. 文件缓冲区](#7. 文件缓冲区)
-
- [7.1 fflush](#7.1 fflush)
- [8. 更新文件](#8. 更新文件)

1.为什么使用文件
文件的作用是持久化保存数据 。程序运行时数据通常存放在内存中,程序退出后内存会被回收,数据也会丢失。使用文件可以把数据保存下来 ,方便下次运行程序时继续读取和使用。
2.什么是文件
文件是磁盘(硬盘)上的文件,可以用来长期保存数据。
在程序设计中,文件按功能可分为两类:
2.1 程序文件
程序⽂件包括源程序⽂件(后缀为.c),⽬标⽂件(windows环境 后缀为.obj),可执⾏程序(windows环境 后缀为.exe)。

2.2 数据文件
保存程序运行时需要读取或者写入的数据,不一定是程序本身。
我们接下来主要讨论数据文件。
以前的数据输入输出多以终端为对象 ,比如从键盘输入、在显示器输出,数据文件 则是将信息保存到磁盘上 ,需要时再从磁盘读取到内存中使用。

2.3 文件名
文件名是文件的唯一标识,方便用户识别和引用文件。
文件名通常由三部分组成:
文件路径 + 文件名 + 文件后缀
例如:c:\code\test.txt
其中 c:\code是文件路径,test是文件名主干,.txt是文件名后缀。通常将整个文件标识简称为文件名。
我们了解完上述之后,再来了解一下文件的相对路径 以及绝对路径。
1. 绝对路径:
绝对路径是从磁盘根目录开始写的完整路径 ,不管当前程序在哪运行,都能明确找到文件。
例如:C:\Users\Byte\Desktop\data.txt。
含义是:C盘 → Users → Byte → Desktop → data.txt。
2.相对路径:
在了解相对路径的时候,我们要了解一下两个小点:
-
.-- 当前目录 -
..-- 上一级路径
相对路径是相对于当前工作目录来查找文件的。
下图中是项目目录 ,里面包含 .vcxproj、.c 源文件等项目文件 。VS 调试运行程序时,默认情况下常把该目录作为当前工作目录 。

如果当前工作目录是 C:\Users\Byte\Desktop,要访问同一目录下的 data.txt,可以写:data.txt 或 .\data.txt。
如果当前工作目录是 C:\Users\Byte,要访问桌面上的 data.txt,可以写:Desktop\data.txt 或 .\Desktop\data.txt。
如果文件在上一级目录,可以使用 ..\文件名。

3.二进制文件和文本文件
数据文件按组织形式分为文本文件 和二进制文件 。
二进制文件: 内存中的数据不经过转换,直接以二进制的形式存入外存文件。
文本文件: 数据在存储前转换为 ASCII 字符形式 , 在保存到外存文件中。
文件中数据的存储方式:
字符数据 通常以 ASCII 形式 存储。
数值型数据 既可以用 ASCII形式 存储,也可以用二进制形式来存储。
例如整数10000 :
-
用ASCII 形式存储:占 5个字节 (每一个字符占一个字节)
-
用二进制形式存储:占4个字节
因此,二进制存储通常比 ASCII 存储更节省空间。
演示代码:
c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "wb");
fwrite(&a, 4, 1, pf);//⼆进制的形式写到⽂件中
fclose(pf);
pf = NULL;
return 0;
}
补充一点,在VS上打开⼆进制⽂件:

找到对应的文件,点击添加

鼠标右键test.txt,点击打开方式


正常情况下可以看到:

为什么在二进制存储中是这个结果,由于这里涉及到大小端存储 的问题,相关知识点:C语言数据在内存中的存储, 我们通过画图来演示一下:

4. 文件的打开与关闭
文件操作通常分为三步:
-
打开文件
-
读取 / 写入文件
-
关闭文件
可以类比给瓶子装水和取水:
-
打开瓶子
-
装水 / 取水
-
关上瓶子
4.1 流和标准流
4.1.1 流
程序中的数据可能需要输出到文件、屏幕等外部设备,也可能需要从键盘、文件等外部设备读取数据。由于不同设备的输入输出方式不同,为了方便程序员操作,提出了流 的概念。
流可以理解为一条"传递字符数据的通道" 。在 C 语言中,对文件、屏幕、键盘等进行输入输出操作,本质上都是通过流来完成的。
一般情况下:先打开流,再读写数据,最后关闭流。

4.1.2 标准流
C 程序启动时,会默认打开 3 个标准流,所以我们平时用 scanf、printf 时,不需要手动打开流。
| 标准流 | 含义 | 常见作用 |
|---|---|---|
stdin |
标准输入流 | 通常从键盘读取数据 |
stdout |
标准输出流 | 通常向屏幕输出普通信息 |
stderr |
标准错误流 | 通常向屏幕输出错误信息 |
例如:scanf("%d",&a);本质上是从stdin中读取数据,printf("hello world\n")本质上是把数据输出到stdout中。
在 C 语言中,stdin、stdout、stderr 的类型都是:FILE*,也就是文件指针 。
因此,C 语言通过 FILE * 来维护和操作各种流,包括键盘、屏幕、文件等输入输出对象。
4.2 文件指针
文件指针 本质上是指向 FILE 类型 结构体的指针,通常写作:FILE* fp。
在 C 语言的缓冲文件系统中,每个被使用的文件都会在内存中对应一个文件信息区 ,用来保存文件的相关信息(文件名、文件状态、文件当前位置、读写方式、缓冲区信息),这些信息被保存在一个结构体变量中 ,该结构体类型由系统声明,名字叫:FILE。
VS2013编译环境提供的stdio.h头⽂件中有以下的⽂件类型申明:
c
struct _iobuf
{
char* _ptr;
int _cnt;
char* _base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char* _tmpfname;
};
typedef struct _iobuf FILE;
不同编译器中的 FILE 结构体内容可能不完全相同,例如 MSVC 和 GCC 中的实现细节会有差异,但作用基本一致,都是用来维护文件相关信息。
程序一般不会直接操作 FILE 结构体本身,而是通过文件指针来操作文件:FILE* pf,FILE* pf表示定义了一个文件指针变量 pf,它可以指向某个文件对应的 FILE 结构体。
也就是说,文件指针并不是直接指向 磁盘上的文件内容,而是指向内存中与该文件相关的文件信息区 。程序通过这个文件指针 ,就能间接找到并操作与它关联的文件 。


4.3 文件的打开与关闭
文件读写前要先打开文件 ,使用结束后要关闭文件 。
在 C 语言中:使用 fopen 打开文件,使用 fclose 关闭文件。打开文件后会返回一个 FILE* 类型的指针变量,这个文件指针用于建立程序与文件之间的联系,后续读写操作都通过它完成。
4.3.1 fopen
| 项目 | 说明 |
|---|---|
| 函数名 | fopen |
| 所属头文件 | stdio.h |
| 函数原型 | FILE *fopen(const char *filename, const char *mode); |
| 功能 | 打开 filename 指定的文件,并将该文件与一个流关联起来 |
参数 filename |
要打开的文件名,可以是相对路径 ,也可以是绝对路径 |
参数 mode |
文件打开方式,用来指定对文件进行读、写、追加等操作 |
| 返回值 | 打开成功:返回指向 FILE 对象的文件指针 |
| 返回值 | 打开失败:返回 NULL |
| 注意点 | 使用 fopen 后,应判断返回值是否为 NULL,防止文件打开失败 |
mode表⽰对打开的⽂件的操作⽅式:
| 文件使用方式 | 含义 | 如果指定文件不存在 |
|---|---|---|
"r"(只读) |
为了输入数据,打开一个已经存在的文本文件 | 出错 |
"w"(只写) |
为了输出数据,打开一个文本文件 | 建立一个新文件 |
"a"(追加) |
向文本文件添加数据 | 建立一个新文件 |
"rb"(只读) |
为了输入数据,打开一个二进制文件 | 出错 |
"wb"(只写) |
为了输出数据,打开一个二进制文件 | 建立一个新文件 |
"ab"(追加) |
向一个二进制文件尾添加数据 | 建立一个新文件 |
"r+"(读写) |
为了读和写,打开一个文本文件 | 出错 |
"w+"(读写) |
为了读和写,建立一个新的文件 | 建立一个新文件 |
"a+"(读写) |
打开一个文件,在文件尾进行读写 | 建立一个新文件 |
"rb+"(读写) |
为了读和写,打开一个二进制文件 | 出错 |
"wb+"(读写) |
为了读和写,新建一个新的二进制文件 | 建立一个新文件 |
"ab+"(读写) |
打开一个二进制文件,在文件尾进行读和写 | 建立一个新文件 |
代码演示:
c
#include <stdio.h>
int main()
{
FILE* pf1 = fopen(".\\test.txt", "r"); //写法1:相对路径 - 文件在同一目录下
FILE* pf2 = fopen("test.txt", "r"); //写法2:相对路径 - 文件在同一目录下
FILE* pf3 = fopen("..\\test2.txt", "r"); //写法2:相对路径 - 文件不在同一目录下(假设在上级目录下)
FILE* pf4 = fopen("C:\\2026C就业课语言代码\\2026-c-code\\2026_6_3\\2026_6_3\\test.txt", "r"); //写法3:绝对路径
if (pf1 == NULL)
{
perror("fopen");
return 1;
}
else
{
printf("打开文件成功\n");
}
if (pf2 == NULL)
{
perror("fopen");
return 1;
}
else
{
printf("打开文件成功\n");
}if (pf3 == NULL)
{
perror("fopen");
return 1;
}
else
{
printf("打开文件成功\n");
}if (pf4 == NULL)
{
perror("fopen");
return 1;
}
else
{
printf("打开文件成功\n");
}
//读文件
//关闭文件
fclose(pf1);
fclose(pf2);
fclose(pf3);
fclose(pf4);
pf1 = NULL;
pf2 = NULL;
pf3 = NULL;
pf4 = NULL;
return 0;
}

在 C 语言字符串中,\ 是转义字符的开始。如果写 ".\test.txt",其中的 \t 会被解析为制表符。因此要表示路径中的反斜杠,应写成 ".\\test.txt";也可以使用 ./test.txt。转义字符相关知识点链接:初识C语言。
4.3.2 fclose
| 项目 | 内容 |
|---|---|
| 函数名 | fclose |
| 所属头文件 | stdio.h |
| 函数原型 | int fclose(FILE *stream); |
| 功能 | 关闭 stream 指向的文件,并取消文件与流之间的关联 |
| 参数 | stream:指向要关闭的流的 FILE * 文件指针 |
| 返回值 | 关闭成功返回 0,关闭失败返回 EOF |
fclose函数的主要作用:
| 作用 | 说明 |
|---|---|
| 关闭文件 | 结束文件的使用 |
| 解除关联 | 取消文件和流之间的联系 |
| 刷新缓冲区 | 将输出缓冲区中还没写入文件的数据写入文件 |
| 释放资源 | 释放与该流相关的内存缓冲区等资源 |
| 丢弃输入缓冲区 | 未读取的输入缓冲区内容会被丢弃 |
代码演示:
c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
FILE* fp = fopen("test.txt", "r"); // 以 "r" 的形式打开文件,如果文件不存在,则打开失败
if (fp == NULL)
{
perror("fopen");
return 1;
}
printf("打开文件成功,可以对文件进行操作\n");
fclose(fp); // 不再使用文件时,需要关闭文件
fp = NULL; // 将指针置为 NULL,避免成为野指针
return 0;
}
5. 文件的顺序读写
在进行文件读写时,会涉及下面的函数:
| 函数名 | 功能 | 适用于 |
|---|---|---|
fgetc |
从输入流中读取一个字符 | 所有输入流 |
fputc |
向输出流中写入一个字符 | 所有输出流 |
fgets |
从输入流中读取一个字符串 | 所有输入流 |
fputs |
向输出流中写入一个字符串 | 所有输出流 |
fscanf |
从输入流中读取带格式的数据 | 所有输入流 |
fprintf |
向输出流中写入带格式的数据 | 所有输出流 |
fread |
从输入流中读取一块数据 | 文件输入流 |
fwrite |
向输出流中写入一块数据 | 文件输出流 |
其中,所有输入流 一般包括:标准输入流 stdin、文件输入流、其他输入流。像 fgetc、fgets、fscanf 这类读取函数,既可以从键盘读取,也可以从文件中读取;
所有输出流 一般包括:标准输出流 stdout、文件输出流、其他输出流。像 fputc、fputs、fprintf 这类写入函数,既可以向屏幕输出,也可以向文件中写入。
5.1 fputc
| 项目 | 内容 |
|---|---|
| 函数名 | fputc |
| 所属头文件 | stdio.h |
| 函数原型 | int fputc(int character, FILE *stream); |
| 功能 | 向 stream 指向的输出流中写入一个字符 |
参数 character |
要写入的字符,虽然类型是 int,但实际写入的是一个字符 |
参数 stream |
指向输出流的 FILE * 文件指针,可以是文件流 ,也可以是 stdout |
| 写入位置 | 字符会写入当前文件位置指示器所指向的位置 |
| 写入后变化 | 写入成功后,文件位置指示器 会自动向后移动一个字符位置 |
| 返回值:成功 | 返回写入的字符 ,返回类型为 int |
| 返回值:失败 | 返回 EOF(end of file),通常是 -1 |
| 错误检查 | 如果写入失败,会设置错误标志,可用 ferror(stream) 检查 |
这里要弄清光标 和文件位置指示器的概念:
| 概念 | 说明 |
|---|---|
| 光标 | 通常是屏幕或编辑器中可见的位置 |
| 文件位置指示器 | 文件流内部维护的当前读写位置,看不见 |
| 作用 | 决定下一次从哪里读、往哪里写 |
| 自动移动 | 读写操作后会根据实际读写的字符数自动移动 |
| 手动控制 | 可用 fseek、rewind 改变位置,用 ftell 获取当前位置 |
代码演示:
c
#include <stdio.h>
int main()
{
//以w形式打开文件,才能正确的写文件
FILE* pf = fopen(".\\test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//代码1:一次性写入几个字符
//写入文件
//fputc('x', pf);
//fputc('y', pf);
//fputc('z', pf);
//在test.txt中写入xyz
//代码2:循环写入字符
for (char x = 'a'; x <= 'z'; x++)
{
fputc(x, stdout); //向屏幕中输出a ~ z
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
5.2 fgetc
| 项目 | 内容 |
|---|---|
| 函数名 | fgetc |
| 所属头文件 | stdio.h |
| 函数原型 | int fgetc(FILE *stream); |
| 功能 | 从 stream 指向的输入流中读取一个字符 |
参数 stream |
FILE * 类型的文件指针,可以是 stdin,也可以是文件输入流 |
| 适用于 | 所有输入流,例如标准输入流 stdin、文件输入流 |
| 读取位置 | 从文件位置指示器当前指向的位置读取字符 |
| 读取后变化 | 读取成功后,文件位置指示器自动向后移动一个字符位置 |
| 返回值:成功 | 返回读取到的字符 ,返回类型为 int |
| 返回值:文件结束 | 如果读到文件末尾,返回 EOF,并设置文件结束标志,可用 feof(stream) 检查 |
| 返回值:读取错误 | 如果读取出错,也返回 EOF,并设置错误标志,可用 ferror(stream) 检查 |
| 注意点 | 因为失败和文件结束都会返回 EOF,所以需要用 feof() 和 ferror() 区分原因 |
| 代码演示: |
c
#include <stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
////读取文件-方式1
//int ch = fgetc(pf);
//printf("%c\n", ch);
//ch = fgetc(pf);
//printf("%c\n", ch);
//ch = fgetc(pf);
//printf("%c\n", ch);
//ch = fgetc(pf);
//printf("%c\n", ch);
//读取文件-方式2
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
printf("%c", ch);
printf("\n");
if (feof(pf))
printf("文件正常读取结束\n");
if (ferror(pf))
printf("文件读写错误\n");
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}

5.3 feof 和 ferror
| 函数 | 函数原型 | 作用 | 检测对象 | 返回值含义 |
|---|---|---|---|---|
feof |
int feof(FILE *stream); |
检测文件是否到达文件末尾 | 文件结束指示符 | 若文件结束指示符已被设置,返回非 0;否则返回 0 |
ferror |
int ferror(FILE *stream); |
检测文件读/写过程中是否发生错误 | 错误指示符 | 若错误指示符已被设置,返回非 0;否则返回 0 |
代码演示:
c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
//假设test.txt文件中存放abcdef
int main()
{
FILE* fp = fopen("test.txt", "r");
if (fp == NULL)
{
perror("fopen\n");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
int c = fgetc(fp);
if (c == EOF)
{
if (feof(fp))
printf("遇到文件末尾了\n");
else if (ferror(fp))
printf("读取发生了错误\n");
}
else
{
fputc(c, stdout); //使用fputc在标准输出流上打印字符
}
}
//不再使用文件时,需要关闭文件
fclose(fp);
fp = NULL; //将指针置为NULL,避免成为野指针。
return 0;
}
注意:文件打开模式与实际操作不匹配,会导致 I/O 错误。用写模式打开却读取,会发生读取错误;用读模式打开却写入,会发生写入错误,看如下代码:
c
#include <stdio.h>
//以写的形式打开文件后,再去读文件,就会发生错误
int main()
{
FILE* fp = fopen("test.txt", "w");
if (fp == NULL)
{
perror("fopen");
return 1;
}
int c = fgetc(fp);//读文件
if (c == EOF)
{
if (feof(fp))
printf("遇到文件末尾了\n");
else if (ferror(fp))
{
printf("读写文件发生了错误\n"); //执行语句
}
}
else
{
fputc(c, stdout); //使用fputc在标准输出流上打印字符
}
//不再使用文件时,需要关闭文件
fclose(fp);
fp = NULL; //将指针置为NULL,避免成为野指针。
return 0;
}
5.4 fputs
| 项目 | 说明 |
|---|---|
| 函数名 | fputs |
| 函数原型 | int fputs(const char *str, FILE *stream); |
| 所属头文件 | #include <stdio.h> |
| 功能 | 将字符串 str 写入到 stream 指定的流中 |
参数 str |
指向要写入的字符串,字符串必须以 \0 结尾 |
参数 stream |
FILE * 类型的文件流指针,表示要写入的目标流 |
| 写入内容 | 写入字符串中的有效字符 ,但不写入字符串末尾的 \0 |
| 适用对象 | 可用于文件流,也可用于标准输出流 stdout |
| 成功返回值 | 返回一个非负整数 |
| 失败返回值 | 返回 EOF,通常为 -1 |
| 错误检测 | 写入失败时会设置流的错误指示器,可使用 ferror(stream) 检查错误原因 |
代码演示:
c
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fputs("hello ", stdout); //使用 fputs 向标准输出流 stdout 写入字符串
fputs("world", pf); //使用 fputs 向 pf 指向的文件流中写入字符串
fclose(pf);
pf = NULL;
return 0;
}


5.5 fgets
| 项目 | 说明 |
|---|---|
| 函数名 | fgets |
| 函数原型 | char *fgets(char *str, int num, FILE *stream); |
| 所属头文件 | #include <stdio.h> |
| 功能 | 从 stream 指定的输入流中读取字符串 ,并存放到 str 指向的字符数组中 |
参数 str |
字符数组指针,用来保存读取到的字符串 |
参数 num |
最多读取的字符数量,包含字符串结尾的 \0 |
| 实际最多读取字符数 | 最多读取 num - 1 个有效字符,因为最后要留一个位置存放 \0 |
参数 stream |
输入流指针,可以是文件流 ,也可以是标准输入流 stdin |
| 读取停止条件 | 读到换行符 \n、读到文件末尾 EOF、达到 num - 1 个字符、发生读取错误 |
| 是否保留换行符 | 如果在读取范围内读到了 \n,会把 \n 一起存入 str |
是否自动添加 \0 |
会自动在读取到的字符串末尾添加 \0 |
| 成功返回值 | 返回 str 指针 |
| 失败返回值 | 返回 NULL |
| 文件末尾情况 | 如果尝试读取时已经遇到文件末尾 ,返回 NULL,可用 feof(stream) 判断 |
| 读取错误情况 | 如果发生读取错误 ,返回 NULL,可用 ferror(stream) 判断 |
代码演示:
c
#include <stdio.h>
int main()
{
FILE* fp = fopen("test.txt", "r");
if (fp == NULL)
{
perror("fopen");
return 1;
}
//读文件
char arr[20] = "----------";
while (fgets(arr, 20, fp) != NULL)
{
printf("%s", arr);
}
//不再使用文件时,需要关闭文件
fclose(fp);
fp = NULL; //将指针置为NULL,避免成为野指针。
return 0;
}

5.6 fprinf
| 项目 | 说明 |
|---|---|
| 函数名 | fprintf |
| 函数原型 | int fprintf(FILE *stream, const char *format, ...); |
| 所属头文件 | #include <stdio.h> |
| 功能 | 将格式化数据写入到指定的文件流中 |
与 printf 的关系 |
fprintf 与 printf 类似,但 printf 默认输出到屏幕,而 fprintf 可以输出到指定流 |
参数 stream |
指向 FILE 对象的指针,表示要写入的目标流 |
参数 format |
格式化字符串,包含普通文本和格式说明符,如 %d、%s、%f 等 |
参数 ... |
可变参数列表,用来提供与格式说明符对应的数据 |
| 输出目标 | 可以是文件流,也可以是标准输出流 stdout、标准错误流 stderr 等 |
| 成功返回值 | 返回成功写入的字符总数 ,返回值为非负数 |
| 失败返回值 | 返回负值 |
| 错误检测 | 写入失败时,会设置对应流的错误指示器,可以使用 ferror(stream) 检测 |
代码演示:
c
#include <stdio.h>
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s = { "zhangsan", 23, 100.0f };
FILE* fp = fopen("test.txt", "w");
if (fp == NULL)
{
perror("fopen");
return 1;
}
//写文件
fprintf(stdout, "名字:%s 年龄:%d 成绩:%.2f\n", s.name, s.age, s.score); // 向标准输出流 stdout 输出格式化数据
fprintf(fp, "名字:%s 年龄:%d 成绩:%.2f\n", s.name, s.age, s.score); // 向 fp 指向的文件流中写入格式化数据
//不再使用文件时,需要关闭文件
fclose(fp);
fp = NULL; //将指针置为NULL,避免成为野指针。
return 0;
}

5.7 fscanf
| 项目 | 说明 |
|---|---|
| 函数名 | fscanf |
| 函数原型 | int fscanf(FILE *stream, const char *format, ...); |
| 所属头文件 | #include <stdio.h> |
| 功能 | 从指定文件流中读取格式化数据 |
与 scanf 的关系 |
scanf 默认从键盘 stdin 读取;fscanf 可以从指定流读取,如文件流、stdin 等 |
参数 stream |
指向 FILE 对象的指针,表示要读取数据的输入流 |
参数 format |
格式化字符串,用来规定读取数据的格式 ,如 %d、%f、%s 等 |
参数 ... |
可变参数列表,通常是变量地址 ,用来保存读取到的数据 |
| 成功返回值 | 返回成功读取并赋值的数据个数 |
| 读取失败返回值 | 如果在读取任何数据前就遇到文件末尾 或读取错误 ,返回 EOF |
| 格式不匹配 | 如果格式与数据不匹配,读取会停止,返回已成功读取的数据个数 ,可能为 0 |
| 文件末尾检测 | 可使用 feof(stream) 判断是否到达文件末尾 |
| 读取错误检测 | 可使用 ferror(stream) 判断是否发生读取错误 |
代码演示:
c
#include <stdio.h>
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s = { 0 };
FILE* fp = fopen("test.txt", "r");
if (fp == NULL)
{
perror("fopen");
return 1;
}
//读文件
//scanf("%s %d %f", s.name, &(s.age), &(s.score));
// 从 fp 指向的文件流中按指定格式读取数据,保存到结构体变量 s 中
// 注意:%s 对应字符数组名 s.name,数组名本身就是地址;%d 和 %f 对应变量地址 &s.age、&s.score
fscanf(fp, "名字:%s 年龄:%d 成绩:%f", s.name, &(s.age), &(s.score));
fprintf(stdout, "名字:%s 年龄:%d 成绩:%.2f", s.name, s.age, s.score);
//不再使用文件时,需要关闭文件
fclose(fp);
fp = NULL; //将指针置为NULL,避免成为野指针。
return 0;
}

5.8 fwrite
| 项目 | 说明 |
|---|---|
| 函数名 | fwrite |
| 函数原型 | size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream); |
| 所属头文件 | #include <stdio.h> |
| 功能 | 将数据块写入到 stream 指定的文件流中 |
| 写入方式 | 以二进制形式写入 |
参数 ptr |
指向要写入数据的内存地址 |
参数 size |
每个数据项的大小,单位是字节 |
参数 count |
要写入的数据项个数 |
参数 stream |
指向 FILE 类型结构体的文件流指针 |
| 返回值类型 | size_t |
| 成功返回值 | 返回实际成功写入的数据项个数 |
| 失败返回值 | 如果写入失败,返回值可能小于 count |
| 错误检测 | 可使用 ferror(stream) 判断是否发生写入错误 |
很多人可能不知道数据项是什么 ,在fwrite中,数据项 可以理解为:数据项就是你规定的"一份数据" ,在 fwrite 里,不是它自己决定什么叫"一份数据",而是你通过 size 和 count 告诉它,每份数据有多大,要写几份数据。
代码演示:
c
#include <stdio.h>
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s = {.name = "zhangsan", .score = 150.0f , .age = 20}; //不按顺序初始化
FILE* fp = fopen("test.txt", "wb"); //应该用二进制写入的方式打开文件
if (fp == NULL)
{
perror("fopen");
return 1;
}
fwrite(&s, sizeof(struct Stu), 1, fp);
//不再使用文件时,需要关闭文件
fclose(fp);
fp = NULL; //将指针置为NULL,避免成为野指针。
return 0;
}
5.9 fread
| 项目 | 说明 |
|---|---|
| 函数名 | fread |
| 函数原型 | size_t fread(void *ptr, size_t size, size_t count, FILE *stream); |
| 所属头文件 | #include <stdio.h> |
| 功能 | 从 stream 指定的文件流中读取数据块,并保存到 ptr 指向的内存空间中 |
| 读取方式 | 通常用于二进制方式读取 |
参数 ptr |
指向内存空间的指针,用来存放从文件中读取到的数据 |
参数 size |
每个数据项的大小,单位是字节 |
参数 count |
要读取的数据项数量 |
参数 stream |
指向 FILE 类型对象的文件流指针,表示从哪个文件流中读取 |
| 返回值类型 | size_t |
| 成功返回值 | 返回实际成功读取的数据项个数 |
| 读取不足 | 如果返回值小于 count,可能是读到文件末尾或发生读取错误 |
| 文件末尾检测 | 可使用 feof(stream) 判断是否到达文件末尾 |
| 读取错误检测 | 可使用 ferror(stream) 判断是否发生读取错误 |
代码演示:
c
#include <stdio.h>
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s = { 0 };
FILE* fp = fopen("test.txt", "rb"); //以二进制方式读取文件
if (fp == NULL)
{
perror("fopen");
return 1;
}
fread(&s, sizeof(struct Stu), 1, fp);
printf("%s %d %.2f\n", s.name, s.age, s.score);
//不再使用文件时,需要关闭文件
fclose(fp);
fp = NULL; //将指针置为NULL,避免成为野指针。
return 0;
}

5.10 对比一组函数:

5.10.1 sprintf
| 项目 | 说明 |
|---|---|
| 函数名 | sprintf |
| 函数原型 | int sprintf(char *str, const char *format, ...); |
| 所属头文件 | #include <stdio.h> |
| 功能 | 将格式化的数据写入字符数组,也就是生成一个字符串 |
与 printf 的关系 |
printf 是把格式化数据输出到屏幕;sprintf 是把格式化数据写入字符串 |
参数 str |
指向字符数组的指针,用来保存生成后的字符串 |
参数 format |
格式化字符串,用来指定数据的输出格式,如 %d、%f、%s 等 |
参数 ... |
可变参数列表,提供与格式说明符对应的数据 |
| 写入位置 | 写入到用户指定的字符数组中 |
| 成功返回值 | 返回写入字符数组中的字符个数,不包括结尾的 \0 |
| 失败返回值 | 返回负值 |
是否自动添加 \0 |
会在生成的字符串末尾自动添加 \0 |
| 使用风险 | 如果 str 指向的字符数组空间不够,可能造成缓冲区溢出,最终导致程序崩溃 |
代码演示:
c
//代码1
#include <stdio.h>
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s = { .score = 149.0f,.name = "zhangsan",.age = 21 };
char str[30] = { 0 };
sprintf(str, "%s %d %.2f", s.name, s.age, s.score);
printf("%s\n", str);
return 0;
}
//代码2
#include <stdio.h>
int main()
{
int num = 12345;
char str_num[10] = { 0 };
sprintf(str_num, "%d", num);
printf("%s\n", str_num);
return 0;
}
5.10.2 sscanf
| 项目 | 说明 |
|---|---|
| 函数名 | sscanf |
| 函数原型 | int sscanf(const char *str, const char *format, ...); |
| 所属头文件 | #include <stdio.h> |
| 功能 | 从字符串 str 中按照指定格式读取数据 |
与 scanf 的关系 |
scanf 从键盘输入读取;sscanf 从字符串中读取 |
参数 str |
要解析的源字符串,也就是数据来源 |
参数 format |
格式化字符串,用来规定如何解析数据 ,如 %d、%f、%s 等 |
参数 ... |
可变参数列表,通常传入变量地址 ,用来保存解析出来的数据 |
| 成功返回值 | 返回成功解析并赋值的数据个数 |
| 失败返回值 | 如果解析失败或没有成功匹配任何数据,返回 EOF,通常是 -1 |
| 常见用途 | 从已有字符串中提取整数、浮点数、字符串等结构化数据 |
代码演示:
c
#include <stdio.h>
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s = { .score = 149.0f,.name = "zhangsan",.age = 21 };
char str[30] = { 0 };
sprintf(str, "%s %d %.2f", s.name, s.age, s.score);
printf("%s\n", str);
//从str中解析出一个结构体数据
struct Stu t = { 0 };
sscanf(str, "%s %d %f\n", t.name, &(t.age), &(t.score));
fprintf(stdout, "%s %d %.2f\n", t.name, t.age, t.score);
return 0;
}
6. 文件的随机读写
6.1 fseek
| 项目 | 内容 |
|---|---|
| 函数名 | fseek |
| 所属头文件 | stdio.h |
| 函数原型 | int fseek(FILE *stream, long int offset, int origin); |
| 主要功能 | 根据文件指针的位置和偏移量,移动文件内部的位置指针,也就是改变文件内容的光标 |
| 作用 | 用于文件的随机读写,可以跳到文件中的指定位置进行读取或写入 |
| 返回值 | 成功返回 0,失败返回非 0,通常是 -1 |
| 参数 | 含义 | 说明 |
|---|---|---|
stream |
文件指针 | 指向已经打开的文件,例如 fp |
offset |
偏移量 | 表示从指定起点移动多少个字节,可以是正数、负数或 0 |
origin |
起始位置 | 表示从哪里开始移动文件指针 |
orignal有三个取值:
| 取值 | 含义 | 大白话理解 |
|---|---|---|
SEEK_SET |
文件开头位置 | 从文件最开始的位置算起 |
SEEK_CUR |
文件当前位置 | 从当前文件指针所在位置算起 |
SEEK_END |
文件末尾位置 | 从文件最后的位置算起 |
使用注意事项:
| 类型 | 说明 |
|---|---|
| 二进制文件 | fseek 行为比较稳定,可以精确移动到某个字节位置 |
| 文本文件 | 某些环境下不一定完全稳定,因为换行符可能会发生转换 |
| 文本文件推荐写法 | offset 设置为 0,origin 设置为 SEEK_SET、SEEK_CUR 或 SEEK_END |
和 ftell 配合 |
可以先用 ftell() 获取当前位置,再用 fseek() 回到该位置 |
| 使用前提 | 文件必须已经成功打开,否则不能使用 fseek |
代码演示:
c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
//假设test.txt文件中存储了hello world
FILE* fp = fopen("test.txt", "r");
if (fp == NULL)
{
perror("fopen");
return 1;
}
int c = fgetc(fp);
fputc(c, stdout); //h
// fgetc 读取一个字符后,文件位置指示器(光标)自动向后移动 1 个字符位置
// 定位文件位置指示器
//fseek(fp, 2, SEEK_CUR); // 以当前位置为起点,向后移动 2 个字符
//fseek(fp, 4, SEEK_SET); // 以文件开头为起点,向后移动 4 个字符
fseek(fp, -4, SEEK_END); // 以文件末尾为起点,向前移动 4 个字符
c = fgetc(fp);
fputc(c, stdout);// 情况1读到 l,情况2读到 o,情况3读到 o
//不再使用文件时,需要关闭文件
fclose(fp);
fp = NULL; //将指针置为NULL,避免成为野指针。
return 0;
}

6.2 ftell
| 项目 | 内容 |
|---|---|
| 函数名 | ftell |
| 所属头文件 | stdio.h |
| 函数原型 | long int ftell(FILE *stream); |
| 主要功能 | 返回当前文件位置指示器相对于文件起始位置的偏移量 |
| 作用对象 | 已经打开的文件 |
| 常见用途 | 获取当前读写位置、记录文件位置、配合 fseek 恢复到指定位置 |
参数说明:
| 参数 | 含义 | 说明 |
|---|---|---|
stream |
文件指针 | 指向一个已经打开的文件,例如 fp |
返回值:
| 情况 | 返回值 | 含义 |
|---|---|---|
| 成功 | 当前偏移量 | 返回当前位置相对于文件开头的偏移量,单位通常是字节 |
| 失败 | -1L |
说明发生错误,例如文件流无效或文件不支持定位 |
使用注意事项:
| 文件类型 | 说明 |
|---|---|
| 二进制文件 | ftell 返回值通常表示从文件开头算起的精确字节数,比较可靠 |
| 文本文件 | 由于不同系统对换行符处理不同,例如 Windows 中 \r\n 和 Linux 中 \n 不一样,ftell 返回值不一定能简单理解成字符个数 |
| 推荐用法 | 在文本文件中,ftell 通常和 fseek 配合,用来记录和恢复位置 |
代码演示:
c
#include <stdio.h>
int main()
{
//假设test.txt文件中存储了hello world
FILE* fp = fopen("test.txt", "r");
if (fp == NULL)
{
perror("fopen");
return 1;
}
int c = fgetc(fp);
fputc(c, stdout); //h
int pos = ftell(fp);//1
// fgetc 读取一个字符后,文件位置指示器(光标)自动向后移动 1 个字符位置
// 定位文件位置指示器
fseek(fp, 2, SEEK_CUR); // 以当前位置为起点,向后移动 2 个字符
c = fgetc(fp);
fputc(c, stdout); //l
fseek(fp, pos, SEEK_SET); // 以文件开头为起点,向后移动 4 个字符
c = fgetc(fp);
fputc(c, stdout); //e
//不再使用文件时,需要关闭文件
fclose(fp);
fp = NULL; //将指针置为NULL,避免成为野指针。
return 0;
}

6.3 rewind
| 项目 | 内容 |
|---|---|
| 函数名 | rewind |
| 所属头文件 | #include <stdio.h> |
| 函数原型 | void rewind(FILE *stream); |
| 函数功能 | 将文件位置指示器重新定位到文件开头 |
| 参数说明 | stream:指向已打开文件的文件指针 |
| 返回值 | 无返回值,即 void |
| 主要作用 | 让文件从头开始重新读写 |
| 额外作用 | 清除文件的错误标志和文件结束标志 |
与 fseek 的区别 |
fseek(fp, 0, SEEK_SET) 只移动文件位置指示器;rewind(fp) 不仅移动到文件开头,还会清除错误标志和文件结束标志 |
| 注意事项 | rewind 没有返回值,不能直接判断是否执行成功;如果需要判断是否定位成功,建议使用 fseek |
代码演示:
c
#include <stdio.h>
int main()
{
//假设test.txt文件中存储了hello world
FILE* fp = fopen("test.txt", "r");
if (fp == NULL)
{
perror("fopen");
return 1;
}
int c = fgetc(fp);
fputc(c, stdout); //h
// fgetc 读取一个字符后,文件位置指示器(光标)自动向后移动 1 个字符位置
// 定位文件位置指示器
fseek(fp, 2, SEEK_CUR); // 以当前位置为起点,向后移动 2 个字符
c = fgetc(fp);
fputc(c, stdout); //l
rewind(fp); //让文件指示器回到起始位置
c = fgetc(fp);
fputc(c, stdout); //h
//不再使用文件时,需要关闭文件
fclose(fp);
fp = NULL; //将指针置为NULL,避免成为野指针。
return 0;
}

7. 文件缓冲区
文件缓冲区是程序和磁盘文件之间的一块内存临时区域。程序读写文件时,数据通常不会马上直接和磁盘交互,而是先经过缓冲区。
写文件时:
程序数据先进入缓冲区,等缓冲区满了、调用 fflush、调用 fclose 或程序正常结束时,再真正写入磁盘。
读文件时:
系统通常先从磁盘一次性读入一批数据到缓冲区,程序再从缓冲区中逐个读取。
这样做的好处是:减少磁盘访问次数,提高文件读写效率。

7.1 fflush
| 项目 | 内容 |
|---|---|
| 函数名 | fflush |
| 所属头文件 | #include <stdio.h> |
| 函数原型 | int fflush(FILE *stream); |
| 函数功能 | 强制刷新指定流的缓冲区,确保缓冲区中的数据被写入目标设备或文件 |
| 参数说明 | stream:指向文件流的指针,如 stdout、文件指针等 |
| 用于输出流 | 将缓冲区中还没有写入的数据立即写入文件或屏幕 |
| 用于输入流 | 行为不标准,不同编译器结果可能不同,不建议使用 |
参数为 NULL |
刷新所有已经打开的输出流 |
| 返回值 | 成功返回 0,失败返回 EOF |
| 常见用法 | fflush(stdout); 用来立即刷新屏幕输出 |
| 注意事项 1 | fflush 主要用于输出流或更新流 |
| 注意事项 2 | 不建议使用 fflush(stdin); 清空输入缓冲区,因为它不是标准 C 语言用法 |
| 注意事项 3 | 程序正常结束时会自动刷新缓冲区,但程序异常退出时缓冲区数据可能丢失 |
代码演示:
c
#include <stdio.h>
#include <windows.h>
// VS2022 WIN11环境测试
int main()
{
FILE* pf = fopen("test.txt", "w");
fputs("abcdef", pf); // 先将数据存入输出缓冲区
printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf); // 刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
Sleep(10000);
fclose(pf);
// 注:fclose在关闭文件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
我们从中得出结论:因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题。
8. 更新文件
| 行为 | "r+" |
"w+" |
"a+" |
|---|---|---|---|
| 含义 | 读写模式 | 读写模式 | 追加读写模式 |
| 文件不存在时 | 打开失败 | 自动创建新文件 | 自动创建新文件 |
| 文件存在时 | 保留原有内容 | 清空原有内容 | 保留原有内容 |
| 初始文件指针位置 | 文件开头 | 文件开头 | 文件末尾 |
| 写入是否覆盖原数据 | 是,可在指定位置覆盖 | 是,原内容先被清空,再从头写入 | 否,默认在文件末尾追加数据 |
| 典型用途 | 修改文件的部分内容 | 创建新文件或完全重写文件 | 在文件末尾追加内容,比如写日志 |
在文件以 r+、w+、a+ 这类可读可写 模式打开时,读写操作不能随便连续切换,中间通常要做一次刷新或重新定位 。
写完后如果要继续读 ,需要先调用 fflush() 刷新缓冲区,或者使用 fseek()、rewind() 重新定位文件位置指示器。这样可以保证刚刚写入的数据已经同步到文件中,并且读取位置是确定的。
读完后如果要继续写 ,在写入之前需要先使用 fseek() 或 rewind() 重新定位文件位置指示器 。这样可以明确接下来要写入的位置,避免写入位置不确定。
代码演示:
c
#include <stdio.h>
int main()
{
FILE* fp = fopen("test.txt", "w+");
if (fp == NULL)
{
perror("fopen for w+");
return 1;
}
//写abcdefghi到文件中
fputs("abcdefghi", fp);
//刷新缓冲区,保证数据写入文件
fflush(fp);
//要读取数据b字符,先定位文件指针
fseek(fp, 1, SEEK_SET);
int ch = fgetc(fp);//读取字符
printf("%c\n", ch); //b
//abcdefghi
//在b的位置开始写入hello
fseek(fp, -1, SEEK_CUR);
//解释:因为前面读取一个字符后,文件指示器现在指向了c,需要从当前位置退回一个字符
fputs("hello", fp);
//ahelloghi
//
//关闭文件
fclose(fp);
fp = NULL;
return 0;
}
