1. 文本文件与二进制文件概念
在开始学习标准 I/O 的二进制读写之前,需要区分两个核心概念:文本文件与二进制文件。
-
文本文件 (Text File)
- 存在形式:通常以人类可读的字符形式存在(例如
.c,.txt文件)。 - 存储内容:内部存储的是字符对应的 ASCII 值。
- 计算机存储原理:虽然人类看到的是字符,但计算机底层存储的是 ASCII 值对应的二进制(0/1)形式。
- 特点:适合人类直接阅读和编辑。
- 存在形式:通常以人类可读的字符形式存在(例如
-
二进制文件 (Binary File)
- 存在形式:编译后的可执行文件或其他非文本数据文件。
- 存储内容:直接存储数据的二进制形式(0/1),没有经过字符编码转换。
- 特点:人类直接打开阅读通常是乱码,但计算机处理效率更高。
- 本质:对于计算机底层而言,文本文件和二进制文件最终都是以二进制(0/1)形式存储的,区别在于解释和读写的方式不同。
-
编程中的应用
- 一般编程中处理文本文件较多。
- 处理后台信息或特定数据结构时,接触二进制文件较多。
- 标准 I/O 接口(如
fread,fwrite)既可以读文本文件,也可以读二进制文件,具体取决于文件本身的内容格式。接口原理是读取字节(ASCII 值或二进制值)。
2. 二进制读写接口 fread 与 fwrite
2.1 fread 函数详解
- 函数原型
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); - 参数说明
void *ptr:读取内容存放的位置(缓冲区指针)。size_t size:每个对象的大小(字节数)。size_t nmemb:要读取的对象个数。FILE *stream:文件流指针(指定从哪个文件流读取)。
- 返回值
- 成功:返回成功读取的对象个数(
nmemb)。 - 文件结束:如果读到文件末尾,返回 0。
- 出错:返回负数或小于请求个数的值(通常配合
feof或ferror判断)。
- 成功:返回成功读取的对象个数(
- 特点
- 不同于
getchar或fgets默认限制大小,fread允许自定义每次读取的大小和数量。 - 原理是读取字节对应的 ASCII 值或二进制值。
- 不同于
2.2 fwrite 函数详解
- 函数原型
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); - 参数说明
const void *ptr:要写入内容的对象首地址(注意加了const修饰,表示写入过程中内容不可被修改)。size_t size:每个对象的大小。size_t nmemb:要写入的对象个数。FILE *stream:文件流指针(指定写入到哪个文件)。
- 返回值
- 成功:返回成功写入的对象个数。
- 出错:返回小于请求个数的值。
3. 代码实战:fread 测试
3.1 测试文本文件
- 准备:创建一个名为
test.txt的文本文件,内容为 "Hello World" 等字符。 - 代码逻辑
- 定义文件流指针
FILE *fp并初始化。 - 定义缓冲区
char buffer[10]并初始化。 - 使用
fopen打开文件,模式为"r",并进行 NULL 判断。 - 调用
fread(buffer, 1, 10, fp):每次读 1 字节,读 10 个。 - 判断返回值
read_size,若小于 0 则报错并fclose。 - 使用
printf输出 buffer 内容。 - 再次调用
fread读取剩余内容。 - 最后
fclose关闭文件。
- 定义文件流指针
- 结果观察
- 第一次读取 10 个字符("Hello World" 包含空格共 11 字符,视具体内容而定)。
- 第二次读取可能遇到换行符,换行符也被当作一个字符读取。
- 文本文件内容可读。
3.2 测试二进制文件
- 准备:将编译后的可执行文件(如
fread编译出的二进制文件)作为输入文件。 - 操作:通过命令行参数
argv传入二进制文件路径。 - 结果观察
fread能正常读取数据,不会报错。- 但使用
printf输出 buffer 时,显示为乱码(不可理解的字符)。 - 证明
fread可以读取二进制文件,但内容是以二进制形式存储的。
4. 代码实战:fwrite 与结构体对象
4.1 强化对象概念
- 为了强化“对象”读写概念,不使用字符数组,而是使用 结构体 (struct) 作为对象。
- 结构体定义示例
struct Stu { int id; char name[20]; // 防止字节对齐问题,适当预留空间 float score; }; - 测试逻辑
- 定义结构体数组
struct Stu stus[2]并初始化数据(如 ID, 姓名,成绩)。 - 计算对象大小:
sizeof(struct Stu)。 - 使用
fopen打开文件,模式建议使用"a"(追加) 或"w",避免覆盖原有内容以便观察。 - 调用
fwrite(&stus[0], sizeof(struct Stu), 1, fp)写入第一个对象。 - 缓冲问题演示:
- 写入后加入
sleep(5)暂停程序。 - 此时查看文件,发现内容尚未写入。
- 原因:标准 I/O 文件流通常是 全缓冲 (Full Buffered) 的,数据先存在缓冲区,未填满或未关闭/刷新前不会写入磁盘。
- 解决:调用
fflush(fp)强制刷新缓冲区,数据会立即写入文件。
- 写入后加入
- 继续写入第二个对象
stus[1]。 - 最后
fclose关闭文件。
- 定义结构体数组
4.2 读写指针位置问题
- 现象:如果在写入后立即尝试读取同一文件流,可能读不到数据。
- 原因:写入操作后,文件流指针位于文件末尾 (EOF)。
- 解决方案
- 使用
rewind(fp)将指针重置到文件开头。 - 或者关闭文件后,重新以
"r"模式打开文件进行读取。
- 使用
- 读取验证
- 使用
fread读取刚才写入的结构体。 - 必须使用与写入时相同的结构体定义和大小,才能正确解析二进制数据。
- 如果直接用文本方式打开该文件,看到的是二进制乱码;必须用对应的
fread逻辑读回结构体变量后才能正常显示内容。
- 使用
5. 实战案例:文件拷贝程序
5.1 需求与设计
- 目标:实现一个文件拷贝程序,将源文件内容完整复制到目标文件。
- 参数:通过命令行参数
argv[1](源文件) 和argv[2](目标文件) 传递路径。 - 流程
- 定义两个文件流指针
fp_src,fp_dest。 - 打开源文件(
"r"或"rb"),判断是否成功。 - 打开目标文件(
"w"或"wb"),判断是否成功。 - 定义缓冲区
char buffer[512]。 - 定义变量记录实际读取字节数
size_t bytes_read。 - 进入循环进行读写操作。
- 定义两个文件流指针
5.2 核心逻辑实现
- 循环读写
while (1) { // 1. 读取 bytes_read = fread(buffer, 1, 512, fp_src); // 2. 判断读取错误 if (bytes_read < 0) { printf("Read failed\n"); goto cleanup; // 错误处理跳转 } // 3. 判断文件结束 if (feof(fp_src)) { printf("Read finished (EOF)\n"); break; } // 4. 写入 size_t bytes_written = fwrite(buffer, 1, bytes_read, fp_dest); // 5. 判断写入错误或不等 if (bytes_written < 0 || bytes_written != bytes_read) { printf("Write failed or incomplete\n"); goto cleanup; } // 6. 清空缓冲区 (防止脏数据) bzero(buffer, sizeof(buffer)); } - 关键点说明
feof判断:用于检测文件流指针是否到达末尾。当fread返回 0 且feof为非零值时,表示正常结束。bzero:每次循环后清空 buffer,防止最后一次读取不足 512 字节时残留旧数据影响后续处理(虽然fwrite指定了实际读取长度,但清空是好习惯)。- 错误处理:使用
goto统一跳转到清理代码块,关闭所有打开的文件指针。 - 返回值检查:必须比较
bytes_read和bytes_written,确保读写一致性。
5.3 测试验证
- 编译程序。
- 运行:
./copy_program source.txt dest.txt。 - 验证:对比源文件和目标文件内容,确保完全一致(包括二进制文件)。
6. 格式化输入输出接口
除了基本的字节读写,标准 I/O 还提供了格式化输入输出接口,适用于需要特定数据格式的场景。
6.1 带流的格式化 I/O
fprintf(格式化输出到流)- 原型:
int fprintf(FILE *stream, const char *format, ...); - 功能:类似于
printf,但输出目标是指定的文件流stream而非标准输出。 - 用途:将格式化后的数据写入文件。
- 原型:
fscanf(从流格式化输入)- 原型:
int fscanf(FILE *stream, const char *format, ...); - 功能:类似于
scanf,但输入来源是指定的文件流stream而非标准输入。 - 用途:从文件中按格式读取数据到变量。
- 原型:
6.2 不带流的格式化 I/O (字符串操作)
sprintf(格式化输出到字符串)- 原型:
int sprintf(char *str, const char *format, ...); - 功能:将格式化后的数据输出到字符数组
str中,不涉及文件流。 - 用途:构建特定格式的字符串缓冲区。
- 原型:
sscanf(从字符串格式化输入)- 原型:
int sscanf(const char *str, const char *format, ...); - 功能:从字符串
str中按格式解析数据到变量。 - 用途:解析特定格式的字符串内容。
- 原型:
6.3 返回值与错误处理
- 成功:返回成功匹配并赋值的参数个数。
- 失败/结束:返回
EOF。 - 注意:如果格式不匹配(如字符串中数据不足),返回值会小于预期参数个数。
6.4 代码演示逻辑
fprintf测试:打开文件,使用fprintf(fp, "%s", "Hello")写入格式化字符串。fscanf测试:结合标准输入stdin或文件,使用fscanf(stdin, "%s", buffer)读取用户输入,再写入文件。sscanf测试:- 定义字符串
"1 2"。 - 使用
sscanf(str, "%d %d %d", &a, &b, &c)尝试解析 3 个整数。 - 结果:
a=1,b=2,c未被赋值(因为源字符串数据不足)。
- 定义字符串
sprintf测试:- 定义 buffer。
- 使用
sprintf(buffer, "A=%d, B=%d", a, b)生成格式化字符串。 - 输出 buffer 验证内容。
7. 应用场景总结
- 网络编程
- 在设计通信协议时,可使用格式化 I/O 组织数据包格式,方便解析。
- 日志系统 (Logging)
- 使用
fprintf或sprintf将程序运行数据格式化为特定结构的日志字符串,存入日志文件,便于后续阅读和分析。
- 使用
- 数据持久化
- 使用
fread/fwrite保存结构体对象(二进制方式),效率高但兼容性差(依赖平台字节序)。 - 使用
fprintf/fscanf保存文本格式数据,兼容性好在不同系统间交换,但效率略低。
- 使用
- 字符串处理
sprintf/sscanf常用于复杂的字符串解析和构建,替代手动字符串操作。
8. 注意事项
- 缓冲区刷新:写文件时注意全缓冲机制,及时使用
fflush或确保fclose被调用,以免数据丢失。 - 文件指针复位:同一文件流进行写后读操作前,务必使用
rewind或fseek重置指针。 - 二进制兼容性:使用
fread/fwrite读写结构体时,需注意不同平台间的字节对齐和大小端问题,跨平台传输建议转为文本或网络字节序。 - 错误检查:所有 I/O 操作(open, read, write, close)都必须检查返回值,确保程序健壮性。
- 资源清理:程序退出前确保所有打开的文件流都已关闭,防止资源泄漏。
- Buffer 安全:使用
bzero或memset清空缓冲区,避免脏数据干扰。