C 语言文件操作

目录

一、文件操作概述

(一)为什么使用文件?

(二)什么是文件?

1、程序文件

2、数据文件

3、文件名(文件标识)

二、文本文件与二进制文件

(一)核心差异

(二)二进制文件的验证

1、先创建文件,进行数据的写入

[2、在 VS 内部打开二进制文件](#2、在 VS 内部打开二进制文件)

(三)适用场景

1、文本文件

2、二进制文件

三、文件的打开与关闭

(一)流与标准流

1、流

2、如何理解流

3、标准流

4、示例

(二)文件指针

1、文件指针概述

2、文件指针的作用

(三)文件的打开与关闭

[1、fopen 函数](#1、fopen 函数)

[2、fclose 函数](#2、fclose 函数)

3、路径示例

4、实例代码

四、文件的读写操作

(一)顺序读写

[1、字符读写:fputc / fgetc](#1、字符读写:fputc / fgetc)

[2、字符串读写: fputs / fgets](#2、字符串读写: fputs / fgets)

[3、格式化读写:fprintf / fscanf](#3、格式化读写:fprintf / fscanf)

[4、二进制读写:fwrite / fread](#4、二进制读写:fwrite / fread)

(二)随机读写

1、fseek:定位文件指针

2、ftell:获取指针偏移量

3、rewind:重置指针到文件头

五、文件读取结束的判定

(一)核心原则

1、核心原因1

[2、核心原因 2](#2、核心原因 2)

[3、核心原因 3](#3、核心原因 3)

4、具体操作

(二)文本文件的判定

(三)二进制文件的判定

六、文件缓冲区

(一)具体流程归纳

(二)缓冲区的验证代码

(三)关键注意事项

1、刷新方式

2、数据安全

3、结论

七、文件操作的标准步骤流程

第一步:打开文件

第二步:对文件进行操作

第三步:关闭文件


一、文件操作概述

(一)为什么使用文件?

如果没有文件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运行程序,是看不到上次程序的数据的。

如果要将数据进行持久化的保存 ,我们可以使用文件。数据存入硬盘文件后,即使关闭程序 / 电脑,下次打开仍存在。

(二)什么是文件?

​ 磁盘(硬盘)上的文件是文件。

但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。

1、程序文件

**(1)包含文件类型:**源文件、目标文件、可执行文件

**(2)示例:**test.c(源文件)、test.obj(目标文件)、test.exe(可执行文件)

**(3)作用:**存储程序代码与运行指令

2、数据文件

**(1)包含文件类型:**程序读写的外部数据文件

**(2)示例:**data.txt(文本数据)、person.bin(二进制数据)

**(3)作用:**存储程序运行时的输入 / 输出数据

3、文件名(文件标识)

(1)组成结构:文件路径 + 文件名主干 + 文件后缀,以 C:\code\test.txt 为例说明:

|--------------|------------|------------------------------------------------------------|
| 组成部分 | 示例 | 细节说明 |
| 文件路径 | C:\code\ | Windows 中需用**\\转义(如"C:\\code\\test.txt"),避免与转义字符冲突 |
| 文件名主干 | test | 核心标识,自定义命名 |
| 文件后缀 | .txt | 指示文件类型,如
.c** (源文件).bin(二进制文件) |

(2)路径类型

|----------|-------------------------------------|------------------|
| 路径类型 | 示例 | 适用场景 |
| 绝对路径 | C:\Users\Desktop\test.txt | 文件不在程序运行目录时使用 |
| 相对路径 | ./test.txt(当前目录),也可以直接 test.txt | 文件在程序运行目录时使用 |

二、文本文件与二进制文件

(一)核心差异

根据数据的组织形式,数据文件被称为文本文件 或者二进制文件

数据在内存中以二进制的形式存储,如果不加转换的输出到外存的文件中,就是二进制文件。

如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件是文本文件。

一个数据在文件中是怎么存储的呢?

字符一律以 ASCII 形式存储;数值型数据既可以用 ASCII 形式存储,也可以使用二进制形式存储。

如有整数10000,如果以 ASCII 码的形式输出到磁盘,则磁盘中占用5个字节(每个字符⼀个字节),而二进制形式输出,则在磁盘上只占4个字节。

|----------|----------------------------------|---------------------------------------------------|
| 对比维度 | 文本文件 | 二进制文件 |
| 存储形式 | ASCII 码字符序列('1'/'0'/'0'/'0'/'0') | 内存二进制补码直接存储 (00000000 00000000 00100111 00010000) |
| 存储空间 | 5 字节(每个字符 1 字节) | 4 字节(int 类型默认大小) |
| 可读性 | 记事本 / VS 可直接打开 (人类可读) | 文本编辑器打开乱码(需 VS 二进制编辑器 ) |
| 转换逻辑 | 内存二进制 → ASCII 码**(存储前转换)** | 内存二进制 → 直接存储**(无转换)** |

(二)二进制文件的验证
1、先创建文件,进行数据的写入
cpp 复制代码
#include <stdio.h>
int main()
{
    int a = 10000;
    // 以二进制写模式打开文件
    FILE* pf = fopen("test.txt", "wb");  
    // 写4字节二进制数据(a的地址、单个数据大小4、写1次、文件指针)
    fwrite(&a, 4, 1, pf);  
    fclose(pf);
    pf = NULL;
    return 0;
}
2、在 VS 内部打开二进制文件
(三)适用场景
1、文本文件

当需求满足 "需人类直接阅读 / 修改" 或**"需跨平台通用"** 时,文本文件是首选,典型场景包括:源代码文件 (如.c)、配置文件 (如config.txt)、日志文件(如log.txt)等。

2、二进制文件

当需求满足**"无需人类直接读写"**且 "需高效存储 / 处理" 时,二进制文件是首选,典型场景包括:可执行程序与库 (.exe/.so)、多媒体文件(如.jpg/.mp3/.mp4)等。

三、文件的打开与关闭

(一)流与标准流
1、流

我们程序的数据需要输出到各种外部设备 ,也需要从外部设备获取数据 ,不同的外部设备的输入输出操作各不相同。为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河。

C程序针对文件、画面、键盘等的数据输入输出操作都是通过流操作的。

一般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后操作。

这样解释,还是有点抽象,我们可以用更通俗的方式去理解它。

2、如何理解流

假设你家没水管,要从楼下的水龙头接水到楼上的桶里,你没法 "把一整池水瞬间隔空挪到桶里",只能:

① 拿根管子(通道)连到水龙头;

② 打开龙头,水一点一点、连续不断地顺着管子流进桶里;

③ 水流没断之前,你没法直接 "抓着某一滴水" 改它的样子,只能等它流到桶里(存成文件)后再处理。

计算机里的 "流",本质就是这个**"管子 + 连续水流" 的组合** ------ 它是数据在 "来源"(比如硬盘文件、键盘输入)和 "目的地"(比如内存、屏幕显示)之间,连续传输的 "通道 + 数据形态"

我们再来一个更简单的例子。

你买了个 10 斤重的大包裹(比如一整箱书),快递员要把它从仓库(硬盘里的文件)送到你家(电脑内存 / 屏幕)。

但有个问题:快递员的电瓶车只能装 3 斤的东西(内存空间不够装整个大文件),没法一次扛走 10 斤。

这时候快递员怎么办?他不硬扛整个包裹,而是拆成 4 次送:第一次抱 3 斤,送到你家放下;回头再抱 3 斤,再送;再抱 3 斤,再送;最后剩 1 斤,送完。

买书肯定是用来读的,所以一开始的3斤书,你并不会在快递员回仓库并送书的路上读完,对你来说,你看到的是 "书一直在往家里来,没停过"------ 这就是 "流" 要的 "连续":不是 "单个数据块没间隙",而是 "整个传输过程没中断,数据块一个接一个,不会让你等"

你看视频时,前 10 秒的画面刚播到第 5 秒,手机就已经在偷偷接 "第 11-20 秒" 的画面数据了;等前 10 秒播完,第 11-20 秒的已经准备好了,无缝接上;同时手机又在接 "第 21-30 秒" 的 ------ 全程没有 "等数据" 的空挡,你就觉得视频是 "连续流畅" 的,这就是流的 "连续" 本质。

要是真的 "断了"(比如网络卡了),你就会看到 "转圈缓冲"------ 这其实就是 "流的连续被打破了":后面的数据块没跟上,前面的播完了,只能停下来等。

所以总结一下:

"流的连续",不是 "数据块粘成一整根不断",就像水不是 "一整根水柱",而是无数水滴连起来;数据块应该是一个接一个地传,中间没有 "等半天" 的空挡,最终呈现出 **"没断过"**的效果"。

现在我们从稍微专业一点的角度来阐述一下。

你寄快递时,不只是有快递员**(工具),还得有 "从寄件点到收件点的路线(通道)** "、"按重量拆分包裹的规则**(数据分块)** "、"先送急件的优先级**(传输顺序)**"------ 这些加起来才是完整的 "快递系统"。

**所以"流" 是计算机里一套 "帮数据分段、有序、不堵车传输的专用系统",**它不只是帮你 "传数据",还自带了 3 个关键 "规则",让数据传得更稳、更省空间。

必须 "分小块传":不管文件多大,都拆成小片段(比如每次 1KB),避免内存装不下;

② 必须 "按顺序传":先传的小块数据会按顺序拼接,不会乱(比如视频不会先传结尾、再传开头);

③ 必须 "盯着接收方":如果接收方(比如内存)暂时装不下,"流" 会暂停传输,等有空位了再继续,不会让数据乱掉。

★ 即流就是 "通道" 和 "数据" 的绑定体,缺了任何一个都不叫 "流"。通道负责 "怎么传、传到哪",数据负责 "传什么",两者绑在一起,才实现了文件操作里 "连续、有序传数据" 的目的。

3、标准流

那为什么我们从键盘输入数据,向屏幕上输出数据,并没有打开流呢?那是因为C语言程序在启动的时候,默认打开了3个流。

(1)stdin - 标准输入流

在大多数的环境中从键盘输入,scanf 函数就是从标准输入流中读取数据

(2)stdout - 标准输出流

大多数的环境中输出至显示器界面,printf 函数就是将信息输出到标准输出流中

(3)stderr - 标准错误流

大多数环境中输出到显示器界面。

这是默认打开了这三个流,我们使用 scanf、printf 等函数就可以直接进行输入输出操作的。

stdin、stdout、stderr 三个流的类型是:FILE * ,通常称为文件指针C语言中,就是通过 FILE* 的文件指针来维护流的各种操作的。

那不是说了流 = 通道 + 数据吗?这也没有数据呀,为什么能称为流呢?

C 语言默认打开的三个流(stdin、stdout、stderr)确实 "一开始没数据",但它们的关键在于:已经提前搭好了 "通道",就等数据来 "流" 了

可以把它们想象成 "三根提前接好的水管" :

① stdin(标准输入流):就像家里接好的 "进水管",一头连在键盘上,另一头连在程序里。虽然刚一开始没按键盘时管子里没水(数据),但管子已经架好了,你一敲键盘,字符就会顺着这根管子 "流" 进程序。

② stdout(标准输出流):像连到屏幕的 "出水管",程序想打印东西时,不用临时架管子,直接往这根现成的管子里 "放水"(比如 printf 输出的内容),数据就会顺着管子流到屏幕上显示。

③ stderr(标准错误流):另一根连到屏幕的 "备用出水管",专门用来流错误信息(比如程序崩溃提示)。它和 stdout 是两根独立的管子,保证错误信息不会被正常输出 "堵在路上"。

为什么 C 语言要默认打开这三个 "空管子"?

因为几乎所有程序都需要 "读键盘、写屏幕、报错误",提前把通道建好,程序员就不用每次写程序都手动 "架管子" 了 ------ 就像家里装修时提前把水电管埋好,入住后直接用就行,不用临时凿墙接水管。

所以这三个流的状态是:"通道已就绪,数据随时可流"。它们的存在不是因为 "有数据",而是因为 "提前备好了传输数据的通道",这正是 "流 = 通道 + 数据" 中 "通道" 的核心价值 ------ 先有通道,数据才能按需流动。

★ 所以更准确的说法是:"流是能传输数据的管道",这个 "流 = 管道 + 数据"中,这个数据更像是一种能力,一种传数据的能力,流它可以处于 "空着 (没数据)" 或 "有数据流动两种状态"

4、示例
cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
int main() {
    
    //fgetc(stdin)可直接从键盘读字符(一个)
    int ch = fgetc(stdin);  // 从stdin(键盘)读字符
   
    //fputc('a', stdout)可直接向屏幕写字符
    fputc(ch, stdout);     // 向stdout(屏幕)写字符
    
    return 0;
}
(二)文件指针
1、文件指针概述

缓冲文件系统中,关键的概念是**"文件类型指针"** ,简称**"文件指针"**。

每个被使用的文件都在内存中开辟了⼀个相应的文件信息区,用来存放文件的相关信息,如文件的名字,文件状态及文件当前的位置等。

这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名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类型

不同的 C 编译器的 FILE 类型包含的内容不完全相同,但是大同小异:

|------------|------------------------------|--------------------------|
| 编译器版本 | FILE 结构差异 | 共性特征 |
| VS2013 | 明确定义_iobuf结构体,包含 8 个成员 | 均维护文件名称、状态、缓冲区、当前位置等核心信息 |
| GCC(Linux) | 成员名称不同 (如_IO_read_ptr替代_ptr) | 功能逻辑一致,仅底层实现细节差异 |

每当打开一个文件的时候,系统会根据文件的情况自动创建⼀个 FILE 结构的变量,并填充其中的信息,使用者不必关心细节。

2、文件指针的作用

一般都是通过一个 FILE 的指针来维护这个 FILE 结构的变量,这样使用起来更加方便。即通过 FILE* 指针访问 FILE 结构体 (文件信息区),间接操作文件

下面我们可以创建一个 FILE* 的指针变量:

FILE* pf; // 文件指针变量

定义 pf 是一个指向 FILE 类型数据的指针变量。可以使 pf 指向某个文件的文件信息区(是⼀个结构体变量)。

通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够间接找到与它关联的文件

(三)文件的打开与关闭

文件在读写之前应该先打开文件 ,在使用结束之后应该关闭文件

在编写程序的时候,在打开文件的同时,都会返回一个 FILE* 的指针变量指向该文件,也相当于建立了指针和文件的关系。

ANSI C 规定使用 fopen 函数来打开文件, fclose 来关闭文件。

1、fopen 函数

(1)函数原型与参数

FILE *fopen(const char *filename, const char *mode);

**① filename:**文件路径(绝对 / 相对路径);

**② mode:**打开模式,决定文件读写权限。

(2)打开模式的对比

文件使用方式 含义 如果指定文件不存在
"r"(只读) 为了输入数据,打开一个已经存在的文本文件 出错
"w"(只写) 为了输出数据,打开一个文本文件 建立一个新的文件
"a"(追加) 向文本文件尾添加数据 建立一个新的文件
"rb"(只读) 为了输入数据,打开一个二进制文件 出错
"wb"(只写) 为了输出数据,打开一个二进制文件 建立一个新的文件
"ab"(追加) 向一个二进制文件尾添加数据 建立一个新的文件
"r+"(读写) 为了读和写,打开一个文本文件 出错
"w+"(读写) 为了读和写,建立一个新的文件 建立一个新的文件
"a+"(读写) 打开一个文件,在文件尾进行读写 建立一个新的文件
"rb+"(读写) 为了读和写打开一个二进制文件 出错
"wb+"(读写) 为了读和写,新建一个新的二进制文件 建立一个新的文件
"ab+"(读写) 打开一个二进制文件,在文件尾进行读和写 建立一个新的文件

以**"r"** 开头的,文件存在时,指针指向起始位置;以**"w"** 开头的,文件存在时,会清空原有内容;以**"a"**开头的,指针指向文件末尾。

2、fclose 函数

(1)函数原型与参数

int fclose(FILE *stream);

**① stream:**文件指针变量

(2)关键注意事项

**① 必须关闭文件:**fclose会自动刷新缓冲区,未关闭可能导致数据丢失;

**② 指针置空:**关闭后需将指针设为NULL,避免后续误操作

**③ 返回值检查:**失败返回EOF(-1),可用于排查关闭错误(如文件已被其他程序占用)

3、路径示例

(1)绝对路径(Windows):"C:\\Users\\Desktop\\test.txt"(双反斜杠转义);

(2)相对路径

① 当前目录:"./test.txt"(可省略./,直接写"test.txt");

② 上一级目录:"../test.txt";

③ 上两级目录:"../../test.txt"。

4、实例代码
cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
int main()
{
	FILE* pFile;
	//打开⽂件
	pFile = fopen("myfile.txt", "w");
	//⽂件操作
	if (pFile != NULL){
		fputs("fopen example", pFile);
		//关闭⽂件
		fclose(pFile);
		pFile = NULL;
	}else {
		// 明确提示文件打开失败
		printf("Failed to open the file!\n");
	}
	return 0;
}

四、文件的读写操作

(一)顺序读写

因为 fread 与 fwrite 适用于文件输入流与文件输出流,即为文件流的范畴,所以仅凭C语言初始打开的标准流,这两个函数无法被适用。

所以只有当 fopen 函数调用成功时,才会打开文件流,同时返回一个指向该流的 FILE* 指针;如果 fopen 失败,只会得到一个 NULL 指针,此时文件流并没有被打开。

即使用仅适用于文件流的函数时,需要 fopen 打开文件成才可以使用

1、字符读写:fputc / fgetc

(1)fputc:" 向流写 1 个字符,成功返回字符,失败返回 EOF**"**

① 函数原型

int fputc(int character, FILE *stream);

② 参数理解

返回值为 int,成功时,返回写入的字符(即参数 character 的低 8 位,范围 0~255);失败时,返回 EOF(通常定义为 -1)。

参数 1 为 int character,要写入的字符,虽然声明为 int,但实际只使用低 8 位,即 0~255 的字符编码。

参数 2 为 FILE *stream,指向目标流(FILE 类型指针),可以是标准流(如 stdout)或通过 fopen 打开的文件流。

(2)fgetc:" 从流读 1 个字符,返回 ASCII 码**"**

① 函数原型

int fgetc(FILE *stream);

② 参数理解

返回值类型为 int,成功兼容 EOF 的设计,如果是 char 类型,127 以上的字符编码会被解读为负数,恰好可能与 EOF(通常是 -1)冲突。

当成功正常读取到字符时,返回该字符的 ASCII 码(范围 0~255);当读取到文件末尾或发生错误时,返回特殊值 EOF(通常定义为 -1)。

参数为 FILE *stream,指向一个已打开的流(FILE 类型指针),可以是标准流(如 stdin)或通过 fopen 打开的文件流。

(3)实际操作案例

"打开流 → 操作流 → 关闭流 (同时文件指针置为空)"

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>

int main() {
    // 第一步:向文件写入'A'到'Z'
    FILE* pf_write = fopen("letters.txt", "w");
    if (pf_write == NULL) {
        perror("写入时打开文件失败");
        return 1;
    }

    // 循环写入大写字母
    for (char c = 'A'; c <= 'Z'; c++) {
        fputc(c, pf_write);
    }

    // 关闭写入流
    fclose(pf_write);
    pf_write = NULL;

    // 第二步:从文件读取并打印内容
    FILE* pf_read = fopen("letters.txt", "r");
    if (pf_read == NULL) {
        perror("读取时打开文件失败");
        return 1;
    }

    // 读取并打印每个字符
    int ch;  // 用int类型接收,兼容EOF
    printf("文件内容:");
    while ((ch = fgetc(pf_read)) != EOF) {
        putchar(ch);
    }
    printf("\n");  // 输出换行,使结果更整洁

    // 关闭读取流
    fclose(pf_read);
    pf_read = NULL;

    return 0;
}
2、字符串读写: fputs / fgets

(1)fputs

用于写字符串,不自动加'\n',需手动添加换行。

① 函数原型

int fputs(const char *str, FILE *stream);

② 参数理解

返回值为 int,成功时,返回一个非负值(通常是 0 或其他正数,具体值因实现而异);失败时,返回 EOF(通常定义为 -1)。

参数 1 为 const char *str,指向要写入的字符串(以空字符 '\0' 结尾),const 表示该字符串在函数内不会被修改。

参数 2 为 FILE *stream,指向目标流(FILE 类型指针),可以是标准流(如 stdout)或文件流。

(2)fgets

读 1 行字符串,最多读 n-1 个字符(留 1 字节存'\0'),遇'\n'或文件尾停止;其中的 n 为文件中字符的个数。

① 函数原型

char *fgets(char *str, int num, FILE *stream);

② 参数理解

返回值为 char *,成功时,返回指向 str 的指针(即第一个参数);失败或读取到文件末尾时,返回 NULL(此时 str 中的内容可能不完整)。

参数 1 为 char *str,指向一个字符数组的指针,用于存储读取到的字符串,包括结尾的空字符 '\0'。

参数 2 为 int num,要读取的最大字符数(包含结尾的空字符)。实际最多会读取 num-1 个字符,因为最后会自动添加 '\0' 来终止字符串。

参数 3 为 FILE *stream,指向要读取的流(FILE 类型指针),可以是标准流(如 stdin)或文件流。

③ 使用时的细节

文件内部有一个 "位置指针"(文件指针),它会随着读写操作自动移动,记录当前的操作位置。当你用 fgets 读完第一行后,位置指针已经指向了第二行的开头,所以第二次调用 fgets 时,自然就会读取第二行内容。

(3)实际操作案例

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>

int main()
{
    // 写文件
    FILE* pf = fopen("lines.txt", "w");
    if (pf == NULL) { 
        perror("fopen"); 
        return 1; 
    }
    fputs("Hello World\n", pf);  // 手动加'\n'换行
    fputs("C File Operation\n", pf);
    fclose(pf);

    // 读文件
    pf = fopen("lines.txt", "r");
    if (pf == NULL) { 
        perror("fopen"); 
        return 1; 
    }
    char buf[100];  // 存储读取的字符串
    // 读第一行(遇'\n'停止)
    fgets(buf, 100, pf);
    printf("%s", buf);  // 输出"Hello World"(含换行)
    // 读第二行
    fgets(buf, 100, pf);
    printf("%s", buf);  // 输出"C File Operation"

    fclose(pf);
    pf = NULL;
    return 0;
}
3、格式化读写:fprintf / fscanf

(1)fprintf

与前面 fputc 以及 fputs 一样,都是将内容输出到文件中。

① 函数原型

int fprintf(FILE *stream, const char *format, ...);

② 参数理解

返回值为 int,成功时,返回实际输出的字符总数,包括所有普通字符和转换后的字符 (格式符占位后);失败时,返回负数(通常是 EOF,即 -1)。

参数 1 为 FILE *stream,指向目标输出流(FILE 类型指针),可以是标准流(如 stdout,对应屏幕输出)或文件流(通过 fopen 打开的文件)。

参数 2 为 const char *format,格式控制字符串,包含普通字符和格式说明符(如 %d、%s、%f 等),用于指定输出的格式。

参数 3 为 ...(可变参数),一系列要输出的数据,数量和类型需与格式字符串中的说明符一一匹配。

说白了它与 printf 的不同点是,多了一个文件指针,即内容输出的位置,从而造成内容的输出位置不同 ------ printf 将程序的内容或文本输出到终端 ,fprintf 将程序的内容或文本输出到文件

其它的地方都是差不多的,第二个参数,可以是 纯普通字符,也可以纯格式符,也可以普通字符与格式符一起。第三个参数,如果没有格式符,就不需要存在,与 printf 是一样的。

它可以是 fprintf(pf, "%s %d %.1f", s.name, s.age, s.score);,也可以是 fprintf(pf, "111111")。

(2)fscanf

与前面的 fgetc 与 fgets 一样,都是将文件中的内容输入到程序中。

① 函数原型

int fscanf(FILE *stream, const char *format, ...);

② 参数理解

返回值为 int,成功时,返回成功匹配并赋值的变量个数;失败或读取到文件末尾时,返回 EOF(通常定义为 -1)。

参数 1 为 FILE *stream,指向要读取数据的流(FILE 类型指针),可以是标准流(如 stdin)或文件流。

参数 2 为 const char *format,格式控制字符串,包含格式说明符(如 %d、%s、%f 等),用于指定读取数据的类型和格式。

参数 3 为 ...(可变参数),一系列指针,指向用于存储读取结果的变量(数量和类型需与格式字符串中的说明符匹配)。

这个与 scanf 与 fscanf 的不同点也是多了一个文件指针,从而造成内容输出的位置不同------scanf 是从键盘输入数据到程序 ,fscanf 是从文件输入数据到程序

第二个参数就是格式符与普通字符,两者可以一起出现,也可以单独出现。格式符用于匹配文件中对应数据,匹配成功了,才能被写入第三个参数的可变参数中,举个例子:

cpp 复制代码
fscanf(pf, "%s %d %f", tmp.name, &tmp.age, &tmp.score);

只有**"%s"** 在 pf 指针对应的文件中找到了对应的内容,那么这个内容就会被写入到 tmp.name中去,如果第二个参数,没有任何的格式符,那么这个函数基本就没有意义了,因为它最核心的作用就是用来将文件的数据输出到程序中。不然它就只可以检查文件开头是否匹配该字符串

第二个参数里面的普通字符,可以理解为是匹配器

就比如文件里面的第一行数据为:"用户:张三,20,95.5",如果你要一开始就用**"%s"**去获取的话,就会获取到 "用户:张三",这与我们要获取的 "张三" 是有出入的。

因为格式符会从第一个非空格且符合的字符开始读取,到空格/不符合的字符停止。此时如果想要获取 "张三" 这个数据的话,就要如下操作:

cpp 复制代码
fscanf(pf, "用户:%s %d %f", tmp.name, &tmp.age, &tmp.score);

此时代码中的 "用户:" 会与文件中的 "用户:" 匹配,然后 **"%s"**就会从张三的位置开始读取,此时就可以得到正确的 "张三"。

所以普通字符的作用就是在输入流中找到匹配的字符,确保 fscanf 定位到正确的数据起始位置(类似 "锚点"),但这些普通字符本身不会被存入任何变量

格式符的作用就是从输入流中提取数据,并按照指定类型存入对应的变量中,最终程序能使用的就是这些被提取的数据。

总结一下:格式字符串中的普通字符是 "匹配器",格式符是 "提取器"

③ 如何获得指定行数的数据?

我们以只读的方式打开文件,那么此时光标在第一行的开头,如果我们是想要获取第二行的数据,那么不能直接执行上面的代码,因为这样获取的就是第一行的数据。

此时我们可以------用带**" * "** 的格式符读取整行数据但不存储,实现跳过fscanf(pf, "%*s %*d %*f");,如果一次要跳过多行,就可以使用循环,控制这个循环就可以实现获得指定行的数据

cpp 复制代码
#define SKIP_LINES 2     // 要跳过的行数(这里跳过前2行)

//跳过前SKIP_LINES行(这里跳过2行)
for (int i = 0; i < SKIP_LINES; i++)
{
    // 用带*的格式符读取整行数据但不存储,实现跳过
    fscanf(pf, "%*s %*d %*f");
}

(3)实际操作案例

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#define STUDENT_COUNT 5  // 学生数量

// 定义学生结构体
typedef struct {
    char name[20];
    int age;
    float score;
} Student;

int main() {
    // 定义结构体数组(一次性创建5个学生变量)
    Student students[STUDENT_COUNT] = {
        {"张三", 20, 95.5f},
        {"李四", 21, 88.0f},
        {"王五", 19, 92.3f},
        {"赵六", 22, 77.5f},
        {"钱七", 20, 89.0f}
    };

    // 1. 批量写入文件
    FILE* pf = fopen("students.txt", "w");
    if (pf != NULL) {
        // 循环写入数组中所有学生的数据
        for (int i = 0; i < STUDENT_COUNT; i++) {
            fprintf(pf, "%s %d %.1f\n",
                students[i].name,
                students[i].age,
                students[i].score);
        }
        fclose(pf);
    }

    // 2. 批量从文件读取到新的结构体数组
    Student readStudents[STUDENT_COUNT];
    pf = fopen("students.txt", "r");
    if (pf != NULL) {
        // 循环读取数据到数组
        for (int i = 0; i < STUDENT_COUNT; i++) {
            fscanf(pf, "%s %d %f",
                readStudents[i].name,
                &readStudents[i].age,
                &readStudents[i].score);
        }
        fclose(pf);
    }

    // 3. 批量打印读取结果
    printf("所有学生数据:\n");
    for (int i = 0; i < STUDENT_COUNT; i++) {
        printf("第%d个:%s %d %.1f\n",
            i + 1,
            readStudents[i].name,
            readStudents[i].age,
            readStudents[i].score);
    }

    return 0;
}
4、二进制读写:fwrite / fread

(1)fwrite

fwrite 是 C 语言中用于二进制方式写入数据的函数,即将程序中的数据以二进制的形式输出到文件中。

① 函数原型

size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);

② 参数理解

返回值为 size_t,成功时,返回实际写入的元素个数(等于 nmemb);失败或写入过程中出错时,返回小于 nmemb 的值(需通过 ferror 判断具体错误)。

参数1是 ptr,指向要写入的数据的指针,可以是数组、结构体等任意数据的地址。

参数 2 是 size,单个数据元素的大小(单位:字节),通常用 sizeof 获取,如 sizeof(int)。

参数 3 是 nmemb,要写入的数据元素的个数。

参数 4 是 stream,目标文件流(FILE* 类型指针,通过 fopen 打开)。

③ 代码示例

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>

typedef struct {
    char name[20];
    int age;
    float score;
} Student;

int main() {
    Student s = { "张三", 20, 95.5f };
    FILE* pf = fopen("student.bin", "wb");  // 注意用"wb"模式(二进制写入)

    if (pf != NULL) {
        // 写入1个Student类型的元素,每个元素大小为sizeof(Student)
        size_t ret = fwrite(&s, sizeof(Student), 1, pf);

        if (ret == 1) {
            printf("写入成功\n");
        }
        else {
            printf("写入失败\n");
        }
        fclose(pf);
    }
    return 0;
}

(2)fread

fread 以二进制形式从文件中读取数据,将文件中的原始字节直接复制到内存缓冲区中,还原成程序中可以使用的数据(如结构体、数组等)。

它与 fwrite 是 "逆操作"------fwrite 把内存数据写入文件,fread 把文件数据读回内存。

fread 读取的是 "字节",可以读任何文件,但只有读取 fwrite 写入的二进制文件时,才能还原出程序中的原始数据。

普通文本文件(fprintf 写入)存储的是 "字符的 ASCII 码",而非数据的原始二进制,因此用 fread 读取后无法直接得到原来的数据。

即 fwrite 与 fread 配对使用,fprintf 与 fscanf 配对使用,不要混搭。

① 函数原型

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

② 参数理解

返回值为 size_t,成功时,返回实际读取到的元素个数(等于 nmemb,如果文件中数据足够);若读取到文件末尾或出错,返回小于 nmemb 的值(可通过 feof 或 ferror 区分是到末尾还是出错)。

参数 1 为 ptr,指向具体变量的指针,用于存储读取到的数据,如结构体变量、数组的地址。

参数 2 为 size,单个数据元素的大小(单位:字节),通常用 sizeof 获取。

参数 3 为 nmemb,要读取的数据元素的个数,需与写入时的 nmemb 对应。

参数 4 为 stream,源文件流(FILE* 类型指针,通过 fopen 以二进制读模式 "rb" 打开)。

(3)实际操作案例

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <string.h>

typedef struct {
    char name[20];  // 20字节
    int age;        // 4字节
    float score;    // 4字节(共28字节)
} Student;

// 二进制方式:用fwrite写入,fread读取
void binary_operation() {
    Student s_write = { "张三", 20, 95.5f };
    Student s_read;
    FILE* pf;

    // 1. 二进制写入
    pf = fopen("binary.bin", "wb");
    if (pf) {
        fwrite(&s_write, sizeof(Student), 1, pf);  // 写入28字节原始二进制
        fclose(pf);
        printf("二进制写入完成\n");
    }

    // 2. 二进制读取
    pf = fopen("binary.bin", "rb");
    if (pf) {
        // 读取28字节,原样还原到s_read
        fread(&s_read, sizeof(Student), 1, pf);
        fclose(pf);
        printf("二进制读取结果:%s %d %.1f\n",
            s_read.name, s_read.age, s_read.score);
    }
}

// 文本方式:用fprintf写入,fscanf读取
void text_operation() {
    Student s_write = { "李四", 21, 88.0f };
    Student s_read;
    FILE* pf;

    // 1. 文本写入
    pf = fopen("text.txt", "w");
    if (pf) {
        fprintf(pf, "%s %d %.1f",  // 转换为文本字符写入
            s_write.name, s_write.age, s_write.score);
        fclose(pf);
        printf("文本写入完成\n");
    }

    // 2. 文本读取
    pf = fopen("text.txt", "r");
    if (pf) {
        fscanf(pf, "%s %d %f",  // 解析文本字符为数据
            s_read.name, &s_read.age, &s_read.score);
        fclose(pf);
        printf("文本读取结果:%s %d %.1f\n",
            s_read.name, s_read.age, s_read.score);
    }
}

// 错误示例:用fread读取文本文件
void wrong_operation() {
    Student s_read;
    FILE* pf = fopen("text.txt", "rb");  // 打开文本文件
    if (pf) {
        fread(&s_read, sizeof(Student), 1, pf);  // 用fread读取
        fclose(pf);
        printf("错误操作(fread读文本)结果:%s %d %.1f\n",
            s_read.name, s_read.age, s_read.score);  // 乱码或错误数据
    }
}

int main() {
    binary_operation();  // 正确:二进制读写
    printf("\n");
    text_operation();    // 正确:文本读写
    printf("\n");
    wrong_operation();   // 错误:fread读文本
    return 0;
}
(二)随机读写

通过调整文件指针位置实现 "跳读 / 跳写",核心函数为 fseek、ftell、rewind。

1、fseek:定位文件指针

fseek 是 C 语言中用于移动文件指针位置的函数,可灵活控制文件读写的位置。

因为它的引动是以字节为单位,所以在非 char 类型的文本文件 中使用时需要额外注意,同时在二进制文件 中使用更可靠。

(1)函数原型

int fseek(FILE *stream, long int offset, int origin);

(2)参数理解

返回值为 int,成功移动指针时,返回 0;失败时(如偏移量超出文件范围),返回非 0 值。

参数 1 为 stream,目标文件流(FILE* 类型指针,通过 fopen 打开)。

参数 2 为 offset ,偏移量(长整数),表示从 whence 位置移动的字节数(正数表示向后移,负数表示向前移)。

参数 3 为 whence,起始位置(基准点),必须是以下三个宏之一:

SEEK_SET:从文件开头开始计算偏移(偏移量必须非负);

SEEK_CUR:从当前文件指针位置开始计算偏移;

SEEK_END:从文件末尾开始计算偏移(偏移量为负时表示向前移动)。

如果参数为 SEEK_END,指针从文件末尾向后移动 20 字节,最终指向 120 字节处(100 + 20 = 120),此时写入数据会从 120 字节处开始,原 100~120 字节之间会形成 "空洞"(用 0 填充,不占用实际磁盘空间,但逻辑上存在)。

(3)实际操作案例

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
int main() 
{
    // 写入数据到 test.txt
    FILE* pf_write = fopen("test.txt", "w");
    if (pf_write == NULL) 
    {
        perror("fopen (write)");
        return 1;
    }
    fprintf(pf_write, "abcdef");
    fclose(pf_write);
    pf_write = NULL;

    // 读取并定位数据
    FILE* pf = fopen("test.txt", "r");
    if (pf == NULL) 
    {
        perror("fopen (read)");
        return 1;
    }

    // 方式1:从文件头偏移5字节(SEEK_SET)
    fseek(pf, 5, SEEK_SET);
    // 方式2:从当前位置(初始0)偏移5字节(SEEK_CUR)
    // fseek(pf, 5, SEEK_CUR);
    // 方式3:从文件尾偏移-1字节(SEEK_END,文件尾是6,6-1=5)
    // fseek(pf, -1, SEEK_END);

    int ch = fgetc(pf);
    printf("%c\n", ch);  // 输出'f'

    fclose(pf);
    pf = NULL;
    return 0;
}
2、ftell:获取指针偏移量

ftell 是 C 语言标准库中的一个函数,用于获取文件指针的当前位置。

在使用 fseek 函数对文件指针进行移动后,需要确认是否移动到了预期的位置,就可以通过 ftell 函数获取移动后的文件指针位置

先使用 fseek 函数将文件指针移动到文件末尾(fseek(stream, 0, SEEK_END) ),然后调用 ftell 函数,返回值就是文件的总字节数

(1)函数原型

long int ftell(FILE *stream);

(2)参数理解

返回值类型为 long int ,如果调用成功,ftell 函数会返回一个 long int 类型的值,表示文件指针相对于文件开头的字节偏移量 ,即文件指针当前位置距离文件起始位置的字节数。如果调用失败(比如文件流指针无效),则返回 -1L ,并会设置全局变量 errno 来指示具体的错误原因

参数为 stream,指向 FILE 类型的指针,代表需要获取文件指针位置的文件流。这个文件流是通过 fopen 函数打开文件后得到的返回值。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>

int main() 
{
    FILE* pf = fopen("test.txt", "r");
    if (pf == NULL) 
    {
        perror("fopen");
        return 1;
    }

    // 读取文件中的一部分内容
    char buffer[10];
    fread(buffer, sizeof(char), 5, pf);

    // 获取当前文件指针的位置
    long int current_position = ftell(pf);
    printf("当前文件指针位置相对于文件开头的偏移量为:%ld 字节\n", current_position);

    // 将文件指针移动到文件末尾
    fseek(pf, 0, SEEK_END);
    long int file_size = ftell(pf);
    printf("文件大小为:%ld 字节\n", file_size);

    fclose(pf);
    return 0;
}
3、rewind:重置指针到文件头

等价于 fseek(stream, 0, SEEK_SET),快速将指针重置到起始位置。

(1)函数原型

void rewind(FILE *stream);

(2)参数理解

rewind 函数的返回类型是 void,即该函数不返回任何值。

参数为 stream,是指向 FILE 类型的指针,代表需要操作的文件流。这个文件流是通过 fopen 函数打开文件后得到的返回值。

五、文件读取结束的判定

(一)核心原则

在文件读取过程中,不能用 feof 函数的返回值直接来判断文件的是否结束。

feof 的作用是:当文件读取结束的时候,判断是读取结束的原因是否是:遇到文件尾结束。

1、核心原因1

feof 的 "滞后置位" 特性,导致多执行一次循环。(最常见问题)

(1)具体解释

feof 的本质是**"判断上一次读写操作是否因为遇到文件尾而失败"**,而不是 "判断下一次读写是否会失败"。它的置位(从 0 变 1)是 "滞后" 的 ------ 必须先执行一次 "失败的读写操作",feof 才会被标记为 "文件尾"。

这就导致直接用 while (!feof(pf)) 时,必然会多执行一次循环,进而引发问题。

(2)具体案例

假设 test.txt 内容为:

cpp 复制代码
第一行
第二行

错误用法代码:

cpp 复制代码
char buf[100] = {0};  // 初始化buf为0
FILE* pf = fopen("test.txt", "r");
while (!feof(pf)) 
{  
    // 直接用feof判断
    fgets(buf, 100, pf);  // 读取一行
    printf("读取:%s", buf);  // 打印
}

第一次循环:feof(pf) 是 0(未置位),进入循环 → fgets 读取 "第一行" → 打印 "第一行";

第二次循环:feof(pf) 仍是 0(还没到文件尾),进入循环 → fgets 读取 "第二行" → 打印 "第二行";

第三次循环:feof(pf) 还是 0(此时指针在文件尾,但未执行过失败的读写),进入循环 → fgets 尝试读取,发现文件尾,返回 NULL(读取失败),但 buf 仍保留上一次的 "第二行" 数据 → 打印 "第二行"(重复打印!);

第四次判断:feof(pf) 被置为 1,退出循环。

结果:本应打印 2 行,实际打印 3 行(多打印一次最后一行),出现数据重复的异常。

2、核心原因 2

无法区分 "文件尾" 和 "读写错误",导致错误排查困难。

(1)具体解释

直接用 feof 判断时,即使读写过程中发生错误(如磁盘损坏、文件被删除、权限不足),代码也会把 "错误导致的读取失败" 当成 "文件尾" 处理,无法定位问题根源。

(2)具体案例

① 错误用法代码

cpp 复制代码
while (!feof(pf)) 
{
    fgets(buf, 100, pf);
    printf("读取:%s", buf);
}
printf("读取完成\n");

如果读取到一半时,test.txt 被手动删除:

fgets 会因 "文件不存在" 返回 NULL,但 feof(pf) 不会置位(因为不是 "正常文件尾");

代码会继续循环(!feof(pf) 仍是 1),反复执行 fgets(每次都返回 NULL),反复打印 buf 的旧数据,直到程序被强制终止;

你无法通过代码判断 "是文件读完了,还是文件被删了",错误排查无头绪。

② 正确用法代码

cpp 复制代码
while (fgets(buf, 100, pf) != NULL) 
{  
    // 先判断读取结果
    printf("读取:%s", buf);
}

// 再查原因
if (feof(pf)){
    printf("正常:文件读完了\n");
} else if (ferror(pf)) {
    printf("错误:读取过程中发生故障(如文件被删、磁盘错误)\n");
}
3、核心原因 3

空文件场景下,会读取到垃圾数据

(1)具体解释

如果文件是空的,直接用 feof 判断会导致代码读取到 "未初始化的垃圾值"。

(2)具体案例

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <string.h>

int main() 
{
    // 1. 先创建一个空文件(如果不存在)
    FILE* create_pf = fopen("empty.txt", "w");
    if (create_pf) {
        // 只创建文件,不写入任何内容(确保是空文件)
        fclose(create_pf);
        printf("已自动创建空文件:empty.txt\n\n");
    }
    else {
        perror("创建文件失败");
        return 1;
    }

    // 2. 演示错误用法
    char buf[100];  // 未初始化的缓冲区(会存储垃圾值)
    FILE* pf = fopen("empty.txt", "r");
    if (!pf) {
        perror("打开文件失败");
        return 1;
    }

    printf("---错误用法(直接用feof判断)---\n");
    while (!feof(pf)) {  // 错误:用feof判断是否继续循环
        fgets(buf, 100, pf);  // 空文件中fgets返回NULL,不修改buf
        printf("读取内容:%s", buf);  // 打印未初始化的垃圾值
    }
    printf("\n循环结束\n");

    // 3. 演示正确用法
    rewind(pf);  // 重置文件指针到开头
    printf("\n---正确用法(先判断fgets返回值)---\n");
    while (fgets(buf, 100, pf) != NULL) {  // 正确:用fgets返回值判断
        printf("读取内容:%s", buf);  // 空文件中不会进入循环
    }

    // 判断结束原因
    if (feof(pf)) {
        printf("循环结束原因:正常读取到文件尾(文件为空)\n");
    }
    else if (ferror(pf)) {
        printf("循环结束原因:读取过程发生错误\n");
    }

    fclose(pf);
    return 0;
}
4、具体操作

总结:为什么必须强调 "不能直接用 feof 判断"?

不只是因为效率,而是因为直接使用会导致逻辑错误 (多循环、重复数据)、数据异常(垃圾值)、错误无法排查------ 这些都是 "程序正确性" 的致命问题,比效率重要得多。

正确的逻辑永远是:

先用读写函数的返回值判断 "是否读取成功"(如 fgets != NULL、fread > 0);

只有当读取失败时,再用 feof 判断 "是不是文件尾",用 ferror 判断 "是不是出错了"。

(二)文本文件的判定

文本文件读取是否结束,fgetc 判断返回值是否为 EOFfgets 则判断返回值是否为 NULL。以下为具体代码演示:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int c;  // 必须用int,不能用char(EOF是-1,char可能溢出)
    FILE* fp = fopen("test.txt", "r");
    if (!fp) 
    { 
        perror("File opening failed"); 
        return EXIT_FAILURE; 
    }

    // 循环读取:fgetc返回EOF表示结束
    while ((c = fgetc(fp)) != EOF) 
        putchar(c);

    // 判断结束原因
    if (ferror(fp)) {  // 错误导致结束(如磁盘错误)
        puts("I/O error when reading");
    }
    else if (feof(fp)) {  // 正常结束(遇到文件尾)
        puts("\nEnd of file reached successfully");
    }

    fclose(fp);
    return 0;
}
(三)二进制文件的判定

二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。例如 fread 判断返回值是否小于实际要读的个数。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>

enum { SIZE = 5 };  // 数组大小

int main(void)
{
    double a[SIZE] = { 1.,2.,3.,4.,5. };
    // 写二进制文件
    FILE* fp = fopen("test.bin", "wb");
    fwrite(a, sizeof(*a), SIZE, fp);
    fclose(fp);

    // 读二进制文件
    double b[SIZE];
    fp = fopen("test.bin", "rb");
    // 实际读取个数存于ret_code
    size_t ret_code = fread(b, sizeof(*b), SIZE, fp);
    if (ret_code == SIZE) 
    {  
        // 读取成功(实际=请求)
        puts("数组读取成功,内容为: ");
        for (int n = 0; n < SIZE; ++n)
            printf("%f ", b[n]);
        putchar('\n');
    }
    else 
    {  
        // 读取结束,查原因
        if (feof(fp))
            printf("Error: 文件意外结束\n");
        else if (ferror(fp)) {
            perror("读取test.bin文件时出错");
        }
    }

    fclose(fp);
    return 0;
}

六、文件缓冲区

ANSIC标准采用**"缓冲文件系统"**处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块"文件缓冲区"。

从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上

如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。

缓冲区的大小根据C编译系统决定的。

(一)具体流程归纳

**1、输出流程:**内存数据 → 缓冲区(装满 / 刷新)→ 磁盘;

**2、输入流程:**磁盘数据 → 缓冲区(装满)→ 程序数据区(变量 / 数组);

**3、缓冲区大小:**由编译器决定(VS 默认 4KB,Linux 默认 8KB)。

(二)缓冲区的验证代码
cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <windows.h>  // Sleep函数(Windows环境,单位:毫秒)
// VS2022 WIN11环境测试

int main() 
{
    FILE* pf = fopen("test.txt", "w");
    fputs("abcdef", pf);  // 数据存入缓冲区,未写入磁盘
    printf("睡眠10秒:打开test.txt无内容\n");
    Sleep(10000);  // 暂停10秒,验证缓冲区未刷新

    fflush(pf);  // 显式刷新缓冲区,数据写入磁盘
    printf("刷新缓冲区后,睡眠10秒:打开test.txt有内容\n");
    Sleep(10000);

    fclose(pf);  // 关闭文件,自动刷新缓冲区(冗余但安全)
    pf = NULL;
    return 0;
}
(三)关键注意事项
1、刷新方式

(1)显式刷新:fflush(stream)(高版本 VS 不支持,建议用fclose替代)

(2)隐式刷新:fclose 关闭文件时自动刷新、缓冲区装满时系统自动刷新。

2、数据安全

未刷新 / 关闭文件可能导致数据丢失(如程序崩溃时,缓冲区数据未写入磁盘);

3、结论

C 语言操作文件必须 "刷新缓冲区" 或 "关闭文件",否则可能出现读写异常。

七、文件操作的标准步骤流程

第一步:打开文件

注意如果是以"读"的形式打开,那必须保证文件的存在;如果是以"写"或"追加"的形式打开,那么如果文件不存在,会自动创建文件。

如果打开失败了,可以使用 perror 函数去获得具体的原因。

cpp 复制代码
FILE* pf_write = fopen("letters.txt", "w");
if (pf_write == NULL) 
{
    //会告诉你具体原因,打印时为 ------ 写入时打开文件失败:具体原因
    perror("写入时打开文件失败");
    return 1;
}
第二步:对文件进行操作

文件的操作,无非就两个。

第一个是将内容输出到文件,对应的函数有 fputc、fputs、fprintf、fwrite;第二个是将文件中的内容输入到程序,对应的函数有 fgetc、fgets、fscanf、fread。

然后为了实现文件特定位置的输入,有 fseek、ftell、rewind 三个函数去定位光标的位置。

对于文本文件,输出与输入基本都会用到循环去完成。对于二进制文件的,输出与输入可以自定义字符,如果不是字节数特别多,基本不需要用到循环。

输出的时候没有那么多的细节,正常使用函数就可以了,但是输入的时候,要对文件的读取结束做判定。

比如对于文本文件读取的判定,fgetc 使用 EOF,fgets使用NULL。然后结束之后,你要知道它是因为什么结束的,是正常结束,还是异常结束

**feof:**判断 "读取失败" 是否因为 "文件内容已全部读完"(正常结束)。

**ferror:**判断 "读取失败" 是否因为 "操作过程中发生了错误"(异常结束)。

cpp 复制代码
while (fgets(buf, 100, pf) != NULL)
     printf("读取内容:%s", buf);  

if (feof(pf)) 
    printf("循环结束原因:正常读取到文件尾(文件为空)\n");
else if (ferror(pf)) 
    printf("循环结束原因:读取过程发生错误\n");

对于二进制文件的判定,fread 判断返回值是否小于实际要读的个数。

cpp 复制代码
size_t ret_code = fread(b, sizeof(*b), SIZE, fp);

if (ret_code == SIZE)
{
    puts("数组读取成功,内容为: ");
    for (int n = 0; n < SIZE; ++n)
        printf("%f ", b[n]);
    putchar('\n');
}
else
{
    if (feof(fp))
        printf("Error: 文件意外结束\n");
    else if (ferror(fp)) 
        perror("读取test.bin文件时出错");
}
第三步:关闭文件

一定要关闭文件,否则如果不使用 fflush(pf) 动态刷新,就无法对缓冲区进行刷新,从而无法成功保存文件。

同时将指针置为空,可以规避野指针,同时后续避免误用,与动态开辟内容里面,将指针置为空是一个逻辑。

cpp 复制代码
fclose(pf); 
pf = NULL;

以上即为 C语言文件操作 的全部内容,麻烦三连支持一下呗~

相关推荐
小莞尔2 小时前
【51单片机】【protues仿真】 基于51单片机波形发生器系统
c语言·单片机·嵌入式硬件·物联网·51单片机
No0d1es3 小时前
电子学会青少年软件编程(C语言)等级考试试卷(二级)2025年12月
c语言·青少年编程·等级考试·电子学会·二级
皮皮哎哟3 小时前
数据结构:从队列到二叉树基础解析
c语言·数据结构·算法·二叉树·队列
一匹电信狗3 小时前
【高阶数据结构】并查集
c语言·数据结构·c++·算法·leetcode·排序算法·visual studio
进击的小头3 小时前
设计模式组合应用:传感器数据采集与处理系统
c语言·设计模式
日拱一卒——功不唐捐3 小时前
01背包(C语言)
c语言
Hello World . .3 小时前
数据结构:二叉树(Binary tree)
c语言·开发语言·数据结构·vim
范纹杉想快点毕业4 小时前
嵌入式实时系统架构设计:基于STM32与Zynq的中断、状态机与FIFO架构工程实战指南,基于Kimi设计
c语言·c++·单片机·嵌入式硬件·算法·架构·mfc
划破黑暗的第一缕曙光4 小时前
[数据结构]:6.二叉树链式结构的实现2
c语言·数据结构·二叉树