前言:
本篇系统梳理 C 语言标准库文件 IO 的全部核心内容,从基础 API 用法、缓冲区底层原理到实战案例、面试高频坑点全覆盖,兼顾入门语法与进阶底层认知,是 C 语言从语法学习到项目开发的必备模块,适合零基础入门、知识点复盘与校招社招面试突击复习。
一、文件 IO 基础概念
1. 文件与文件指针
C 语言中,文件是一组有序数据的集合,操作系统通过文件描述符管理文件;标准库封装后,通过FILE结构体指针操作文件,所有文件 IO 函数都围绕文件指针工作。
#include <stdio.h>
FILE* fp; // 文件指针,指向文件的信息结构体
FILE结构体内部记录了文件描述符、缓冲区指针、读写位置、错误标记等信息,开发者无需关心内部细节,通过标准库函数间接操作即可。
2. 文本文件 vs 二进制文件
二者本质是存储与解析方式的不同,数据本身都是二进制存储:
- 文本文件:以字符为单位存储,每个字节对应一个 ASCII/Unicode 字符,人类可直接阅读,如
.txt、.c文件- 二进制文件:按数据的内存原始形态存储,以字节为单位,需按特定格式解析,如图片、可执行程序、
.bin文件
| 对比维度 | 文本文件 | 二进制文件 |
|---|---|---|
| 存储单位 | 字符编码 | 原始内存字节 |
| 可读性 | 可直接打开阅读 | 需按格式解析,无法直接阅读 |
| 换行处理 | Windows 下自动转换\n与\r\n | 不做任何转换,原样读写 |
| 空间占用 | 数字转字符存储,占用更大 | 按原始大小存储,更节省空间 |
| 读写效率 | 有编码转换,略低 | 直接拷贝,效率更高 |
3. 文件打开模式详解
fopen打开文件时需指定打开模式,核心模式分为基础模式与修饰符组合:
| 模式 | 含义 | 文件不存在时 | 文件存在时 | 可读 | 可写 |
|---|---|---|---|---|---|
r | 只读 | 打开失败 | 正常打开,保留原内容 | ✅️ | ❌️ |
w | 只写 | 创建新文件 | 清空原内容,从头写入 | ❌ | ✅ |
a | 追加 | 创建新文件 | 保留原内容,末尾追加 | ❌ | ✅ |
r+ | 读写 | 打开失败 | 正常打开,保留原内容 | ✅ | ✅ |
w+ | 读写 | 创建新文件 | 清空原内容 | ✅ | ✅ |
a+ | 读写追加 | 创建新文件 | 末尾追加写入,可读全部内容 | ✅ | ✅ |
修饰符:在基础模式后加
b表示二进制模式(如rb、wb),不加默认是文本模式;Linux 下文本与二进制模式无差异,Windows 下存在换行符转换区别。
二、标准库核心 IO 函数
所有函数均定义在<stdio.h>中,按操作粒度分为字符级、字符串级、格式化、块级四大类。
1. 字符级读写:fgetc /fputc
按单个字节读写,通用性最强,文本与二进制文件均适用。
// 读一个字符,成功返回字符ASCII值,读到末尾/失败返回EOF
int fgetc(FILE *stream);
// 写一个字符,成功返回写入的字符,失败返回EOF
int fputc(int c, FILE *stream);
代码示例:逐字符读取文件
int main() {
FILE* fp = fopen("test.txt", "r");
if (fp == NULL) { // 必须校验打开结果
perror("fopen failed");
return -1;
}
int ch;
while ((ch = fgetc(fp)) != EOF) {
putchar(ch);
}
fclose(fp); // 必须关闭文件,否则资源泄漏
return 0;
}
注意:返回值用
int接收,因为EOF是值为 - 1 的整型,char类型无法区分 0xFF 与 EOF。
2. 字符串级读写:fgets /fputs
按行读写字符串,仅适用于文本文件。
// 最多读n-1个字符,遇到换行/文件结束停止,自动补'\0'
char *fgets(char *str, int n, FILE *stream);
// 写入字符串,不自动写入换行符
int fputs(const char *str, FILE *stream);
核心特点
fgets会读取换行符并存入字符串,这是和gets的核心区别- 最多读取
n-1个字符,永远保证末尾有'\0',不会溢出,安全可控 - 行长度超过
n-1时,只会读取部分内容,剩余内容下次读取
3. 格式化读写:fprintf /fscanf
按指定格式读写文件,用法和printf/scanf完全一致,只是目标从控制台变成文件。
int fprintf(FILE *stream, const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
代码示例:写入格式化数据
fprintf(fp, "姓名:%s 年龄:%d 成绩:%.1f\n", "张三", 20, 95.5);
4. 块级读写:fread /fwrite
按内存块批量读写,是二进制文件的核心操作函数,直接操作原始字节,不做格式转换。
// 返回成功读取的元素个数
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
// 返回成功写入的元素个数
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
size:单个元素的字节大小count:要读写的元素个数
代码示例:写入结构体数组
typedef struct {
int id;
char name[20];
} Student;
Student stu = {1001, "李四"};
fwrite(&stu, sizeof(Student), 1, fp);
5. 文件定位操作:fseek /ftell/rewind
控制文件内部的读写指针位置,实现随机读写。
// 移动文件指针到指定位置
int fseek(FILE *stream, long offset, int origin);
// 获取当前文件指针位置,返回距离文件开头的字节数
long ftell(FILE *stream);
// 重置文件指针到文件开头
void rewind(FILE *stream);
origin基准值有三种:
SEEK_SET:文件开头SEEK_CUR:当前位置SEEK_END:文件末尾
经典用法:获取文件大小
fseek(fp, 0, SEEK_END); // 指针移到末尾
long size = ftell(fp); // 得到总字节数
rewind(fp); // 指针回到开头
注意:仅二进制文件结果准确;文本文件因换行符转换,ftell 结果无实际字节意义。
三、缓冲区深度解析(面试核心重点)
标准库 IO 并非直接调用系统接口读写磁盘,而是通过缓冲区中转,批量读写磁盘,大幅提升 IO 性能,这是文件 IO 最核心的底层原理。
1. 为什么需要缓冲区
磁盘 IO 的速度远慢于内存,每次读写都直接访问磁盘会导致性能极差。缓冲区的作用是批量合并读写请求:
- 写操作:先写入内存缓冲区,缓冲区满了再一次性写入磁盘
- 读操作:一次性从磁盘读满缓冲区,后续读操作直接从内存取
本质是用少量内存空间,换取大幅的 IO 性能提升。
2. 三种缓冲类型
| 缓冲类型 | 刷新规则 | 典型代表 |
|---|---|---|
| 全缓冲 | 缓冲区满了才触发实际 IO | 普通磁盘文件默认全缓冲 |
| 行缓冲 | 遇到换行符\n、缓冲区满时刷新 | 标准输出stdout默认行缓冲 |
| 无缓冲 | 直接调用系统 IO,不经过缓冲区 | 标准错误stderr默认无缓冲 |
经典现象:
printf("hello");不加换行符,程序运行时不会立刻打印,要等程序结束缓冲区刷新才输出;加了\n会立刻输出,就是行缓冲的特性。
3. 缓冲区刷新的触发时机
- 缓冲区空间已满
- 行缓冲遇到换行符
\n - 主动调用
fflush()函数 - 调用
fclose()关闭文件时 - 程序正常退出时
4. fflush 函数详解
int fflush(FILE *stream);
- 功能:强制刷新缓冲区,把缓冲区中未写入磁盘的数据立刻刷到磁盘
- 适用场景:需要实时落盘的日志、关键数据写入,避免程序崩溃丢失数据
- 注意:只能刷新输出缓冲区,输入缓冲区的 fflush 行为标准未定义,不可用于清空输入
四、经典易错坑点精讲
1. feof 函数的经典误用(笔试最高频坑题)
错误认知:
很多人以为feof(fp)可以判断 “是否到达文件末尾”,然后写出如下错误代码:
// 错误写法
while (!feof(fp)) {
char ch = fgetc(fp);
printf("%c", ch);
}
错误原因:
feof的作用不是预判结束,而是读取失败后,判断失败原因是不是 “到达文件末尾”。 当读完最后一个字符时,文件指针还没到末尾标记,feof返回假;再读一次失败后,feof才会返回真。因此错误写法会多读一次,输出一个多余的 EOF 乱码字符。
正确写法:
先执行读操作,判断读操作是否成功,失败后再用feof区分是文件结束还是读错误:
// 正确写法
int ch = fgetc(fp);
while (ch != EOF) {
printf("%c", ch);
ch = fgetc(fp);
}
if (feof(fp)) {
printf("正常到达文件末尾\n");
} else if (ferror(fp)) {
printf("读取文件出错\n");
}
2. 文件打开失败未校验
fopen打开文件可能因路径不存在、权限不足等原因失败,返回NULL。如果不校验直接使用空指针读写,会直接触发程序崩溃。
规范:所有
fopen调用后必须判断返回值,配合perror打印错误原因。
3. 读写模式切换的注意事项
使用r+、w+、a+等读写模式时,不能在读和写之间随意切换,中间必须有定位操作:
- 读完直接写、写完直接读都会出现数据异常
- 切换方向前必须调用
fseek、fsetpos、rewind中的任意一个定位函数
底层原因:读写共用同一个缓冲区,切换方向时缓冲区状态不一致,需要定位操作重置缓冲区状态。
4. 结构体读写的内存对齐问题
直接用fwrite写入结构体时,结构体内部存在内存对齐填充字节,这些填充字节也会被写入文件:
- 不同编译器、不同编译选项对齐规则不同,跨平台读取时会出现数据错位
- 工程规范:序列化写入时,要按字节逐个写入成员,避免对齐填充影响
五、实战案例:二进制文件拷贝程序
综合运用fread/fwrite与缓冲区知识,实现通用文件拷贝功能:
#include <stdio.h>
#define BUF_SIZE 1024 // 缓冲区大小,可调整
int main(int argc, char* argv[]) {
if (argc != 3) {
printf("用法:%s 源文件 目标文件\n", argv[0]);
return -1;
}
FILE* src = fopen(argv[1], "rb");
if (src == NULL) {
perror("打开源文件失败");
return -1;
}
FILE* dest = fopen(argv[2], "wb");
if (dest == NULL) {
perror("创建目标文件失败");
fclose(src);
return -1;
}
char buf[BUF_SIZE];
size_t len;
// 循环读取,每次读满缓冲区,读到多少写多少
while ((len = fread(buf, 1, BUF_SIZE, src)) > 0) {
fwrite(buf, 1, len, dest);
}
fclose(src);
fclose(dest);
printf("文件拷贝完成\n");
return 0;
}
六、面试高频考点与易错汇总
1. 经典面试问答
Q1:文本文件和二进制文件有什么区别?
答:
- 存储方式:文本文件按字符编码存储,二进制文件按内存原始字节存储
- 换行处理:Windows 下文本模式会转换
\n与\r\n,二进制模式不转换- 可读性:文本文件可直接阅读,二进制文件需按格式解析
- 效率:文本文件有编码转换,效率略低;二进制文件直接读写,效率更高
Q2:文件 IO 为什么要设计缓冲区?有哪几种缓冲类型?
答: 缓冲区的作用是减少磁盘 IO 次数,批量读写提升性能,用少量内存换取 IO 效率提升。 三种缓冲类型:
- 全缓冲:缓冲区满才刷新,普通文件默认
- 行缓冲:遇到换行符刷新,标准输出默认
- 无缓冲:直接 IO,不缓冲,标准错误默认
Q3:feof 函数的作用是什么?常见的错误用法是什么?
答:
- feof 的作用是读取失败后,判断失败原因是否为到达文件末尾,不是预判文件结束。
- 常见错误是用
while(!feof(fp))作为循环条件,会导致多读一次乱码字符。 正确做法是先执行读操作,判断读操作返回值,失败后再用 feof 判断结束原因。
Q4:fopen 的 r、w、a 模式有什么核心区别?
答:
- r:只读,文件必须存在,不存在则打开失败,保留原内容
- w:只写,文件不存在则创建,存在则清空原内容从头写
- a:追加,文件不存在则创建,存在则在末尾追加写入,保留原内容
Q5:fflush 函数的作用是什么?适用场景有哪些?
答:
- fflush 用于强制刷新文件缓冲区,把缓冲区中的数据立刻写入磁盘。
- 适用场景:关键数据实时落盘、日志系统保证数据不丢失、程序异常退出前同步数据。
2. 常见易错坑点
- 用
char接收fgetc返回值,无法区分 EOF 与 0xFF 字符 - 用
feof预判文件结束,导致多读乱码 - 打开文件不判断返回值,空指针操作引发崩溃
- 读写模式直接切换,未加定位操作,数据异常
- 忘记
fclose关闭文件,导致资源泄漏、数据未落盘 - 误以为所有平台文本与二进制模式都一致,忽略 Windows 换行转换
以上就是 C 语言文件 IO 的全部核心内容,是 C 语言实现数据持久化、文件处理的基础模块,底层的缓冲区原理也是面试的高频进阶考点。
制作不易,如果对你有用,希望能点赞收藏支持一下。

1346

被折叠的 条评论
为什么被折叠?



