C语言文件操作详解:顺序读写、随机定位与结束判定

星野
星野
发布时间:2026-10-15| 5人浏览过
原创

本文详细讲解C语言中文件流的常用操作函数,包括文本字符与行的输入输出、格式化读写以及二进制块读写。重点演示如何使用fseek进行文件指针的随机定位,并纠正关于feof判断文件结束的常见误区,帮助开发者掌握安全、高效的文件处理技巧。

前言

C语言标准库(stdio.h)提供了丰富的函数来处理文本文件和二进制文件。本文将接续前篇内容,深入探讨各类顺序读写函数、随机定位机制以及正确的文件结束判定方法,覆盖从基础字符处理到高级块操作的完整流程。

文本流的顺序读写

文本流的处理主要分为按字符、按行和格式化三种粒度。理解它们的适用场景是高效编码的基础。

第1步:单字符的输入与输出

目的:实现对文件流中每一个独立字节的最小粒度读写,适用于逐字分析或加密等场景。

函数原型:

int fgetc(FILE *stream); // 返回读取的字符(整型),失败返回 EOF
int fputc(int c, FILE *stream); // 写入字符c,成功返回c,失败返回EOF

关键参数: - stream:指向FILE对象的指针。 - c:要写入的整数形式字符。

操作演示:写文件

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

int main() {
    FILE* pfWrite = fopen("test.txt", "w");
    if (pfWrite == NULL) {
        perror("打开文件失败");
        return 0;
    }
    // 连续写入三个字符 a, b, c
    fputc('a', pfWrite);
    fputc('b', pfWrite);
    fputc('c', pfWrite);
    
    fclose(pfWrite);
    pfWrite = NULL;
    return 0;
}

上图为 fprintf 示例代码截图,但此处展示的是单字符写操作的结果,文件内容为连续的 abc。

操作演示:读文件

int main() {
    FILE* pfRead = fopen("test.txt", "r");
    if (pfRead == NULL) {
        perror("打开文件失败");
        return 0;
    }
    // 逐个读取并打印
    int ch = fgetc(pfRead);
    while (ch != EOF) {
        putchar(ch); // 或者 printf("%c", ch);
        ch = fgetc(pfRead);
    }
    
    fclose(pfRead);
    pfRead = NULL;
    return 0;
}

注意:原文代码示例中直接连续调用三次 fgetc 属于硬编码特定文件长度的做法,实际开发中应结合 EOF 判断或使用循环。

第2步:按行(字符串)的输入与输出

目的:一次性读取或写入一个完整的文本行(以换行符结尾的字符序列),适合处理配置项或日志记录。

函数原型:

char *fgets(char *string, int n, FILE *stream);
int fputs(const char *string, FILE *stream); // 注:原文为fputs,非fges

fgets 核心逻辑: 从流中读取最多 n-1 个字符,并在末尾自动追加终止符 '\0'。如果读取过程中遇到换行符 '\n',该换行符会被保留在缓冲区中。

操作演示:用 fgets 读取一行

int main() {
    char buff[100] = { 0 };
    FILE* pf = fopen("test.txt", "r");
    if (pf == NULL) return 0;
    
    // 最多读取99个字符,剩余空间给 '\0'
    if(fgets(buff, 100, pf) != NULL) {
        printf("%s", buff); // 包含末尾的换行符
    }
    
    fclose(pf);
    pf = NULL;
    return 0;
}

上图展示了从文件中读取一行数据并输出的效果,注意观察输出结果中是否保留了原有的换行格式。

操作演示:用 fputs 写一行

int main() {
    FILE* pf = fopen("demo.txt", "w");
    if (pf == NULL) return 0;
    
    // fputs 不会自动添加换行符,需手动插入 \n
    fputs("hello\n", pf);
    fputs("world\n", pf);
    
    fclose(pf);
    pf = NULL;
    return 0;
}

上图展示了使用 fputs 将多行文本写入文件后的磁盘保存情况,每行以明确的换行分隔。

控制台交互扩展

这两个函数同样可用于标准输入输出(stdin/stdout),常用于简易的控制台菜单交互。

上图展示了键盘输入 abcd 回车后,屏幕完整打印出 abcd 的效果,验证了 fgets/fputs 对标准流的兼容性。

第3步:格式化数据的读写

目的:按照特定格式(如整数、浮点数、字符串组合)序列化或反序列化复杂结构体或变量。

对比关系:

  • printf / scanf:针对标准输出/输入流(显示器/键盘)。
  • fprintf / fscanf:针对所有文件流(FILE*)。
  • sprintf / sscanf:针对内存中的字符串缓冲区(String Buffer)。

操作演示:fprintf 写入结构体数据

struct S {
    int n;
    float score;
    char arr[10];
};

int main() {
    struct S s = { 100, 3.14, "abc" };
    FILE* pf = fopen("data.txt", "w");
    if (pf == NULL) return 0;
    
    // 将结构化数据转为纯文本格式存储
    fprintf(pf, "%d %f %s", s.n, s.score, s.arr);
    
    fclose(pf);
    pf = NULL;
    return 0;
}

注意:对于 fscanf 读取,需要传入变量的地址(&s.n 等),且通常用于读取上文 fprintf 生成的文本文件。

第4步:二进制块的读写

目的:直接复制内存块到文件,不进行任何字符转换。这是保存图片、音频、压缩文件或内存快照的最高效方式。

函数原型:

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

参数说明:
- buffer:数据首地址。
- size:每个元素的大小(字节)。
- count:元素个数。
即总字节数 = size * count。

操作演示:二进制写结构体

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

int main() {
    struct Student stu = { "张三", 20, 55.6 };
    // 必须使用 wb/wb+ 模式开启二进制写入
    FILE* pf = fopen("binary.dat", "wb");
    if (pf == NULL) return 0;
    
    // 将一个 Student 对象的原生内存写入文件
    fwrite(&stu, sizeof(struct Student), 1, pf);
    
    fclose(pf);
    pf = NULL;
    return 0;
}

读回方法: 对应的 fread 用法类似。读取时,建议检查返回值是否等于请求的元素个数(count),以确认数据完整性。

文件的随机读写

上述操作均为“顺序读写”,指针随读写位置自动后移。若需要从文件中间某处开始读取,或跳转至末尾追加数据,需要使用定位函数。

第5步:使用 fseek 定位指针

目的:改变当前文件指针的位置,基于起始点、当前位置或文件末尾进行相对移动。

函数原型:

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

偏移常量:

  • SEEK_SET:文件开头(Offset 可为正或负)。
  • SEEK_CUR:文件当前位置(Offset 可为正或负,用于前后微调)。
  • SEEK_END:文件末尾(Offset 通常为0用于获取文件大小,负数用于从末尾向前跳)。

操作演示:从当前位置向后偏移

int main() {
    FILE* pf = fopen("test.txt", "r");
    if (pf == NULL) return 0;
    
    // 假设文件内容是 "abced..."
    // 当前指针默认在开头 'a' 处
    fseek(pf, 2, SEEK_CUR); // 跳过当前位置及其前1个字节,总共向右移动2位,指向 'c'
    
    int ch = fgetc(pf);
    printf("%c\n", ch); // 预期输出 'c'
    
    fclose(pf);
    pf = NULL;
    return 0;
}

上图展示了 fseek 执行后的指针位置变化示意,直观呈现了偏移量如何影响后续读取的数据起点。

补充工具:ftell 与 rewind

虽然草稿未详述,但常与 fseek 配合使用的还有:
- long ftell(FILE *stream):获取当前文件指针距离文件开头的字节数(常用于计算文件大小)。
- void rewind(FILE *stream):将指针重置到文件开头。

文件结束的判定(避坑指南)

很多初学者误用 feof() 来作为循环退出的条件,这往往导致最后一次无效数据的重复处理或死循环。

为什么不能用 feof 判断循环?

原理:
当程序读取文件最后一个有效字符后,文件指针移动到文件尾标记之前。此时 feof() 仍返回假(0)。只有当程序试图在文件尾标记之后再次尝试读取失败时,内部错误标志才会被设置,feof() 才返回真。

正确的结束判定策略

  1. 文本文件(字符流):
    - fgetc:判断返回值是否为 EOF。
    - fgets:判断返回值是否为 NULL。
  2. 二进制文件(块流):
    - fread:判断返回值是否小于请求的元素个数(例如 fread(..., 1, size, stream) != size)。

应用场景:
feof 真正的用途是区分“正常读完”还是“读取出错”。例如:
if (feof(stream)) ... else ...

总结

本文梳理了 C 语言文件操作的核心体系。掌握单字符与行的区别能避免内存溢出;格式化 IO 提升了数据可读性,而二进制块读写则保证了性能与保真度。配合 fseek 的定位能力,即可应对绝大多数复杂的文件解析需求。同时,切记依据具体的读取函数(fgets/fread/fgetc)来判断结束状态,而非依赖 feof。

以上就是C语言文件操作详解的详细内容,更多关于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 算法库。