目录
[5.1 顺序读写函数汇总表](#5.1 顺序读写函数汇总表)
[5.2 顺序读写函数详解](#5.2 顺序读写函数详解)
[5.2.1 fgetc 字符输入(读)](#5.2.1 fgetc 字符输入(读))
[5.2.2 fputc 字符输出(写)](#5.2.2 fputc 字符输出(写))
[5.2.3 fgets 按行读取字符串](#5.2.3 fgets 按行读取字符串)
[5.2.4 fputs 写入字符串](#5.2.4 fputs 写入字符串)
[5.2.5 fscanf 格式化读取](#5.2.5 fscanf 格式化读取)
[5.2.6 fprintf 格式化写入](#5.2.6 fprintf 格式化写入)
[5.2.7 fread 二进制数据读取](#5.2.7 fread 二进制数据读取)
[5.2.8 fwrite 二进制数据写入](#5.2.8 fwrite 二进制数据写入)
[6.1 scanf/fscanf/sscanf](#6.1 scanf/fscanf/sscanf)
[6.1.1 sscanf 函数](#6.1.1 sscanf 函数)
[6.2.1 sprintf 函数](#6.2.1 sprintf 函数)
[7.1 fseek 函数](#7.1 fseek 函数)
[7.2 ftell 函数](#7.2 ftell 函数)
[7.3 rewind 函数](#7.3 rewind 函数)
[8.1 feof 函数](#8.1 feof 函数)
[8.2 ferror 函数](#8.2 ferror 函数)
一、为什么使用文件?
程序运行时产生的数据默认存放在内存 中,内存属于临时存储空间:一旦程序结束运行、电脑断电,内存会被系统回收,所有数据会直接丢失,下次重新运行程序无法读取之前的数据。 而文件可以把数据存储在硬盘、U 盘等外部持久化存储设备里,即使程序关闭、设备断电,数据也不会消失,实现数据持久化保存,需要时可以再次读取使用,这就是使用文件的核心原因
二、什么是文件?
存放在硬盘等磁盘设备上的数据集合就是文件。从程序设计的功能角度划分,文件分为程序文件 和数据文件两类。
2.1程序文件
程序文件包括:源程序文件(.c为后缀);目标文件(Windows下.obj为后缀,Linux下.o为后缀);可执行程序文件(Windows下后缀为.exe,Linux 下没有强制后缀,一般默认生成a.out,依靠文件执行权限来判断是否为可执行文件)
2.2数据文件
文件内容不一定是程序代码,也可能是程序运行过程中读写处理的数据。 可以是程序读取的输入数据,也可以是程序运行后输出保存的结果数据。以往的数据交互是键盘输入、显示器输出,而数据文件是在磁盘中读写数据,实现数据持久化。
2.3文件名
文件标识就是文件名,方便起见,我们将文件标识简称为文件名,文件名一般指文件名主干+文件后缀
完整文件标识 = 文件路径 + 文件名主干(主文件名) + 文件后缀(扩展名)
例如:c:\code\test.txt
c:\code\是文件路径 test是文件名主干 .txt是文件后缀
文件路径 :c:\code\ 作用:指明文件在磁盘上的存放位置,用来定位文件所在文件夹。
文件名主干(主文件名) :test 作用:用户自定义的文件名称,用来区分同文件夹下不同文件,是文件的核心标识。
文件后缀(扩展名) :.txt 作用:用来标识文件类型,操作系统根据后缀选择打开该文件的程序;.txt代表纯文本文件。
三、文本文件和二进制文件
根据数据的组织形式,数据文件被称为文本文件或者二进制文件
3.1文本文件
内存中的数据先转换成 ASCII 字符,再存储到外存,文件里保存的是每个字符的 ASCII 编码
- 存储规则:所有内容都当作字符处理,数字、字母、符号都对应一个 ASCII 码;
- 打开方式:可以用记事本、VS 等文本编辑器直接看懂内容;
- 特点:读写时需要做二进制 ↔ ASCII的编码转换,会消耗少量时间;占用空间通常更大。
3.2二进制文件
数据在内存中以二进制形式存储,如果不做任何编码转换,直接原样复制保存到外存的文件中,就是二进制文件
- 存储规则:完全沿用数据在内存中的二进制存储格式;
- 打开方式:用记事本打开会显示乱码,必须用对应专业软件解析;
- 特点:无编码转换,读写速度更快;相同数值数据占用存储空间更小。
3.3两种文件的比较
例如我们要存储数据10000,如果用文本文件的形式存,那么我们将10000的每一位都当做一个字符,转化为对应的ASCII码值,一个字符占一个字节,就需要5个字节;
但如果我们用二进制文件的形式存10000这个int类型的数据,不需要进行编码转换,直接把内存中 int 的 4 字节二进制原样写入磁盘,我们就只需要4个字节就可以了,比文本文件占用的空间小一个字节
文本文件可用记事本直接打开看到10000;二进制文件用记事本打开会显示乱码,必须用程序按照 int 类型读取才能还原数据

下面我们来看看真实的在磁盘中是如何储存10000这个数据的:
二进制文件形式:
cpp
#include <stdio.h>
int main()
{
int a = 10000;
// 以二进制只写模式打开文件 test.txt
FILE* pf = fopen("test.txt", "wb");
// 将变量a的4字节二进制数据写入文件
fwrite(&a, 4, 1, pf);
// 关闭文件
fclose(pf);
// 置空文件指针,防止野指针
pf = NULL;
return 0;
}
运行截图:
使用二进制编辑器以二进制文本文件方式打开test.txt:

前面的00000000是地址偏移量,表示这个数据的写是从第一个字节开始写的
使用记事本打开:

10000转化为16进制是0x00002710,当我们以二进制形式将数据a写入文件时,可以看到,存储的确实是这四个字节的数据,当我们直接用记事本打开,出现的字符是我们看不懂的乱码
文本文件形式:
cpp
#include <stdio.h>
int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "w");
fprintf(pf, "%d", a);
fclose(pf);
pf = NULL;
return 0;
}
运行截图:
使用文本编辑器以文本文件方式打开test.txt:

使用记事本打开:

以文本文件方式读写整型数据时,程序会将数值的每一位数字单独当作字符处理,把每一个字符转换为对应的 ASCII 编码后再存入文件。使用记事本、VS 等文本编辑器打开文件时,编辑器会按照 ASCII 规则解析字节数据,直接还原出对应的字符,因此我们可以直观看懂文件中的内容
四、文件的打开和关闭
4.1流和标准流
4.1.1什么是流?
在 C 语言中,程序和外部设备(磁盘文件、键盘、显示器、打印机等)之间进行数据传输的通道就叫做流。 可以把流理解成一条数据传输的管道:程序要读写外部数据时,数据会沿着这条管道流入 / 流出程序。 C 语言把所有输入、输出设备都统一抽象成「流」来操作,不管是文件、键盘还是屏幕,都可以用同一套文件操作函数处理,屏蔽了不同硬件的差异,不然程序员需要学习不同硬件专属的底层通信规则,开发过程会变得十分繁琐复杂
4.2.2标准流
C语言在程序运行时,会默认打开三个流
-
标准输入流 stdin
- 对应设备:键盘
- 作用:用来读取键盘输入的数据,
scanf默认从stdin读取数据。
-
标准输出流 stdout
- 对应设备:显示器(屏幕)
- 作用:用来向屏幕打印输出数据,
printf默认向stdout输出内容。
-
标准错误流 stderr
- 对应设备:显示器(屏幕)
- 作用:专门用来输出程序错误、警告信息,保证错误信息可以及时在屏幕显示,不受缓冲区刷新规则影响,方便排查程序崩溃原因
stdin、stdout、stderr 三个流的类型是: FILE * ,通常称为文件指针。
C语言中,就是通过 FILE* 的文件指针来维护流的各种操作的。
4.2文件指针
1.程序每打开一个文件,操作系统会在内存中自动开辟一块文件信息区,用来存储文件名称、文件读写状态、当前读写位置、缓冲区地址、缓冲区大小等文件相关数据。
2.这些信息统一存放在系统预定义的结构体中,这个结构体类型名叫 FILE,定义在 <stdio.h> 头文件里。
3.我们定义一个 FILE* 类型的指针(文件指针 ),指向该FILE结构体变量,后续所有文件读写、关闭操作都通过这个指针完成。
比如VS2013 中 FILE 的底层定义:
cpp
// 底层结构体
struct _iobuf {
char* _ptr; // 文件缓冲区当前位置指针
int _cnt; // 缓冲区剩余有效字节数
char* _base; // 文件缓冲区起始地址
int _flag; // 文件状态标志
int _file; // 文件描述符
int _charbuf; // 单字节缓冲标记
int _bufsiz; // 缓冲区大小
char* _tmpfname;// 临时文件名
};
// 类型重命名:把struct _iobuf 别名成 FILE
typedef struct _iobuf FILE;
我们可以通过创建 FILE* 类型的指针,来找到我们相应文件对应的文件信息区,也就是这块结构体,进而用fopen、fclose等库函数进行打开、读写、关闭等操作
4.3文件的打开和关闭
文件在 读写 之前应该先 打开文件 ,在使用 结束 之后应该 关闭文件
调用fopen打开文件:操作系统会在内存自动创建 FILE 结构体、填充文件状态、缓冲区、读写位置等全部信息,然后返回该结构体地址赋值给pf;后续fgetc、fputc、fread、fwrite、fclose等文件操作函数,都依靠传入的pf找到这个 FILE 结构体,依靠结构体里的各项信息完成文件操作
打开文件成功
返回:动态创建的 FILE 结构体的首地址 ,可以用
FILE*类型指针接收。打开文件失败
返回:空指针 NULL
fclose(pf)关闭文件:释放 FILE 结构体占用的内存、刷新缓冲区、断开文件指针和磁盘文件的关联,操作后需要手动将pf=NULL防止野指针
关闭文件成功
返回:整数 0
关闭文件失败
返回:EOF(宏常量,值为 -1)
fopen 函数的原型:
cpp
//打开文件
FILE* fopen(const char* filename, const char* mode);
参数说明:
const char * filename:文件名字符串,可以是相对路径 ("test.txt")或绝对路径 ("D:\\C_language\\6.18博客C语言文件操作\\test.txt");用const修饰代表文件名不可被函数修改。
上述test.txt在Windows 系统里真实的文件路径应该是(D:\C_language\6.18博客C语言文件操作\test.txt),但因为在C 语言字符串里,单个反斜杠 \ 是转义起始字符 ,不会被当成路径分隔符,编译器会把 \ 紧跟的字符组合解析成转义字符,导致路径错乱失效,所以我们使用 双斜杠 \\,才能表示一个普通的路径分隔符 \
当然我们也可以用正斜杆 /,不需要转义:("D:/C_language/6.18博客C语言文件操作/test.txt")
const char * mode:mode是指向字符串的常量字符指针,用来指定文件的打开方式 ,决定文件是读、写、追加,以及以文本模式 还是二进制模式打开。
文本文件打开模式(不带 b)
| 文件使用方式 | 含义 | 文件不存在 | 文件已存在 |
|---|---|---|---|
"r"(只读) |
为读取数据,打开一个已存在的文本文件 | 打开失败,报错返回NULL |
从文件起始位置读取原有内容,不可写入修改 |
"w"(只写) |
为写入数据,创建 / 打开文本文件 | 自动新建空白文本文件 | 直接清空文件原有全部内容,从开头写入新数据 |
"a"(追加) |
在文本文件末尾追加写入数据 | 自动新建空白文本文件 | 保留原有所有内容,仅能在文件末尾写入,不能修改前面内容 |
"r+"(读写) |
可读可写,打开已存在的文本文件 | 打开失败,报错返回NULL |
从文件开头位置读写,写入会覆盖原有字符,不会自动清空文件 |
"w+"(读写) |
可读可写,新建文本文件 | 自动新建空白文本文件 | 先清空原有全部内容,既可以写入数据,也可以读取数据 |
"a+"(追加读写) |
文件末尾追加写入,同时支持读取全部内容 | 自动新建空白文本文件 | 保留原文件内容;写入只能在文件末尾,读取可从头读到尾 |
二进制文件打开模式(带 b)
| 文件使用方式 | 含义 | 文件不存在 | 文件已存在 |
|---|---|---|---|
"rb"(只读) |
读取二进制文件数据 | 打开失败,报错返回NULL |
从文件起始位置读取二进制数据,不可写入 |
"wb"(只写) |
写入二进制文件数据 | 自动新建空白二进制文件 | 清空原文件全部内容,从头写入二进制数据 |
"ab"(追加) |
在二进制文件末尾追加数据 | 自动新建空白二进制文件 | 保留原内容,仅可在文件末尾追加写入 |
"rb+"(读写) |
可读可写打开二进制文件 | 打开失败,报错返回NULL |
从文件开头读写,写入会覆盖原有数据,不清空文件 |
"wb+"(读写) |
可读可写新建二进制文件 | 自动新建空白二进制文件 | 清空原有内容,支持二进制数据的读写操作 |
"ab+"(读写) |
二进制文件末尾追加写入 + 全局可读 | 自动新建空白二进制文件 | 保留原内容,写入只能在末尾,读取不受位置限制 |
实例代码:
cpp
#include <stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
//必须判断是否打开失败,失败则直接return 1结束程序
if (pf == NULL)
{
perror("fopen");
return 1;
}
//打开成功,操作文件
//....
//操作完成,关闭文件
fclose(pf);
pf = NULL;
return 0;
}
五、文件的顺序读写
5.1 顺序读写函数汇总表
| 函数名 | 功能 | 适用范围 |
|---|---|---|
fgetc |
从流中读取单个字符 | 所有输入流(文件流、标准输入流stdin) |
fputc |
向流中写入单个字符 | 所有输出流(文件流、标准输出流stdout) |
fgets |
从流中读取一行字符串 | 所有输入流 |
fputs |
向流中写入一行字符串 | 所有输出流 |
fscanf |
从流中格式化读取数据 | 所有输入流 |
fprintf |
向流中格式化写入数据 | 所有输出流 |
fread |
二进制方式批量读取数据 | 仅文件输入流(二进制文件) |
fwrite |
二进制方式批量写入数据 | 仅文件输出流(二进制文件) |
5.2 顺序读写函数详解
5.2.1 fgetc 字符输入(读)
fgetc 函数的原型:
cpp
int fgetc(FILE* stream);
用法:
- 从指定输入流读取一个字符,返回该字符的 ASCII 值
- 读到文件末尾 / 读取失败,返回
EOF(值为 - 1) - 适用:文件流、
stdin(键盘)
cpp
int ch = fgetc(pf); // 从文件读取一个字符
int ch = fgetc(stdin);// 等价 getchar(),从键盘读取单个字符
示例用法:
首先我提前在test.txt中写入了abcdef六个字符,然后要将这六个字符读出来并打印在屏幕上
cpp
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
int c = 0;
//使用fgetc加循环拿出文件里存的所有字符
while ((c=fgetc(pf) )!= EOF)
{
printf("%c ", c);
}
fclose(pf);
pf = NULL;
return 0;
}
运行截图:
5.2.2 fputc 字符输出(写)
fputc 函数的原型:
cpp
int fputc(int ch, FILE* stream);
用法:
- 从指定输入流读取一个字符,返回该字符的 ASCII 值
- 读到文件末尾、读取失败时返回
EOF(值为 - 1) - 适用范围:文件流、标准输入流
stdin(键盘)
cpp
fputc('a', pf); // 将字符a写入文件
fputc('a', stdout); // 等价 putchar('a'),在屏幕打印字符a
示例用法:
将字符'a'到'z'都写进文件里
cpp
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen:");
return 1;
}
for (char ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, pf);
}
fclose(pf);
pf = NULL;
return 0;
}
运行截图:成功写入

5.2.3 fgets 按行读取字符串
fgets 函数的原型:
cpp
char* fgets(char* str, int num, FILE* stream);
用法:
- 从流中最多读取
num-1个字符存入字符数组str,数组末尾自动补充\0 - 读取过程遇到换行符
\n就停止读取,换行符\n会一并存入字符数组 - 读取成功返回传入的数组首地址
str;读到文件末尾或读取失败返回NULL
示例用法:
首先我提前在test.txt中写入了abcdefghijklmn十四个字符,接着我要读取其中前九个字符放入字符串数组ch中并打印出这个数组
cpp
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen:");
return 1;
}
char ch[10] = { 0 };
fgets(ch, 10, pf);
printf("%s\n", ch);
fclose(pf);
pf = NULL;
return 0;
}
运行截图:

5.2.4 fputs 写入字符串
fputs 函数的原型:
cpp
int fputs(const char* str, FILE* stream);
用法:
- 将字符串
str整体写入指定输出流,不会自动在末尾追加换行符\n; - 写入成功返回非负整数,写入失败返回
EOF。 fputs只把字符串中可见的有效字符 写入文件,不会把字符串末尾用来标记字符串结束的隐藏\0写入磁盘文件。
cpp
fputs("hello world", pf); // 将字符串写入文件
fputs("hello world", stdout); // 在屏幕输出字符串
示例用法:
cpp
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fputs("abcde\n", pf);
fputs("cdeee\n", pf);
fclose(pf);
pf = NULL;
return 0;
}
当我们连续写入两段字符串给文件时,文件中不会自动换行,需要我们主动写\n进行换行
运行截图:

5.2.5 fscanf 格式化读取
fscanf 函数的原型:
cpp
int fscanf(FILE* stream, const char* format, 变量地址);
- 第一个参数FILE* stream:指定从哪个输入流读取数据
- 后面参数用法和
scanf完全一样:格式字符串 + 变量地址 (比如fscanf (pf, "% d", &a);)
示例用法:
1.从文件里读数据
首先我在test.txt文件中写入了100,然后我使用fscanf函数给它读取到变量a中并打印
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
int a = 0;
// 从文件里读取一个十进制整数存到a
fscanf(pf, "%d", &a);
printf("从文件读到的整数:%d\n", a); // 输出:100
fclose(pf);
pf = NULL;
return 0;
}
运行截图:

2.从屏幕上拿数据
fscanf (stdin, "%d", &a) 等价于 scanf ("%d", &a)
cpp
#include <stdio.h>
int main()
{
int a = 0;
printf("请输入一个整数:");
// 等价 scanf("%d", &a);
fscanf(stdin, "%d", &a);
printf("你输入的数字:%d", a);
return 0;
}
运行截图:

5.2.6 fprintf 格式化写入
fprintf 函数的原型:
cpp
int fprintf(FILE* stream, const char* format, 写入的变量);
用法:
- 按照指定格式,将数据写入指定输出流;可以写入到文件pf,也可以写入到屏幕上,stdout
- 用法和
printf完全一致,仅第一个参数多了文件流指针
示例用法:
cpp
int a = 10;
fprintf(pf, "a=%d", a); // 格式化内容写入文件
fprintf(stdout, "a=%d", a); // 等价 printf("a=%d", a),格式化输出到屏幕上
5.2.7 fread 二进制数据读取
fread 函数的原型:
cpp
size_t fread(void* ptr, size_t size, size_t count, FILE* stream);
不会自动转换格式,直接按二进制字节原样读取
参数说明:
ptr:缓冲区首地址,用来存放从文件读到的数据,可以是数组、结构体地址;size:单个数据占用的字节大小count:需要读取的数据元素个数stream:文件指针,文件必须以二进制读模式rb打开
返回值:
返回实际成功读取到的数据元素个数
调用一次fread函数,会尝试从文件中读取count个大小为size的数据块,函数返回本次实际成功读取到的数据块个数。若返回值等于count,代表本次成功读取到预期数量的数据;若返回值小于count,说明已读到文件末尾或读取发生错误,本次读取结束。想要持续读取文件全部内容,需要我们在外层手动写循环反复调用fread,判断返回值是否小于count来终止循环
比如:
cpp
size_t ret;
char buf[100];
while((ret = fread(buf,1,100,fp)) == 100)
{
// 处理读取到的ret个数据
}
示例用法:(常用于结构体,以结构体为例)
cpp
#include <stdio.h>
struct Student
{
char name[20];
int age;
};
int main()
{
//写入结构体
struct Student s1 = { "张三", 18 };
FILE* pf = fopen("stu.bin", "wb");
fwrite(&s1, sizeof(struct Student), 1, pf);
fclose(pf);
//fread读取结构体
struct Student s2;
pf = fopen("stu.bin", "rb");
size_t ret = fread(&s2, sizeof(struct Student), 1, pf);
if (ret == 1)
{
printf("姓名:%s,年龄:%d\n", s2.name, s2.age);
}
fclose(pf);
return 0;
}
运行截图:

5.2.8 fwrite 二进制数据写入
fwrite 函数的原型:
cpp
size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream);
参数说明:
ptr:要写入文件的数据在内存中的起始地址;size:单个数据的字节大小;count:要写入多少个该类型数据;- 文件必须以二进制只写模式
wb打开; - 直接把内存里的二进制字节原样写入文件,不转成文本;
- 返回实际成功写入的数据元素个数。
使用示例:
cpp
int arr[5] = {1,2,3,4,5};
fwrite(arr, sizeof(int), 5, pf);
六、一组函数的对比
6.1 scanf/fscanf/sscanf
- scanf--从标准输入流中格式化读取数据放入变量中
- fscanf--从指定的输入流中格式化读取数据放入变量中
- sscanf--从字符串中格式化读取数据放入变量中
6.1.1 sscanf 函数
sscanf 函数原型:
cpp
int sscanf(const char* buffer, const char* format, ...);
和scanf函数的用法基本一致,就是多了个字符串的地址,表示格式化读取字符串的内容并写进后面的变量中,下面我们来看一个使用案例:
cpp
#include <stdio.h>
struct Stu
{
char name[20];
int age;
};
int main()
{
char buf[20] = "kkeeper 21";
struct Stu s1 = { 0 };
sscanf(buf, "%s %d", s1.name,&(s1.age));
printf("%s %d", s1.name, s1.age);
return 0;
}
利用了 sscanf 函数将字符串数组buf中的数据读取到结构体 s1.name 和 &(s1.age) 中,因为s1.age是整型不是地址,所以要取地址,接着我们打印,看是否读取成功
运行截图:

6.2printf/fprintf/sprintf
- printf--将数据格式化输出到标准输出流中
- fprintf--将数据格式化输出到指定的输出流中
- sprintf--将数据格式化输出到字符串中
6.2.1 sprintf 函数
sprintf 函数原型:
cpp
int sprintf(char* buffer, const char* format, ...);
sprintf 函数的用法和 printf 函数类似,就是参数多一个字符串的地址,表示把后面的数据格式化输出到这个字符串当中,相当于将数据拷贝打印进了这个buffer字符串中,而不是打印到屏幕上,下面我们来看一个使用案例:
cpp
#include <stdio.h>
struct Stu
{
char name[20];
int age;
};
int main()
{
char buf[20] = { 0 };
struct Stu s1 = { "kkeeper",21};
sprintf(buf, "%s %d", s1.name,s1.age);
printf("%s",buf);
return 0;
}
将结构体struct Stu s1当中的数据通过sprintf 函数放入到字符串数组buf中,并打印出来
运行截图:

七、文件的随机读写
7.1 fseek 函数
fseek 函数的原型:
cpp
int fseek(FILE* stream, long int offset, int origin);
作用:用来移动文件位置指针 ,实现文件的随机读写,不再只能从头到尾顺序读取 / 写入文件,可以直接跳到文件任意指定字节位置操作数据
参数说明:
FILE *stream:已经成功打开的文件指针;long offset:偏移量,单位字节- 正数:向文件末尾方向偏移(向后)
- 负数:向文件开头方向偏移(向前)
int origin:参考起始位置,只能选下面三个宏:SEEK_SET:文件起始位置SEEK_CUR:文件指针当前所在位置SEEK_END:文件末尾位置
使用示例:
cpp
#include <stdio.h>
int main()
{
FILE* pFile;
// 以二进制只写模式打开文件
pFile = fopen("example.txt", "wb");
// 先写入字符串:This is an apple.
fputs("This is an apple.", pFile);
// 将文件指针从文件开头向后偏移9个字节
fseek(pFile, 9, SEEK_SET);
// 从偏移后的位置写入字符串 " sam",覆盖原有内容
fputs(" sam", pFile);
// 关闭文件
fclose(pFile);
return 0;
}
我们以二进制只写模式打开文件,这样做是为了我们用fseek函数可以精确定位到我们所要的位置,原文本怎样,二进制模式打开就是怎样,如果用"w"文本模式打开,若文件内容中已有'\n',比如abc\n123,文本模式会自动将\n补全为\r\n,变成abc\r\n123多了一个字节的数据,当我们用fseek(4, SEEK_SET)想要定位到字符1,但因为换行被扩充多了一个字节,导致我们找到的不是1,而是\n,所以我们用二进制只写模式打开此文件
接着我们将字符串"This is an apple."写入到文件中,通过fseek(pFile, 9, SEEK_SET)将文件指针从文件起始位置向后偏移 9 个字节,定位到下标为 9 的字符n处,然后用fputs(" sam", pFile)覆盖此位置写入内容,得到的结果就是"This is a sample."
运行截图:

7.2 ftell 函数
ftell 函数的原型:
cpp
long int ftell(FILE* stream);
作用:返回文件指针相对于起始位置的字节偏移量
使用示例:
cpp
#include <stdio.h>
int main()
{
FILE* pf = fopen("myfile.txt", "wb");
fputs("abcde",pf);
fclose(pf);
pf = NULL;
FILE* pFile;
long size;
pFile = fopen("myfile.txt", "rb");
if (pFile == NULL)
{
perror("Error opening file");
return 1;
}
else
{
fseek(pFile, 0, SEEK_END); // non-portable
size = ftell(pFile);
printf("Size of myfile.txt: %ld bytes.\n", size);
}
fclose(pFile);
pFile = NULL;
return 0;
}
我们首先用二进制写入的模式打开文件,使用fputs将字符串"abcde"写入到文件中,紧接着关闭此文件再以二进制读的模式打开,然后再用fseek将偏移量置到文件末尾,紧接着用ftell返回文件末尾到文件起始之间的字节偏移量,因为此时文件里全是英文字符,一个英文字符占一个字节,所以其实得到的结果也就是此文件字符的个数
选用二进制模式打开文件,不会自动转换换行符,能够保证fseek与ftell统计的字节偏移量精准无误
运行截图:

7.3 rewind 函数
rewind 函数的原型:
cpp
void rewind(FILE* stream);
作用:让文件指针的位置回到文件的起始位置
使用示例:
cpp
#include <stdio.h>
int main()
{
int n;
FILE* pFile;
char buffer[27];
pFile = fopen("myfile.txt", "wb+");
for (n = 'A'; n <= 'Z'; n++)
fputc(n, pFile);
rewind(pFile);
fread(buffer, 1, 26, pFile);
fclose(pFile);
pFile = NULL;
buffer[26] = '\0';
printf("%s\n",buffer);
return 0;
}
我们首先以二进制可读可写的模式打开文件myfile.txt,接着使用for循环搭配fputc函数将二十六个英文字母写进去,最关键的来了,写入完成后文件指针停留在文件末尾,所以接着我们使用了rewind函数将文件指针指向的位置重新置到文件起始位置,为我们后续使用fread顺序读取函数的正确使用做了铺垫,然后我们用fread将文件中这26个字节的数据放入字符数组buffer中,由于文件内不会自动存储字符串结束符\0,所以我们手动在buffer数组下标 26 的位置添加'\0'作为字符串终止标记,并打印,就能打印出我们文件当中的26个英文字母
运行截图:

八、文件读取结束的判定
8.1 feof 函数
feof 函数的原型:
cpp
int feof(FILE* stream);
作用:当文件读取结束的时候,判断是读取结束的原因是否是:遇到文件末尾结束
- 若因为读到文件末尾 EOF 导致读取结束:返回非 0(真),代表正常读取完成
- 若因为文件读写出错导致读取结束:返回0(假)
8.2 ferror 函数
ferror 函数的原型:
cpp
int ferror(FILE* stream);
作用:当文件读取结束的时候,判断是读取结束的原因是否是:发生读写错误
- 如果之前的文件读写操作出现错误,返回非 0 值(真)
- 如果没有发生读写错误,返回0(假)
8.3综合使用案例
总结:
feof:结束原因是读到文件末尾(正常结束)ferror:结束原因是读写出错(异常结束)
使用示例1:(文本文件)
cpp
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int c; // 注意:int,非char,要求处理EOF
FILE* fp = fopen("test.txt", "r");
if (!fp) {
perror("File opening failed");
return EXIT_FAILURE;
}
//fgetc 当读取失败的时候或者遇到⽂件结束的时候,都会返回EOF
while ((c = fgetc(fp)) != EOF) // 标准C I/O读取⽂件循环
{
putchar(c);
}
//判断是什么原因结束的
if (ferror(fp))
puts("\nI/O error when reading");
else if (feof(fp))
puts("\nEnd of file reached successfully");
fclose(fp);
}
我们提前在test.txt文件中写入"abcde"五个字符
我们以读的模式打开文件test.txt,搭配fgetc函数读取文件里的内容,只要读出来的结果不等于EOF,就继续读,读取到的字符会通过putchar打印到屏幕上,直到等于EOF后不再读取,我们判断,如果ferror(fp)为真,就说明是文件读取错误,读取异常,导致错误,如果feof(fp)为真,就说明读取到文件末尾结束,正常读取结束
补充一点:fgetc 返回 EOF 有两种完全不同的原因,不只有 "读到文件末尾" 这一种:
- 正常读取到文件末尾,没有更多数据 → 返回
EOF - 文件读写发生 I/O 异常(比如文件损坏、权限异常等),读取失败 → 也会返回
EOF
运行截图:

使用示例2:(二进制文件)
cpp
#include <stdio.h>
enum { SIZE = 5 };
int main(void)
{
// 定义double数组并初始化
double a[SIZE] = { 1., 2., 3., 4., 5. };
// 二进制只写模式打开文件
FILE* fp = fopen("test.bin", "wb");
// 将整个double数组写入二进制文件
fwrite(a, sizeof * a, SIZE, fp);
fclose(fp);
// 定义接收数据的数组
double b[SIZE];
// 二进制只读模式打开文件
fp = fopen("test.bin", "rb");
// 读取SIZE个double类型数据
size_t ret_code = fread(b, sizeof * b, SIZE, fp);
if (ret_code == SIZE)
{
// 读取成功,遍历打印数组
puts("Array read successfully, contents: ");
for (int n = 0; n < SIZE; ++n)
printf("%f ", b[n]);
putchar('\n');
}
else
{
// 读取失败,判断是读到文件末尾还是读写错误
if (feof(fp))
{
printf("Error reading test.bin: unexpected end of file\n");
}
else if (ferror(fp))
{
perror("Error reading test.bin");
}
}
fclose(fp);
return 0;
}
补充:sizeof 有两种用法:
- 类型:必须加括号
sizeof(double) - 变量 / 表达式:可以不加括号
sizeof *a,写成sizeof(*a)也合法
首先我们定义了一个枚举常量SIZE赋值为5,代表大小为5,接着定义了一个double类型SIZE大小的数组,以"wb"二进制写入模式打开文件test.bin,并使用fwrite函数将这个数组的五个数据都写入到文件中,关闭文件,接着再创建了b数组来接收数据,用二进制只读模式"rb"打开文件,并使用fread函数读取文件中的数据复制到数组b中,如果返回值等于SIZE,我们就打印数组b的内容,返回值不等于SIZE,就看一下出错的原因,如果是遇到了文件末尾 if (feof(fp)),我们就打印 printf("Error reading test.bin: unexpected end of file\n"),如果是遇到了文件读错写误 else if (ferror(fp)),我们就使用perror("Error reading test.bin")打印一下错误的原因,最后关闭文件释放资源
运行截图:

九、文件缓冲区
我们在程序中使用文件时,都会为此文件单独创建一个文件缓冲区,当我们写入数据到文件中时,不是直接写的,而是先会把需要写入的数据先存入到缓冲区上,当缓冲区装满时就会自动写入磁盘 ,没装满时需要刷新 / 关文件才将缓冲区的内容写入磁盘中;当我们从文件中读取数据时,也不是直接读的,而是会将需读取的数据先存入到文件缓冲区中,再逐个读取数据,读取时除了fgetc逐个读取,也可以通过fgets、fread批量从缓冲区读取数据
我们用代码来实际感受文件缓冲区的存在:
cpp
#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);//刷新缓冲区时,才将输出缓冲区的数据写到⽂件(磁盘)
//注:fflush 在⾼版本的VS上不能使⽤了
printf("再睡眠10秒-此时,再次打开test.txt⽂件,⽂件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭⽂件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
首先我们以只写的模式"w"打开文件,使用fputs函数将" abcdef "写入文件中,此时使用Sleep(10000)以毫秒为单位睡眠10秒,缓冲区不会被填满,数据依然存在缓冲区中,不会被写入到文件,此时我们打开文件test.txt会发现里面什么都没有,执行fflush(fp)手动刷新缓冲区后,我们再打开test.txt,就会发现文件里存入了" abcdef ",后面的Sleep是为了和fclose刷新缓冲区做个区别,表示这个缓冲区是通过fflush函数刷新,而不是fclose关闭文件时自动刷新
结论:
所以,由于文件缓冲区机制的存在,使用 C 语言进行文件操作时,必须手动刷新缓冲区,或是文件操作完毕后调用
fclose关闭文件若不执行刷新、不关闭文件,缓冲区中未写入磁盘的数据可能会仅存放在内存中,程序异常终止时数据会丢失,引发文件读写异常、数据保存失败等问题。
感谢大家的观看,新人求互三,关注我必回关!下章见!
