C语言文件操作进阶:格式化/二进制读写与随机定位详解

陆景行
陆景行
发布时间:2026-10-16| 7人浏览过
原创

掌握C语言文件操作不仅是读写文本的基础,更涉及结构体序列化、二进制传输及内存映射等高级应用。本文梳理了从格式化输入输出到二进制存取的完整流程,深入剖析fseek与ftell的指针定位逻辑,并重点澄清feof函数在边界条件下的误用场景,帮助开发者编写健壮的文件处理代码。

一、文件的顺序读写

在上篇文章介绍了基础的字符和字符串读写后,本节将深入介绍针对复杂数据类型(如结构体)的处理方式,包括格式化读写和二进制读写,同时对比不同流处理函数的差异。

第1步:使用fprintf实现结构化数据的格式化写入

目的:将包含多种类型成员的结构体数据,按照指定的格式转换为可读文本写入文件。

入口:stdio.h 头文件中的 fprintf 函数。

fprintf 与 printf 类似,唯一的区别是它将格式化后的数据写入指定的文件流,而非标准输出。printf(stdout, ...) 等价于 printf(...)。

关键参数与动作:

  • 第一个参数为文件指针 FILE*。
  • 第二个参数为格式化字符串(如 "%s %d %f")。
  • 后续参数为要写入的数据项。
#include <stdio.h>

struct S
{
	char arr[10];
	int num;
	float sc;
};

int main()
{
	// 初始化结构体数据
	struct S s = { "abcdef", 20, 5.5f };
	
	// 打开文件用于写入 (w)
	FILE* pf = fopen("test.data", "w");
	if (NULL == pf)
	{
		return 1; // 打开失败处理
	}
	
	// 执行写入:将结构体成员按格式写入文件
	fprintf(pf, "%s %d %f", s.arr, s.num, s.sc);
	
	// 关闭文件并置空指针
	fclose(pf);
	pf = NULL;
	return 0;
}

第2步:使用fscanf实现结构化数据的格式化读取

目的:从文件中解析出特定格式的内容,并按顺序还原到结构体变量中。

入口:stdio.h 头文件中的 fscanf 函数。

重要参数与动作:

  • 接收地址:fscanf 对于指针类型或需要修改变量的位置,必须传入变量的地址(如 &(s.num))。
  • 注意:原草稿中 s.arr 本身即为数组首地址,通常无需加 &,但需确保缓冲区足够大以防止溢出。
#include <stdio.h>

struct S
{
	char arr[10];
	int num;
	float sc;
};

int main()
{
	struct S s = {0};
	
	// 以只读模式打开文件
	FILE* pf = fopen("test.data", "r");
	if (NULL == pf)
	{
		return 1;
	}
	
	// 执行读取:注意结构体成员地址传递
	fscanf(pf, "%s %d %f", s.arr, &(s.num), &(s.sc));
	
	// 验证读取结果
	printf("%s %d %f\n", s.arr, s.num, s.sc);
	
	// 关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

第3步:使用fwrite实现二进制数据的高效写入

目的:绕过格式化转换,直接将内存中的数据块原样写入文件。这种方式体积更小且支持非文本数据(如图片、加密数据)。

入口:stdio.h 中的 write 家族函数。

核心参数:

  • 指向数据块的指针 (void*)ptr。
  • 每个元素的大小 size_t size(通常用 sizeof 获取)。
  • 元素个数 size_t count。
  • 目标文件流。
struct S
{
    char arr[10];
    int num;
    float sc;
};

int main()
{
    struct S s = {"abcdef", 20, 5.5f};
    FILE* pf = fopen("test.data", "wb"); // 建议明确指定二进制模式
    if (NULL == pf)
    {
        return 1;
    }
    
    // 写入整个结构体二进制内容
    fwrite(&s, sizeof(struct S), 1, pf);
    
    fclose(pf);
    pf = NULL;
    return 0;
}

第4步:使用fread实现二进制数据读取

目的:将文件中的二进制内容直接加载到内存结构中。

入口:stdio.h 中的 fread 函数。

返回值判断:fread 返回的是成功读取的元素个数。如果返回值小于请求的元素个数,说明可能遇到了文件末尾或读取错误。

#include <stdio.h>

struct S
{
	char arr[10];
	int num;
	float sc;
};

int main()
{
	struct S s = {0};
	FILE* pf = fopen("test.data", "rb"); // 对应二进制写入
	if (NULL == pf)
	{
		return 1;
	}
	
	// 读取二进制数据到结构体
	// 注意:这里应传入实际读取到的字节数或元素计数来验证
    size_t ret = fread(&s, sizeof(struct S), 1, pf);
    if(ret != 1) { /* 处理错误 */ }
	
	printf("%s %d %f\n", s.arr, s.num, s.sc);
	
	fclose(pf);
	pf = NULL;
	return 0;
}

相关函数对比总结

为了理清流操作的边界,以下是对各类型函数的定位描述:

  • scanf / printf:分别针对标准输入(stdin)和标准输出(stdout)。
  • fscanf / fprintf:针对所有流(如文件)。只要第一个参数是合法的 FILE* 即可,功能由第二个参数的格式串决定。
  • sscanf / sprintf:针对字符串缓冲区。一个是从字符串解析,一个是向字符串格式化。

二、文件的随机读写

顺序读写只能从头到尾遍历,当我们需要修改文件中间某处的数据,或快速跳转到特定位置时,需要使用随机定位函数。

第1步:使用fseek调整文件指针位置

目的:根据当前指针位置和偏移量,重新定位文件流内部的位置指示器。

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

参数说明:

  • offset:偏移量。正值表示向后,负值表示向前(相对于origin)。
  • origin:起始点。通常为 SEEK_SET (文件开头)、SEEK_CUR (当前位置) 或 SEEK_END (文件末尾)。
#include <stdio.h>

int main()
{
	FILE* pf = fopen("test.data", "r+"); // 需要可读写模式演示fseek效果更佳
	if (NULL == pf)
	{
		perror("open file failed");
		return 1;
	}
	
	// 先读取前两个字符
	char ch1 = fgetc(pf);
	char ch2 = fgetc(pf);
	printf("Read: %c %c\n", ch1, ch2);
	
	// 将指针调整到距离文件末尾2个字节的位置
	fseek(pf, -2, SEEK_END);
	
	// 读取该位置的字符
	char ch = fgetc(pf);
	printf("Seeked Read: %c\n", ch);
	
	fclose(pf);
	pf = NULL;
	return 0;
}

第2步:使用ftell获取当前偏移量

目的:查询当前文件指针距离文件开头的字节数。常用于记录进度或计算文件大小。

第3步:使用rewind重置指针

目的:将文件指针强制重置回文件开头。等效于 fseek(pf, 0, SEEK_SET),但语义更清晰。

三、被错误使用的feof()

feof 是一个非常容易被误解的函数。它并非“判断是否到达文件末尾”的直接开关,而是反映上一次读取操作的状态。

1. feof的正确应用场景

当循环读取结束时,我们需要区分:

  • 读取失败(Error):发生硬件错误或权限问题。
  • 遇到结尾(EOF):正常读完所有有效数据。

feof(stream) 返回非0值表示曾到达过文件尾,返回0表示未到达过。

2. 常见的while(feof)死循环陷阱

许多初学者会写出如下错误代码:

// 【错误示范】
while (!feof(pf)) 
{
    fgetc(pf);
}

这种写法会导致最后一次读取时,指针仍在最后一个有效字符处,feof 尚未生效,下一次循环会再次尝试读取,导致重复处理或逻辑错误。

3. 正确的拷贝文件范例

在读取循环中,我们应该检查读取函数的返回值来判断是否结束,而不是依赖 feof。

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

int main()
{
	// 1. 准备源文件和目标文件
	FILE* pfread = fopen("test.txt", "r");
	FILE* pfwrite = fopen("test2.txt", "w");
	
	if (pfread == NULL || pfwrite == NULL)
	{
		return 1; // 任一打开失败
	}
	
	// 2. 使用返回值判断作为循环条件
	int ch;
	// fgetc 读取成功返回 ASCII 码,到达 EOF 返回 EOF
	while ((ch = fgetc(pfread)) != EOF)
	{
		fputc(ch, pfwrite);
	}
	
	// 3. 此时若需判断是出错还是正常结束,才调用 feof/ferror
	/*
	if (ferror(pfread)) {
	    printf("Read error\n");
	} else if (feof(pfread)) {
	    printf("Normal end of file\n");
	}
	*/
	
	fclose(pfread);
	fclose(pfwrite);
	
	return 0;
}

总结

C语言文件操作的核心在于理解“流”的概念以及内存数据到磁盘数据的映射关系。掌握 fprintf/fscanf 可以处理结构化文本,fwrite/fread 适用于高性能或二进制场景,而 fseek 则是解决非连续访问的关键。在处理文件结束状态时,务必优先信任读取函数的返回值,仅在事后分析状态时使用 feof。

以上就是C语言文件顺序读写、随机定位及feop误用的详细内容,更多关于C语言文件操作基础与进阶的资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。