文章目录
- 前言
-
- 本文旨在系统性地介绍C语言中的文件操作。
- 一、文件
-
- 1.1、文件的作用
- 1.2、文件的分类
-
- 1.2.1、程序文件
- 1.2.2、数据文件
- 1.3、文件名
- 二、二进制文件与文本文件
- 三、文件的打开和关闭
-
- 3.1、流和标准流
-
- 3.1.1、流
- 3.1.2、标准流
- 3.2、文件指针和文件信息区(FILE结构体)
- 3.3、fopen函数和fclose函数
-
- 3.3.1、fopen函数
- 3.3.2、fclose函数
- 3.3.3、fopen函数和fclose函数的代码示例
- 四、文件的顺序读写
-
- 4.1、fgetc函数
- 4.2、fputc函数
- 4.3、fgets函数
-
- 4.3.1、fgets + sscanf 替代 scanf
- 4.4、fputs函数
- 4.5、fscanf函数
- 4.6、fprintf函数
- 4.7、fread函数
- 4.8、fwrite函数
- 4.9、feof函数
- 4.10、ferror函数
- 五、文件的随机读写
-
- 5.1、ftell函数
- 5.2、fseek函数
- 5.3、rewind函数
- 六、文件的读写更新与缓冲区刷新
-
- 6.1、三种读写模式:"r+"、"w+"、"a+"
- 6.2、读写切换的关键要点
- 6.3、文件缓冲区
- 6.4、fflush函数
前言
本文旨在系统性地介绍C语言中的文件操作。
一、文件
1.1、文件的作用
程序运行时,数据存储在内存中。程序结束,内存被回收,数据就会丢失。 要实现数据的持久化保存,就需要使用文件——将数据存储到磁盘(硬盘)上,下次运行时可以再次读取。
1.2、文件的分类
文件是存储在磁盘上的数据集合。从程序设计的角度,文件分为两类:程序文件、数据文件。
1.2.1、程序文件
与程序编译、链接、运行相关的文件,包括:源程序文件(.c)、目标文件(.obj)、可执行文件(.exe)。
1.2.2、数据文件
程序运行时读写的数据文件,用于保存输入数据或输出结果。
本文讨论的是数据文件。之前我们通过终端(键盘输入、屏幕输出)处理数据;现在我们要把数据保存到磁盘文件中,需要时再读入内存使用。
1.3、文件名
每个文件都有唯一的文件标识,方便用户识别和引用。 文件名由三部分组成:文件路径 + 文件名主干 + 文件后缀。为方便起见,文件标识通常简称为文件名。 示例:c:\\code\\test.txt
二、二进制文件与文本文件
数据在内存中以二进制形式存储。根据数据在文件中的组织形式,数据文件分为二进制文件和文本文件。
二进制文件:将内存中的数据原样复制到磁盘,不进行任何转换。文件存储的是原始二进制数据,其内容不一定对应字符编码,因此用文本编辑器打开时通常显示为乱码。
文本文件:将数据转换为字符序列(通常为ASCII或UTF-8编码)后存储到磁盘。文件中的每个字节都对应可打印字符或控制字符,可用文本编辑器直接查看和编辑。
数据在文件中的存储方式: ①字符数据:一律以ASCII码形式存储,因此二进制文件同样可以包含字符数据。 ②数值数据:既可以用ASCII码形式存储,也可以用二进制形式存储。
例如,整数 10000: 以 ASCII码形式 存储:占 5 个字节(每个字符占1个字节,即 ‘1’ ‘0’ ‘0’ ‘0’ ‘0’) 以 二进制形式 存储:占 4 个字节(int 类型通常为4字节)
代码示例: 以下代码以二进制形式将整数 10000 写入文件,在 VS 或其他编辑器中以二进制方式打开 test.txt,可查看其二进制内容。
#include <stdio.h>
int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "wb"); // 以二进制写入模式打开
fwrite(&a, 4, 1, pf); // 将 a 的二进制内容写入文件
fclose(pf);
pf = NULL;
return 0;
}
三、文件的打开和关闭
#mermaid-svg-JK0VkcG11QJxnnMr{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JK0VkcG11QJxnnMr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JK0VkcG11QJxnnMr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JK0VkcG11QJxnnMr .error-icon{fill:#552222;}#mermaid-svg-JK0VkcG11QJxnnMr .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JK0VkcG11QJxnnMr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JK0VkcG11QJxnnMr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JK0VkcG11QJxnnMr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JK0VkcG11QJxnnMr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JK0VkcG11QJxnnMr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JK0VkcG11QJxnnMr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JK0VkcG11QJxnnMr .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JK0VkcG11QJxnnMr .marker.cross{stroke:#333333;}#mermaid-svg-JK0VkcG11QJxnnMr svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JK0VkcG11QJxnnMr p{margin:0;}#mermaid-svg-JK0VkcG11QJxnnMr .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-JK0VkcG11QJxnnMr .cluster-label text{fill:#333;}#mermaid-svg-JK0VkcG11QJxnnMr .cluster-label span{color:#333;}#mermaid-svg-JK0VkcG11QJxnnMr .cluster-label span p{background-color:transparent;}#mermaid-svg-JK0VkcG11QJxnnMr .label text,#mermaid-svg-JK0VkcG11QJxnnMr span{fill:#333;color:#333;}#mermaid-svg-JK0VkcG11QJxnnMr .node rect,#mermaid-svg-JK0VkcG11QJxnnMr .node circle,#mermaid-svg-JK0VkcG11QJxnnMr .node ellipse,#mermaid-svg-JK0VkcG11QJxnnMr .node polygon,#mermaid-svg-JK0VkcG11QJxnnMr .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JK0VkcG11QJxnnMr .rough-node .label text,#mermaid-svg-JK0VkcG11QJxnnMr .node .label text,#mermaid-svg-JK0VkcG11QJxnnMr .image-shape .label,#mermaid-svg-JK0VkcG11QJxnnMr .icon-shape .label{text-anchor:middle;}#mermaid-svg-JK0VkcG11QJxnnMr .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JK0VkcG11QJxnnMr .rough-node .label,#mermaid-svg-JK0VkcG11QJxnnMr .node .label,#mermaid-svg-JK0VkcG11QJxnnMr .image-shape .label,#mermaid-svg-JK0VkcG11QJxnnMr .icon-shape .label{text-align:center;}#mermaid-svg-JK0VkcG11QJxnnMr .node.clickable{cursor:pointer;}#mermaid-svg-JK0VkcG11QJxnnMr .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JK0VkcG11QJxnnMr .arrowheadPath{fill:#333333;}#mermaid-svg-JK0VkcG11QJxnnMr .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JK0VkcG11QJxnnMr .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JK0VkcG11QJxnnMr .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JK0VkcG11QJxnnMr .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JK0VkcG11QJxnnMr .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JK0VkcG11QJxnnMr .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JK0VkcG11QJxnnMr .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JK0VkcG11QJxnnMr .cluster text{fill:#333;}#mermaid-svg-JK0VkcG11QJxnnMr .cluster span{color:#333;}#mermaid-svg-JK0VkcG11QJxnnMr div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JK0VkcG11QJxnnMr .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JK0VkcG11QJxnnMr rect.text{fill:none;stroke-width:0;}#mermaid-svg-JK0VkcG11QJxnnMr .icon-shape,#mermaid-svg-JK0VkcG11QJxnnMr .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JK0VkcG11QJxnnMr .icon-shape p,#mermaid-svg-JK0VkcG11QJxnnMr .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JK0VkcG11QJxnnMr .icon-shape .label rect,#mermaid-svg-JK0VkcG11QJxnnMr .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JK0VkcG11QJxnnMr .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JK0VkcG11QJxnnMr .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JK0VkcG11QJxnnMr :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
执行
系统建立
通过文件描述符关联
系统创建
返回地址
指向
写入:数据通过流输出
写入:数据流入
写入:数据写入
读取:数据读出
读取:数据流入
读取:数据通过流输入
程序代码
打开文件
流
文件信息区FILE 结构体
文件指针 FILE*
缓冲区
磁盘文件
当程序打开一个文件时,系统会建立一个与该文件相关联的流,同时创建一个文件信息区(FILE结构体)来记录并更新流的当前状态信息,并返回一个文件指针(FILE*)指向该信息区。
程序通过 FILE* 指针找到 FILE 结构体,再通过其中的文件描述符找到对应的流,从而执行读、写、关闭等操作,实现对文件数据的读写。
程序不直接与文件交换数据,而是通过流将数据先写入缓冲区(写入时),再由缓冲区与文件交互;读取时数据从文件进入缓冲区,再通过流进入程序。这种机制减少了直接读写外部存储介质的次数,提高了数据读写效率。
3.1、流和标准流
3.1.1、流
程序需要向各种外部设备(如显示器、磁盘、键盘等)输出数据,也需要从这些设备读取数据。不同设备的输入输出操作方式各不相同,为了简化程序员的操作,C语言引入了流(Stream)的概念。
流是程序与外部设备之间的数据传输通道。可以将流想象成一条流淌着数据的河流——程序从流中读取数据,或将数据写入流中,而无需关心数据具体流向何种设备。
C程序针对文件、屏幕、键盘等设备的输入输出操作,都是通过流来完成的。一般情况下,使用流之前需要先打开流,操作完成后关闭流。
3.1.2、标准流
程序使用 scanf 从键盘读取数据、使用 printf 向屏幕输出数据时,并未显式打开流,这是因为C程序在启动时会默认打开三个标准流: ① 标准输入流:数据从标准输入设备读取,一般为键盘;scanf 从该流读取数据。 ② 标准输出流:数据向标准输出设备写入,一般为显示器;printf 向该流写入数据。 ③ 标准错误流:错误信息向标准错误设备写入,一般为显示器;用于输出程序运行时的错误信息。 stdin、stdout、stderr 分别是指向三个标准流关联的FILE结构体的文件指针,类型为 FILE*。通过它们可以操作对应的标准流。 由于这三个标准流默认已打开,程序可以直接使用 scanf、printf 等函数进行输入输出,无需手动打开和关闭。
3.2、文件指针和文件信息区(FILE结构体)
在缓冲文件系统中,每个被使用的文件,系统都会在内存中创建一个对应的文件信息区,用于存放与该文件关联的流的状态信息。这些信息保存在一个名为 FILE 的结构体变量中,该结构体类型由系统声明。程序通过 FILE* 指针来操作这个结构体变量,这个指针就是文件类型指针,简称文件指针。
创建一个 FILE* 指针变量的方式如下:
FILE* pf; // 文件指针变量
pf 是一个指向 FILE 类型数据的指针变量,可以使 pf 指向某个文件的文件信息区(即结构体变量)。该结构体变量中记录了文件描述符,通过文件指针变量找到文件信息区,再通过其中的文件描述符找到对应的流,从而间接访问与它关联的文件。
FILE 结构体记录的是流的状态信息(本次打开文件时产生的动态信息),而不是文件本身的信息(如大小、权限、创建时间等静态属性,这些存在磁盘 inode 中)。 FILE 结构体的定义(VC 环境)
struct _iobuf
{
char *_ptr; // 缓冲区当前读写的指针位置
int _cnt; // 缓冲区中剩余的字符数
char *_base; // 指向缓冲区基址的指针
int _flag; // 文件状态标志(如读、写、错误、结束等)
int _file; // 文件描述符(操作系统的文件编号)
int _charbuf; // 单字符缓冲区状态
int _bufsiz; // 缓冲区的大小
char *_tmpfname; // 临时文件名(若有)
};
typedef struct _iobuf FILE;
3.3、fopen函数和fclose函数
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
ANSI C 规定使用 fopen 函数来打开文件,fclose 函数来关闭文件。在打开文件的同时,系统会在内存中创建一个文件信息区(FILE 结构体),并返回一个 FILE* 类型的指针变量指向该信息区。程序通过该指针找到文件信息区,再通过其中的文件描述符找到对应的流,从而间接访问文件
3.3.1、fopen函数
fopen 函数打开 filename 参数指定的文件,并将该文件与一个流关联。后续所有对该流的操作都通过 fopen 返回的 FILE* 指针进行,具体的操作方式由 mode 参数决定。
函数原型
FILE *fopen(const char *filename, const char *mode);
头文件:<stdio.h> 参数:①filename:文件名(可含绝对/相对路径);②mode:对于打开的文件的操作方式(见下表) 返回值:①成功:返回 FILE* 指针;②失败:返回 NULL(必须检查)
| “r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
| “w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
| “a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
| “rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
| “wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
| “ab”(追加) | 向一个二进制文件尾添加数据 | 建立一个新的文件 |
| “r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
| “w+”(读写) | 为了读和写,建议一个新的文件 | 建立一个新的文件 |
| “a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
| “rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
| “wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
| “ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
3.3.2、fclose函数
fclose函数关闭 stream 关联的文件,并取消其关联关系。与该流关联的所有内部缓冲区均会解除关联并刷新:①输出缓冲区:任何待写入的输出缓冲区内容将被写入磁盘;②输入缓冲区:任何未读取的输入缓冲区内容将被丢弃。
函数原型
int fclose ( FILE * stream );
头文件:<stdio.h> 参数:stream:指向要关闭的流关联的 FILE 结构体的指针 返回值:①成功:返回0;②失败:返回EOF(通常在 stdio.h 中定义为 -1)
3.3.3、fopen函数和fclose函数的代码示例
#include <stdio.h>
int main()
{
// 以只读方式打开 test.txt
FILE *fp = fopen("test.txt", "r");
if (fp == NULL)
{
perror("fopen"); // 输出错误信息
return 1; // 打开失败,退出程序
}
printf("文件打开成功!\\n");
// … 对文件进行读写操作 …
fclose(fp); // 关闭文件
fp=NULL; // 将指针置为 NULL,避免成为野指针
return 0;
}
四、文件的顺序读写
在进行文件读写的时候,会涉及下列函数:
| fgetc | 从输入流中读取一个字符 | 所有输入流 |
| fputc | 向输出流中写入一个字符 | 所有输出流 |
| fgets | 从输入流中读取一个字符串 | 所有输入流 |
| fputs | 向输出流中写入一个字符串 | 所有输出流 |
| fscanf | 从输入流中读取带有格式的数据 | 所有输入流 |
| fprintf | 向输出流中写入带有格式的数据 | 所有输出流 |
| fread | 从输入流中读取一块数据 | 文件输入流 |
| fwrite | 向输出流中写入一块数据 | 文件输出流 |
| feof | 检测是否到达文件末尾 | 所有输入流 |
| ferror | 检测是否发生读写错误 | 所有输入流/输出流 |
所有输入流:标准输入流+ 其他输入流(如文件输入流) 所有输出流:标准输出流+ 其他输出流(如文件输出流)
4.1、fgetc函数
fgetc函数从 stream 指向的 FILE 结构体关联的输入流中读取一个字符。函数返回文件指示器当前指向的字符,读取这个字符之后,文件指示器自动前进到下一个字符。 函数原型
int fgetc ( FILE * stream );
头文件:<stdio.h> 参数:stream:指向要读取的输入流关联的 FILE 结构体的指针 返回值:①成功:返回读取的字符(以 int 形式);②到达文件末尾 :返回 EOF,并设置流的文件结束指示器;③读取错误:返回 EOF,并设置流的错误指示器
代码示例:
#include <stdio.h>
int main()
{
// 以 "r" 的形式打开文件,才能正确地读取文件
FILE* fp = fopen("test.txt", "r");
if (fp == NULL)
{
perror("fopen");
return 1;
}
int i = 0;
// 循环读取前10个字符
for (i = 0; i < 10; i++)
{
int c = fgetc(fp); // 从文件读取一个字符
fputc(c, stdout); // 使用 fputc 在标准输出流上打印字符
}
fclose(fp);
fp = NULL;
return 0;
}
4.2、fputc函数
fputc函数将 character 指定的字符写入到 stream 指向的的 FILE 结构体关联的输出流中。字符被写入流内部位置指示器当前指向的位置,随后该指示器自动向前移动一个位置。 函数原型
int fputc ( int character, FILE * stream );
头文件:<stdio.h> 参数:①character:被写入的字符(以 int 形式传递);②stream:指向要写入的输出流关联的 FILE 结构体的指针 返回值:①成功:返回写入的字符(以 int 形式);②失败:返回 EOF,并设置流的错误指示器
代码示例:
#include <stdio.h>
int main()
{
// 以 "w" 的形式打开文件,才能正确地写文件
FILE* fp = fopen("test.txt", "w");
if (fp == NULL)
{
perror("fopen\\n");
return 1;
}
int ch = 0;
// 循环写入 a 到 z
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, fp);
}
fclose(fp);
fp = NULL;
return 0;
}
4.3、fgets函数
fgets函数从 stream 指向的 FILE 结构体关联的输入流中读取字符串,至读取到换行符、文件末尾(EOF)或达到指定字符数(包含结尾的空字符 \\0),然后将读取到的字符串存储到 str 指向的空间中。
函数原型
char * fgets ( char * str, int num, FILE * stream );
头文件:<stdio.h> 参数:①str:指向字符数组的指针,用于存储读取到的字符串;②num:最大读取字符数(包含结尾的 \\0,实际最多读取 num-1 个字符);③stream:指向要读取的输入流关联的 FILE 结构体的指针 返回值:①成功:返回 str 指针;②到达文件末尾:返回 NULL,并设置流的文件结束指示器;③读取错误:返回 NULL,并设置流的错误指示器
注意事项: ①读取到换行符时会包含在字符串中(除非超出num-1限制); ②遇到文件末尾时不会补一个换行符; ③自动在字符串末尾添加 \\0,保证它是一个合法的 C 字符串。
代码示例:
#include <stdio.h>
int main()
{
FILE* fp = fopen("test.txt", "r");
if (fp == NULL)
{
perror("fopen\\n");
return 1;
}
char arr1[] = "*************";
fgets(arr1, sizeof(arr1), fp); // 读取第一行
char arr2[] = "*************";
fgets(arr2, sizeof(arr2), fp); // 读取第二行
fclose(fp);
fp = NULL;
return 0;
}
4.3.1、fgets + sscanf 替代 scanf
不用 scanf的原因: ①缓冲区溢出:%s 不检查长度,输入超长会覆盖内存,例如char buf[5]; scanf(“%s”, buf); 输入 “abcdef” 导致溢出; ②空格截断:%s 遇到空格就停止,读不了带空格的字符串,例如输入 “Hello World”,只读到 “Hello”; ③\\n 残留:读取数字后,回车符 \\n 留在缓冲区,影响下次输入 scanf(“%d”) 后接 getchar() 会直接读到 \\n。
用fgets + sscanf的原因: ①防止溢出:fgets 有缓冲区大小限制,安全可靠; ②读取整行:包含空格,不会被截断; ③无 \\n 残留:fgets把 \\n 从缓冲区取走,转移到字符串中,缓冲区干净; ④错误处理灵活:可检查 sscanf 返回值,输入错误可重试。
| 读字符串 | scanf(“%s”, buf); | fgets(buf, sizeof(buf), stdin); |
| 读整数 | scanf(“%d”, &n); | fgets(buf, sizeof(buf), stdin); sscanf(buf, “%d”, &n); |
| 读浮点数 | scanf(“%f”, &f); | fgets(buf, sizeof(buf), stdin); sscanf(buf, “%f”, &f); |
| 读字符 | scanf(" %c", &ch); | fgets(buf, sizeof(buf), stdin); ch = buf[0]; |
代码示例:
#include <stdio.h>
#include <string.h>
// 核心思想: 用 fgets 安全读取整行,再用 sscanf 解析数据
int main()
{
char buf[256] = {0};
int n;
printf("请输入整数:");
fgets(buf, sizeof(buf), stdin); // 安全读取整行
buf[strcspn(buf, "\\n")] = '\\0'; // 去除换行符\\n并替换为 \\0
if (sscanf(buf, "%d", &n) == 1) // 解析数据
printf("读取成功:%d\\n", n);
return 0;
}
strcspn 函数 strcspn 函数返回字符串 str1 中第一个出现在字符串 str2 中的字符的索引位置(从 0 开始计数)。 函数原型
size_t strcspn(const char *str1, const char *str2);
头文件:<string.h> 参数:①str1:被查找的字符串;②str2 包含要查找字符的字符串(查找集合) 返回值:①找到匹配字符:返回该字符在 str1 中的位置(下标);②没有匹配字符:返回 str1 的长度(即 strlen(str1))
示例:
| strcspn(“Hello\\n”, “\\n”) | 5 | \\n 在第 5 位(下标从 0 开始) |
| strcspn(“Hello”, “\\n”) | 5 | 没有 \\n,返回字符串长度 |
| strcspn(“abc123”, “0123456789”) | 3 | 第一个数字 1 在第 3 位 |
4.4、fputs函数
fputs函数将 str 指向的字符串写入到 stream 指向的 FILE 结构体关联的输出流中。
函数原型
int fputs ( const char * str, FILE * stream );
头文件:<stdio.h> 参数:①str:指向要写入的字符串(必须以 \\0 结尾);②stream:指向要写入的输出流关联的 FILE 结构体的指针 返回值:①成功:返回非负整数;②失败:返回 EOF,并设置流的错误指示器
注意事项: ①写入的字符串不包含结尾的 \\0; ②写入时不会自动添加换行符 \\n; ③遇到字符串中的 \\0 会立即停止写入。
代码示例:
#include <stdio.h>
int main()
{
FILE* fp = fopen("test.txt", "w");
if (fp == NULL)
{
perror("fopen\\n");
return 1;
}
fputs("abc\\0def", fp); // 写入到文件
fputs("hehe", fp); // 继续写入
// 不再使用文件时,需要关闭文件
fclose(fp);
fp = NULL; // 将指针置为 NULL,避免成为野指针
return 0;
}
4.5、fscanf函数
fscanf 函数从 stream 指向的 FILE 结构体关联的输入流中读取格式化数据。它类似于 scanf,但可以指定输入源(如文件流、标准输入流等),而非仅限于标准输入流。适用于从文件解析结构化数据(如整数、浮点数、字符串等)。
函数原型
int fscanf ( FILE * stream, const char * format, ... );
头文件:<stdio.h> 参数:①stream:指向要读取的输入流关联的 FILE 结构体的指针;②format:格式化字符串,定义如何解析输入数据(如 %d、%s、%f 等);③…:可变参数存储数据的变量地址(需与格式字符串中的说明符匹配) 返回值:①成功:返回成功填充到参数列表中的项数;②匹配失败:返回小于预期的项数(可能为 0);③到达文件末尾:返回 EOF,并设置文件结束指示器;④读取错误:返回 EOF,并设置流的错误指示器
代码示例:
#include <stdio.h>
struct Stu
{
char name[30];
int age;
float score;
};
// 前提: 在 fprintf 学习时,已向 test.txt 文件写入了数据:zhangsan 20 85.5
int main()
{
struct Stu s = { 0 };
FILE* fp = fopen("test.txt", "r");
if (fp == NULL)
{
perror("fopen\\n");
return 1;
}
// 按照格式读取数据
fscanf(fp, "%s %d %f", s.name, &(s.age), &(s.score));
// 打印数据到 stdout
fprintf(stdout, "%s %d %.1f\\n", s.name, s.age, s.score);
fclose(fp);
fp = NULL;
return 0;
}
4.6、fprintf函数
fprintf函数是将格式化数据写入stream 指向的 FILE 结构体关联的输出流中。它与 printf 类似,可以输出到任意输出流(如文件流、标准输出流、标准错误流等),而不仅限于标准输出流。 函数原型
int fprintf ( FILE * stream, const char * format, ... );
头文件:<stdio.h> 参数:①stream:指向要写入的输出流关联的 FILE 结构体的指针;②format:格式化字符串,包含文本和格式说明符(如 %d、%s 等);③…:可变参数与格式字符串中说明符对应的数据 返回值:①成功:返回写入的字符总数(非负值);②失败:返回负值,并设置流的错误指示器
代码示例:
#include <stdio.h>
struct Stu
{
char name[30];
int age;
float score;
};
int main()
{
struct Stu b = {"zhangsan", 20, 85.5f};
FILE* fp = fopen("test.txt", "w");
if (fp == NULL)
{
perror("fopen\\n");
return 1;
}
fprintf(fp, "%s %d %.1f", b.name, b.age, b.score);
fclose(fp);
fp = NULL;
return 0;
}
4.7、fread函数
fread 函数用于从 stream 指向的 FILE 结构体关联的输入流中读取数据块,并将其存储到 ptr 指向的内存缓冲区中。它是以二进制形式读取的,将文件中的原始数据原样复制到内存中,不进行任何格式转换。
函数原型
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
头文件:<stdio.h> 参数:①ptr:指向内存区域的指针,用于存储从文件中读取的数据;②size:每个数据块的大小(以字节为单位);③count:要读取的数据块的数量;③stream:指向要读取的输入流关联的 FILE 结构体的指针 返回值:①成功:返回实际读取的数据块数量(等于 count);②到达文件末尾:返回值小于 count,并设置文件结束指示器;③读取错误:返回值小于 count,并设置流的错误指示器
注意事项: ①打开模式:必须使用二进制可读方式打开(如 “rb”、“rb+”); ②内存大小:ptr 指向的内存区域必须足够大,至少 size × count 字节; ③返回值检查:始终检查返回值是否等于 count,确认读取成功。
代码示例:
#include <stdio.h>
int main()
{
int data[5] = {0}; // 假设文件包含 5 个整数数据
// 打开文件(二进制读取模式)
FILE *fp = fopen("data.bin", "rb");
// 检查文件是否成功打开
if (fp == NULL)
{
perror("fopen");
return –1;
}
// 从文件中读取整数数据块
// data:存储位置 sizeof(int):每个4字节 5:读取5个元素
size_t num_read = fread(data, sizeof(int), 5, fp);
// 检查读取是否成功
if (num_read != 5)
{
if (feof(fp))
printf("已到达文件末尾\\n");
else if (ferror(fp))
printf("读取文件时发生错误\\n");
}
else
{
// 输出读取的数据
for (int i = 0; i < 5; i++)
{
printf("Data[%d]: %d\\n", i, data[i]);
}
}
fclose(fp);
fp = NULL;
return 0;
}
4.8、fwrite函数
fwrite 函数用于将数据块写入 stream 指向的 FILE 结构体关联的输出流中,是以二进制形式写入的。它将内存中的数据原样复制到文件中,不进行任何格式转换。 函数原型
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
头文件:<stdio.h> 参数:①ptr:指向要写入的数据块的指针(数据来源);②size:每个数据项的大小(以字节为单位);③count:要写入的数据项的数量;④stream : 指向要写入的输出流关联的 FILE 结构体的指针 返回值:①成功:返回实际写入的数据项数量(等于 count);②失败:返回值小于 count,并设置流的错误指示器
注意事项: ①打开模式:必须使用二进制可写方式打开(如 “wb”、“ab”、“wb+”); ②数据形式:以二进制形式写入,适合存储结构体、数组等原始数据; ③文本数据:写入文本数据时,注意换行符和编码问题。
代码示例:
#include <stdio.h>
int main()
{
int data[] = {1, 2, 3, 4, 5};
// 打开文件(二进制写入模式)
FILE *fp = fopen("data.bin", "wb");
// 检查文件是否成功打开
if (fp == NULL)
{
perror("fopen");
return –1;
}
// 将数据写入文件
// data:数据来源 sizeof(int):每个元素4字节 5:写入5个元素
if (fwrite(data, sizeof(int), 5, fp) != 5)
{
perror("fwrite");
return –1;
}
printf("成功写入 5 个整数到 data.bin\\n");
fclose(fp);
fp = NULL;
return 0;
}
4.9、feof函数
feof 函数检测 stream 指针指向的 FILE 结构体关联的输入流是否遇到文件末尾。当读取函数遇到文件末尾时,会在关联的流上设置一个文件结束指示器,feof 函数检测这个指示符是否被设置。 函数原型
int feof ( FILE * stream );
头文件:<stdio.h> 参数:stream:指向要检测的输入流关联的 FILE 结构体的指针 返回值:①非零值(真):已到达文件末尾;②0(假):未到达文件末尾
代码示例:
#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;
// 循环读取 10 次(文件只有 6 个字符)
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;
return 0;
}
4.10、ferror函数
ferror 函数检测 stream 指针指向的 FILE 结构体关联的流是否发生读/写错误。当读/写函数发生错误时,会在关联的流上设置一个错误指示符,ferror 函数检测这个指示符是否被设置。 函数原型
int ferror ( FILE * stream );
头文件:<stdio.h> 参数:stream:指向要检测的流关联的 FILE 结构体的指针 返回值:①非零值(真):发生了读/写错误;②0(假):没有发生错误
代码示例:
#include <stdio.h>
// 以写的形式打开文件后,再去读文件,就会发生错误
int main()
{
// 以 "w" 的形式打开文件(只写模式)
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;
return 0;
}
五、文件的随机读写
5.1、ftell函数
ftell 函数返回当前文件位置指示器相对于文件开头的偏移量。 函数原型
long int ftell ( FILE * stream );
头文件: <stdio.h> 参数:stream:指向要获取位置指示器的流关联的 FILE 结构体的指针 返回值:①成功:返回当前文件位置指示器相对于文件开头的偏移量(字节数);②失败(如文件流无效或文件不支持定位):返回-1L
注意事项: ①二进制文件:ftell 的返回值是相对于文件开头的精确字节数,非常可靠; ②文本文件:由于不同系统对换行符的处理不同(如 Windows 是 \\r\\n,Linux 是 \\n),ftell 的返回值可能不直接等于从文件开头计数的字符数。但在大多数实现中,它仍然可以安全地与 fseek 配合使用来记录和恢复位置。
代码示例:
#include <stdio.h>
int main()
{
FILE* pFile;
long size;
pFile = fopen("myfile.txt", "rb");
if (pFile == NULL)
{
perror("Error opening file");
return 1;
}
if (fseek(pFile, 0, SEEK_END) != 0)
{
perror("fseek failed");
fclose(pFile);
return 1;
}
size = ftell(pFile);
if (size == –1L)
{
perror("ftell failed");
fclose(pFile);
return 1;
}
fclose(pFile);
pFile = NULL;
printf("Size of myfile.txt: %ld bytes.\\n", size);
return 0;
}
5.2、fseek函数
fseek 函数用于移动文件位置指示器到指定位置,以便在文件中进行随机读写。 函数原型
int fseek ( FILE * stream, long int offset, int origin );
头文件: <stdio.h> 参数:①stream:指向要定位的文件流关联的 FILE 结构体的指针;②offset:偏移量(字节数),可为正数(向文件末尾方向移动)、负数(向文件开头方向移动)或 0(不移动);③origin:偏移的起始点(基准位置),取值如下:SEEK_SET:文件开头、SEEK_CUR:文件位置指示器的当前位置、SEEK_END:文件末尾 返回值:①成功:文件位置指示器被成功移动,返回 0;②失败:返回非零值(通常是 -1),如试图移动到无效位置
注意事项: ①二进制文件:fseek 的行为非常直观,可以精确地移动到任何字节位置; ②文本文件:由于换行符的转换(如 Windows 是 \\r\\n,Linux 是 \\n),fseek 的行为在某些环境下可能不确定。唯一被严格保证的用法是:将 offset 设为 0,origin 设为 SEEK_SET(移动到文件开头)、将 offset 设为 ftell() 的返回值(恢复到之前记录的位置)。
代码示例:
#include <stdio.h>
int main()
{
FILE* pFile;
pFile = fopen("example.txt", "wb");
if (pFile == NULL)
{
perror("fopen failed");
return 1;
}
fputs("abcdefg", pFile); // 写入 "abcdefg"
fseek(pFile, 2, SEEK_SET); // 从开头偏移 2 个字节
fputs("123", pFile); // 从偏移位置写入 "123"
fclose(pFile);
pFile = NULL;
return 0;
}
5.3、rewind函数
rewind 函数将文件位置指示器移动到文件的起始位置。 函数原型
void rewind ( FILE * stream );
头文件: <stdio.h> 参数:stream:指向要重置的文件流关联的 FILE 结构体的指针 返回值: 无(void)
注意事项: ①移动位置指示器:将文件位置指示器重置到文件开头; ②清除错误标志:同时清除文件结束指示器(feof)和错误指示器(ferror),状态重置后可重新开始操作; ③无返回值:rewind 不返回任何值,无法判断是否成功(与 fseek 不同)。
代码示例:
/* rewind example */
#include <stdio.h>
int main()
{
int n;
FILE* pFile;
char buffer[27];
pFile = fopen("myfile.txt", "w+");
if (pFile == NULL)
{
perror("fopen failed");
return 1;
}
// 写入 A~Z 共 26 个大写字母
for (n = 'A'; n <= 'Z'; n++)
fputc(n, pFile);
// 回到文件开头
rewind(pFile);
// 读取 26 个字符
fread(buffer, 1, 26, pFile);
fclose(pFile);
pFile = NULL;
buffer[26] = '\\0';
printf("%s\\n", buffer);
return 0;
}
六、文件的读写更新与缓冲区刷新
6.1、三种读写模式:“r+”、“w+”、“a+”
在文件的打开模式中有三种方式值得注意,分别是:“r+”、“w+”、“a+”。
| 解释 | 可读/可写 | 可读/可写 | 可读/可写 |
| 文件不存在时 | 打开失败 | 自动创建新文件 | 自动创建新文件 |
| 文件存在时 | 保留内容 | 清空内容 | 保留内容 |
| 初始文件位置指示器 | 文件开头 | 文件开头 | 文件末尾 |
| 写入是否覆盖原有数据 | 是(可定位覆盖) | 是(内容已清空,从头写入) | 否(默认在文件末尾追加) |
| 典型用途 | 修改文件部分内容 | 创建新文件或完全重写旧文件 | 在文件末尾追加数据(如日志记录) |
6.2、读写切换的关键要点
①写完文件后要继续读取,读取前必须先使用 fflush() 刷新缓冲区,确保数据已写入文件,再使用 fseek() 或 rewind() 重新定位文件位置指示器; ②读完文件后要继续写入,写入前必须使用 fseek() 或 rewind() 重新定位文件位置指示器。
代码示例:
#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',先定位文件位置指示器到索引 1
fseek(fp, 1, SEEK_SET);
int ch = fgetc(fp); // 读取字符 'b'
printf("%c\\n", ch); // 输出:b
// 在 'b' 的位置开始写入 "hello"
// 因为前面读取一个字符后,位置指示器指向了 'c'
// 需要从当前位置退回一个字符到 'b' 的位置
fseek(fp, –1, SEEK_CUR);
fputs("hello", fp);
// 关闭文件
fclose(fp);
fp = NULL;
return 0;
}
6.3、文件缓冲区
ANSI C 标准采用 “缓冲文件系统” 处理数据文件的。所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块 “文件缓冲区”。
输出方向:程序需要把内存中的数据向硬盘上的文件输出时,会先送到内存中的缓冲区,装满缓冲区后才一起送到硬盘上的文件中。
输入方向:从硬盘上的文件向程序的变量中读入数据时,从硬盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。
缓冲区的大小由 C 编译系统决定,缓冲被分为三种模式:无缓冲、行缓冲、全缓冲(完全缓冲)。缓冲模式不是 C 语言全局统一的,而是每个流独立拥有的属性。系统会根据流的类型(终端/文件/错误)选择合适的默认缓冲模式。
| 无缓冲 | 数据立即写入/读取,不经过缓冲区 | stderr |
| 行缓冲 | 遇到换行符 \\n 或缓冲区满时刷新 | stdout(终端) |
| 全缓冲(完全缓冲) | 缓冲区满、fflush、fclose 时刷新 | 文件流 |
6.4、fflush函数
fflush函数强制刷新 stream 指向的 FILE 结构体关联的流的缓冲区,确保数据写入底层设备。,确保数据写入底层设备。 当 stream 指向的 FILE 结构体关联的是输出流时:将缓冲区中未写入的数据立即写入文件。 当 stream 指向的 FILE 结构体关联的是输入流时:行为由具体实现决定,非 C 语言标准行为(可能清空输入缓冲区)。 当 stream 为 NULL 时:刷新所有打开的输出流。 函数原型
int fflush ( FILE * stream );
头文件: <stdio.h> 参数:stream:指向要刷新的流关联的 FILE 结构体的指针 返回值:①成功:0;②失败:EOF
注意事项: ①仅对输出流或更新流(最后一次操作为输出)有明确刷新行为; ②输入流的刷新行为不可移植(如清空输入缓冲区是非标准特性); ③程序正常终止(exit)或调用 fclose 时会自动刷新,但程序崩溃时缓冲区数据可能丢失。
代码示例:
#include <stdio.h>
#include <windows.h>
void delay(int s)
{
for (int i = 0; i < s; i++)
{
printf("倒计时:%d 秒\\n", s – i);
Sleep(1000); // 睡眠1秒
}
}
int main()
{
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fputs("abcdef", pf); // 先将数据放在输出缓冲区
printf("睡眠10秒 – 已经写数据了,打开 data.txt 文件,检查文件内容\\n");
delay(10); // 睡眠10秒
printf("刷新缓冲区\\n");
fflush(pf); // 刷新缓冲区时,才将输出缓冲区的数据写到文件
printf("再睡眠10秒 – 此时,再次打开 data.txt 文件,检查文件内容\\n");
delay(10); // 睡眠10秒
fclose(pf);
// 注:fclose 在关闭文件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}

