征服 C 语言文件 I/O:透视数据流、FILE* 核心机制与高效实践全指南

C语言文件操作核心概念------"文件流"的旅程

C语言的文件操作本质上是在管理一个从内存到外部存储设备(如磁盘)的"数据流",以及管理这个流的"交通管制员"------文件指针。

目录

  • [1. 为什么要用文件?(持久化存储的屏障)](#1. 为什么要用文件?(持久化存储的屏障))
  • [2. 流(Stream)的概念(数据流动的管道)](#2. 流(Stream)的概念(数据流动的管道))
  • [3. 文件指针 `FILE*`(水管的控制阀门)](#3. 文件指针 FILE*(水管的控制阀门))
  • [4. 打开与关闭(建立连接与断开连接)](#4. 打开与关闭(建立连接与断开连接))
  • [5. 什么是文件?](#5. 什么是文件?)
    • [5.1 程序与数据的分离](#5.1 程序与数据的分离)
    • [5.2 文件的核心作用](#5.2 文件的核心作用)
  • [6. 文件的两种基本类型](#6. 文件的两种基本类型)
  • [7. 文件操作(操作光标的位置)](#7. 文件操作(操作光标的位置))
    • [7.1 顺序读写函数](#7.1 顺序读写函数)
      • [7.1.1 `fputc`、`fgetc`](#7.1.1 fputcfgetc)
      • [7.1.2 `fputs`、`fgets`](#7.1.2 fputsfgets)
      • [7.1.3 `fscanf`、`fprintf`](#7.1.3 fscanffprintf)
    • [7.2 文件随机读写](#7.2 文件随机读写)
      • [7.2.1 fseek](#7.2.1 fseek)
      • [7.2.2 ftell](#7.2.2 ftell)
      • [7.2.3 rewind](#7.2.3 rewind)
  • [8. feof、ferror](#8. feof、ferror)
  • [9. 缓冲区](#9. 缓冲区)
    • [9.1 什么是文件缓冲区?](#9.1 什么是文件缓冲区?)
    • [9.2 为什么要缓冲区?(提高效率)](#9.2 为什么要缓冲区?(提高效率))
    • [9.3 缓冲区的工作机制](#9.3 缓冲区的工作机制)
    • [9.4 如何管理缓冲区?(数据同步)](#9.4 如何管理缓冲区?(数据同步))
    • [9.5 验证代码](#9.5 验证代码)

1. 为什么要用文件?(持久化存储的屏障)

程序在运行时的数据都存储在内存中,具有"临时性"。一旦程序退出,内存就会被清空,数据就"消失"了。文件就是给数据一个永久的"家"(磁盘),实现数据的持久化保存。

2. 流(Stream)的概念(数据流动的管道)

"流"是C语言为了统一操作各种输入/输出设备而抽象出来的一个概念。你可以把流想象成一个"水管":

  • 输入流:数据从键盘、文件等流入程序内存。
  • 输出流:数据从程序内存流出到屏幕、文件等。

C语言程序启动默认有三条打开的"水管":stdin(键盘输入)、stdout(屏幕输出)和 stderr(错误信息输出)。

3. 文件指针 FILE*(水管的控制阀门)

你不能直接操作磁盘上的文件,你需要一个在内存中的"管理员"来间接操作它。这个管理员就是文件指针 FILE*
FILE结构体关键成员(不同编译器略有差异):

c 复制代码
typedef struct _iobuf {
int cnt; // 缓冲区剩余字符数
char *ptr; // 下一个要读/写的位置
char *base; // 缓冲区基地址
int flag; // 状态标志(读/写/错误等)
int fd; // 文件描述符
// 其他成员
} FILE;
  • FILE 结构体:系统在内存中为每个打开的文件创建的一个"信息登记表",记录了文件名、状态、当前读写位置等所有细节。
  • FILE* :是一个指针,指向这个"信息登记表"。所有的文件操作函数(如 fputcfread)都是通过这个指针找到正确的文件并执行操作。

4. 打开与关闭(建立连接与断开连接)

  • fopen(打开) :就像在程序和文件之间建立连接,同时生成上面提到的 FILE 结构体和文件指针 FILE*。你需要告诉它文件名和模式(你想对文件做什么,比如"只读"、"只写"、"二进制写"等等)。
c 复制代码
	//打开文件
	FILE* pf = fopen("文件名","w");
	if (pf == NULL)
	{
		perror("fopen");//打印错误信息
		return 1;
	}
  • fclose(关闭) :断开连接,释放内存中的 FILE 结构体资源。
c 复制代码
	//关闭文件
	fclose(pf);
	//防止野指针!!! 千万别忘记这一步,养成好习惯
	pf = NULL;

文件打开模式(mode)

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

5. 什么是文件?

5.1 程序与数据的分离

在C语言编程中,程序本身和它处理的数据在运行时是明确分开的:

程序相关文件:

  • 源代码文件 :如 .c 文件,包含程序员编写的原始代码
  • 目标文件 :如 Windows 环境下的 .obj 文件,编译后的中间文件
  • 可执行文件 :如 .exe 文件,最终可以直接运行的程序

数据文件:

  • 文件的内容不是程序本身,而是程序在运行时需要读取的输入数据或需要保存的输出结果
  • C语言的文件操作主要讨论的就是这类数据文件

5.2 文件的核心作用

文件作为程序与外部环境之间的桥梁,主要发挥以下作用:

  • 数据输入:程序可以从文件中读取配置信息、初始数据等
  • 数据输出:程序可以将运行结果、日志信息等保存到文件中
  • 数据持久化:确保数据在程序退出后仍然存在,实现长期存储
  • 数据共享:不同程序之间可以通过文件交换数据

6. 文件的两种基本类型

类型 数据存储方式 人类可读性 适用场景 示例函数
文本文件 以字符编码(如 ASCII, UTF-8)存储,每个字符对应一个或多个字节。 ✅ 高 (可用记事本直接查看) 配置文件、源代码、日志文件等。 fprintf, fscanf, fgetc, fputs
二进制文件 以数据在内存中的原始二进制形式直接存储。 ❌ 低 (用记事本打开是乱码) 图片、音频、视频、程序数据备份等。 fwrite, fread
  • 文本文件 :数据以我们能看到的 ASCII 码字符形式存储。例如,整数 10000 会被拆成字符 '1''0''0''0''0' 五个字节来存。它牺牲了存储效率,但提高了可读性。
  • 二进制文件 :数据以它们在内存中的原始二进制形式存储。整数 10000 就只有 4 个字节(一个 int 的大小)存下它的二进制表示。它提高了存储效率和读写速度,但直接打开看就是乱码。

这里给个例子也许直观一点:

7. 文件操作(操作光标的位置)

  • 顺序读写 :就像读一本书,光标(文件指针)从头开始,每读一个字节或一个数据,就往后移动一位。fgetc/fputcfgets/fputs 都是顺序操作。
  • 随机读写 :你可以控制光标的位置:
    • fseek:让你任意跳转到文件中的某个位置。
    • ftell:告诉你当前光标在哪里(相对于文件开头有多少字节偏移量)。
    • rewind:把光标一键重置回文件开头。

7.1 顺序读写函数

函数名 功能 适用于 光标影响
fgetc 字符输入函数 所有输入流 读取一个字符后,光标向后移动一个字节
fputc 字符输出函数 所有输出流 写入一个字符后,光标向后移动一个字节
fgets 文本行输入函数 所有输入流 读取一行内容(包括换行符,但不超过指定大小),光标移动到读取内容末尾的下一个位置
fputs 文本行输出函数 所有输出流 写入一个字符串,光标移动到写入内容末尾的下一个位置
fscanf 格式化输入函数 所有输入流 根据格式符读取数据,光标移动到读取操作停止的位置(可能是下一个未读取字符)
fprintf 格式化输出函数 所有输出流 根据格式符输出数据,光标移动到写入内容末尾的下一个位置
fread 二进制输入 文件输入流 读取指定数量的字节,光标移动相应的字节数
fwrite 二进制输出 文件输出流 写入指定数量的字节,光标移动相应的字节数

这里可能有人跟我一样就有疑惑了,类似fgetc这类函数,不是拿出字符或者字符串吗,不应该是字符输出函数吗,为何这里写的是输入函数,其实在 C 语言的文件操作中,"输入"和"输出"是相对于程序内存而言的:

1. 为什么是"输入"函数?

  • 输入 (Input): 指的是将数据从外部设备 (如文件、键盘 stdin读入到程序的内存变量中 。
  • fgetc 的作用: fgetc 从指定的文件流中读取一个字符,并将其作为返回值返回给程序 。
  • 流向: 数据是从文件 程序的内存。因此,它是输入函数。

2. 对应的"输出"函数是谁?

fgetc 功能相反,用于字符输出的函数是 fputc

  • 输出 (Output): 指的是将数据从程序的内存 变量中写入 到外部设备(如文件、屏幕 stdout)中 。
  • fputc 的作用: fputc 将程序内存中的一个字符写入到指定的文件流中 。
  • 流向: 数据是从程序的内存 → 文件。因此,它是输出函数 。

其他函数也是类似一样的理解。

7.1.1 fputcfgetc

这里给出函数原型和描述

这里写入和读出,既可以一个一个输入,也可以循环输入,写入比较简单就直接给出示例:

c 复制代码
	//写文件
	fputc('E', pf);
	......
	fputc('\n', pf);

	char c = 'a';
	for (c = 'a'; c <= 'z'; c++)
	{
		fputc(c, pf);
	}

这里需要注意的就是,如果你不手动输入换行符,那你输入文件的就一直是一行内容。

当你第二次打开文件的时候,重新写入,那么将会覆盖掉你之前写的内容。无论内容多长,都会全部覆盖掉。


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

这里需要提示的是,如果你图方便在原来写操作函数的代码上进行修改的话,一定首先保存代码,不然有可能你上次写的那些字符丢失掉,我已经踩过坑了。

读操作同样也可以单个读和循环读,无非就是看清你刚才写入的那个控制符(换行'\n')。

c 复制代码
	//读文件 -- 单字符
	char c = 0;
	c = fgetc(pf);
	printf("%c", c);

	puts(" ");

	//读文件 -- 循环读
	while ((c = fgetc(pf)) != EOF)
	{
		printf("%c", c);
	}

这里解释一下为什么循环读的时候,没有打印E,因为在循环读之前,已经进行了一次单独读字符,此时光标已经移到下一个字符了。

7.1.2 fputsfgets

1. fputs:文本行输出(写入)
fputs 是 C 语言中用于将字符串写入到文件流的函数。它从程序内存中获取一个 C 字符串(以 \0 结尾),并将其内容复制到指定的文件流中。

  • 核心特点 :写入时不包括字符串末尾的空字符(\0)。
  • 光标变化:光标会移动到写入内容末尾的下一个位置。
  • 关键区别 :与 puts 函数不同,fputs 不会在写入的字符串末尾自动追加换行符(\n)。如果需要换行,必须手动写入 \n

2. fgets:文本行输入(读取)
fgets 是 C 语言中用于从文件流中读取一行文本的函数。它将读取的字符串存储到指定的缓冲区 str 中。

  • 核心特点 :它是安全的行读取函数,因为它接受一个最大字符数 num 参数,防止缓冲区溢出。
  • 换行符处理 :如果读取到了换行符(\n),fgets 会将其包含在存储到缓冲区的结果字符串中。读取结束后,它会自动在字符串末尾追加 \0
  • 结束判定 :应该通过判断其返回值是否为 NULL 来确定文件是否读取结束或发生错误。

这里进行写入就直接按照函数调用格式进行写入就好:

但是读操作需要细看一下了,他的参数有一个个数限制,如果你的个数比你接收字符串的长度要大,这里是会出现问题的。

思考这么一个问题,如果文件中内容很长,但你的接收数组很小,这时候会报错吗?

先来看结果,很明显文件里面是有部分内容长度超出了我的数组长度,但结果还是正确打印在了屏幕上:

这是为什么呢,确实内容很长,但你每次只读五个放在数组里,打印也是打印读入的字符串,就相当于你把文件里面的内容5个5个往出挪,只不过次数多一点而已。

7.1.3 fscanffprintf

fprintffscanf 被称为 printfscanf 的文件版本,它们唯一的、也是最关键的区别在于第一个参数:FILE * stream

FILE * stream 参数是文件 I/O 函数的核心。它使得这些函数具有了通用性,能够操作 C 语言抽象出的所有流:

  • stream 是一个由 fopen 返回的文件指针 时,操作的是磁盘文件
  • streamstdout 时,操作的是标准输出(屏幕)
  • streamstdin 时,操作的是标准输入(键盘)

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

因此, fprintffscanf 是标准库函数中功能最强大的格式化 I/O 版本。


以上就是部分顺序读写函数的使用方法,下来了解一下随机读写吧。

7.2 文件随机读写

7.2.1 fseek

fseek:定位文件光标,是用于定位文件指针(文件光标)的函数。

c 复制代码
int fseek ( FILE * stream, long int offset, int origin );
属性 描述
功能 根据文件指针的当前位置或起始位置,加上指定的偏移量,重新定位文件光标。
函数原型 int fseek (FILE * stream, long int offset, int origin);
光标影响 将光标设置到 origin + offset 的新位置。

参数 origin(起始位置)宏定义:

宏定义 含义
SEEK_SET 从文件开头开始计算偏移量。
SEEK_CUR 从文件当前位置开始计算偏移量。
SEEK_END 从文件末尾开始计算偏移量。

说是随机读写,看着也不是很随机,只不过对比顺序读写,倒是随机了一些。

注意,它是移动光标,没有读写功能,要读写的话还是按照前文所涉及到的函数来读写。

7.2.2 ftell

ftell 用于报告文件指针相对于文件起始位置的偏移量。

属性 描述
功能 返回文件指针当前所处的位置,这个值是相对于文件开头的字节数。
函数原型 long int ftell (FILE * stream);
光标影响 不改变文件光标的位置。

那我们是不是可以结合上面的fseek函数求出字符串长度呢?尝试一下:首先使用 fseek(pf, 0, SEEK_END) 将文件指针移动到文件末尾,然后通过 ftell(pf) 获取当前文件位置(即文件总字节数)。输出的文件大小与字符串 "Extreme 20 65.500000" 的长度一致,表明测量准确。程序最终正常退出(代码为0),验证了文件操作的成功执行。

7.2.3 rewind


rewind 是一个快速将文件指针重置到文件开头的函数。

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

结果也确实跟我们预期的一样。

8. feof、ferror

feof:文件结束标志检测,feof 函数用于检查文件流是否已经到达文件尾 (End-Of-File )。

属性 描述
功能 检查在先前对流进行的输入操作中,是否遇到了文件尾指示器。
函数原型 int feof (FILE * stream);
返回值 如果已设置文件结束指示器,返回一个非零值;否则返回 0。

关键注意事项(防止误用)

牢记: 在文件读取的循环过程中,不能用 feof 的返回值直接来判断文件是否结束。

  • feof 的正确作用:当文件读取已经结束时,判断读取结束的原因是否是遇到文件尾结束。
  • 读取失败 vs. 文件结束 :像 fgetcfread 这样的读取函数,当读取操作失败(例如,遇到文件尾或发生 I/O 错误)时,会返回特殊值(如 EOF 或小于请求的个数)来终止循环。只有当循环结束后,才能使用 feof 来区分是正常读到文件末尾还是发生了读取错误。

正确的文件读取结果判断应该依赖读取函数的返回值:

  • 文本文件 :判断 fgetc 的返回值是否为 EOF,或 fgets 的返回值是否为 NULL
  • 二进制文件 :判断 fread 的返回值是否小于实际要读的个数。

很多人用 feof 来判断文件是否读完,这是错误的。

  • feof 并不判断"是否读完了"。它只在文件已经被读取成功结束后,判断失败的原因是不是"遇到了文件尾"
  • 正确的做法 是检查读操作函数(如 fgetcfread)的返回值:
    • 如果是文本读取,检查返回值是否是 EOFNULL
    • 如果是二进制读取,检查返回值是否小于你请求读取的数据量。

ferror:错误指示器检测,ferror 函数用于检查文件流的错误状态。

属性 描述
功能 检查流的错误指示器是否被设置,以确定输入或输出操作是否发生了错误。
函数原型 int ferror (FILE * stream);
返回值 如果错误指示器已设置,返回一个非零值;否则返回 0。

关键作用ferror 通常与 feof 结合使用,在文件读取循环终止后,用于诊断失败的原因:

  • 如果读取函数返回失败(例如 fread 返回值不完整),并且 feof 返回 0,则说明文件没有到达末尾,此时极有可能是发生了 I/O 错误。
  • 这时,通过检查 ferror(fp) 的返回值是否为非零,可以确认是否是由于 I/O 错误导致了读取操作的终止。

示例:

c 复制代码
int main()
{
	//打开文件
	FILE* pf = fopen("Extreme.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写
	char ch = 0;
	for (ch = 'a'; ch <= 'z'; ch++)
		fputc(ch, pf);

	//判断是什么原因导致读取结束的
	if (feof(pf))
		printf("遇到文件末尾,读取正常结束\n");
	else if (ferror(pf))
		perror("fputc");
	//关闭文件
	fclose(pf);
	//防止野指针
	pf = NULL;
	return 0;
}

我写的这段 C 语言代码核心逻辑存在严重错误:它尝试使用 只读模式 ("r")打开文件 Extreme.txt,但随后却在循环中执行 写入操作fputc)。由于权限冲突,fputc 调用将立即失败,并设置文件流的错误指示器。代码最后尝试用 feofferror 诊断状态,由于没有成功的读取操作,故而没有到文件末尾,feof 返回假;而 ferror 会返回真,正确地指出因写入权限不足导致了 I/O 错误 ,程序将输出系统错误信息(如Bad file descriptor),这确认了文件打开模式与操作行为之间的矛盾。

9. 缓冲区

9.1 什么是文件缓冲区?

C 语言标准采用 缓冲文件系统 来处理数据文件。

  • 定义: 文件缓冲区是系统在内存中为程序中的一个正在使用的文件自动开辟的一块临时存储区域(或称"中转站")。
  • 大小: 缓冲区的大小由 C 编译系统决定。

9.2 为什么要缓冲区?(提高效率)

直接对磁盘(外存)进行读写操作是非常慢的。频繁的小量 I/O 操作(比如每写一个字节就访问一次磁盘)效率极低。
缓冲区的目标: 通过在内存中集中处理数据,将多次小的操作合并为一次大的、高效的磁盘操作。

9.3 缓冲区的工作机制

文件缓冲区的机制分为写入(输出)和读取(输入)两个方向:

  • 输出(写入)机制

      1. 数据暂存:程序调用 fputcfprintf 等输出函数时,数据不会立即写入磁盘,而是先被送到内存中的文件缓冲区。
      1. 批量写入:当缓冲区被装满时,系统才会将缓冲区中的全部数据一次性送到磁盘上。
  • 输入(读取)机制

      1. 预先填充:程序调用 fgetcfscanf 等输入函数时,如果缓冲区为空,系统会从磁盘文件中读取一整块数据(充满缓冲区)输入到内存缓冲区中。
      1. 逐个提取:程序从缓冲区中逐个地将数据送到程序变量(数据区)中使用。

9.4 如何管理缓冲区?(数据同步)

由于数据暂时停留在内存中,如果不进行特殊操作,程序崩溃或异常退出时,缓冲区中的数据可能会丢失。因此,我们必须进行刷新操作,将数据从内存推送到磁盘。

函数 功能 描述
fflush(fp) 强制刷新 手动将文件缓冲区中的所有数据立即写入磁盘。常用于确保关键数据及时保存。
fclose(fp) 关闭文件 在关闭文件之前,系统会自动执行一次刷新缓冲区的操作,然后再释放所有相关资源。

9.5 验证代码

c 复制代码
#include <stdio.h>
#include <windows.h> // 包含 Sleep 函数

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

    fputs("abcdef", pf); // 数据先放在输出缓冲区

    printf("睡眠10秒-此时打开test.txt文件,文件可能没有内容...\n");
    Sleep(10000); // 休眠10秒,数据仍停留在内存缓冲区

    printf("刷新缓冲区\n");
    fflush(pf); // 强制刷新,数据写入磁盘

    printf("再睡眠10秒-此时打开test.txt文件,文件就有内容了。\n");
    Sleep(10000); 
    
    // fclose 在关闭文件时也会刷新缓冲区
    fclose(pf);
    pf = NULL;
    return 0;
}
相关推荐
Bona Sun2 小时前
单片机手搓掌上游戏机(十二)—esp8266运行gameboy模拟器之编译上传
c语言·c++·单片机·游戏机
星期天24 小时前
3.2联合体和枚举enum,还有动态内存malloc,free,calloc,realloc
c语言·开发语言·算法·联合体·动态内存·初学者入门·枚举enum
自信150413057594 小时前
初学者小白复盘23之——联合与枚举
c语言·1024程序员节
秃秃秃秃哇5 小时前
C语言实现循环链表demo
linux·c语言·链表
小曹要微笑9 小时前
STM32H7系列全面解析:嵌入式性能的巅峰之作
c语言·stm32·单片机·嵌入式硬件·算法
松涛和鸣11 小时前
14、C 语言进阶:函数指针、typedef、二级指针、const 指针
c语言·开发语言·算法·排序算法·学习方法
星期天213 小时前
3.0 C语⾔内存函数:memcpy memmove memset memcmp 数据在内存中的存储:整数在内存中的存储 ⼤⼩端字节序和字节序判断
c语言·数据结构·进阶·内存函数·数据内存存储
代码雕刻家20 小时前
C语言的左对齐符号-
c语言·开发语言
star learning white1 天前
xmC语言8
c语言·开发语言·算法