标准IO:对象、格式化读写

标准IO:对象、格式化读写

1. 文本文件与二进制文件概念

在开始学习标准 I/O 的二进制读写之前,需要区分两个核心概念:文本文件与二进制文件。

  • 文本文件 (Text File)

    • 存在形式:通常以人类可读的字符形式存在(例如 .c, .txt 文件)。
    • 存储内容:内部存储的是字符对应的 ASCII 值。
    • 计算机存储原理:虽然人类看到的是字符,但计算机底层存储的是 ASCII 值对应的二进制(0/1)形式。
    • 特点:适合人类直接阅读和编辑。
  • 二进制文件 (Binary File)

    • 存在形式:编译后的可执行文件或其他非文本数据文件。
    • 存储内容:直接存储数据的二进制形式(0/1),没有经过字符编码转换。
    • 特点:人类直接打开阅读通常是乱码,但计算机处理效率更高。
    • 本质:对于计算机底层而言,文本文件和二进制文件最终都是以二进制(0/1)形式存储的,区别在于解释和读写的方式不同。
  • 编程中的应用

    • 一般编程中处理文本文件较多。
    • 处理后台信息或特定数据结构时,接触二进制文件较多。
    • 标准 I/O 接口(如 fread, fwrite)既可以读文本文件,也可以读二进制文件,具体取决于文件本身的内容格式。接口原理是读取字节(ASCII 值或二进制值)。

2. 二进制读写接口 freadfwrite

2.1 fread 函数详解

  • 函数原型
    size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
    
  • 参数说明
    1. void *ptr:读取内容存放的位置(缓冲区指针)。
    2. size_t size:每个对象的大小(字节数)。
    3. size_t nmemb:要读取的对象个数。
    4. FILE *stream:文件流指针(指定从哪个文件流读取)。
  • 返回值
    • 成功:返回成功读取的对象个数(nmemb)。
    • 文件结束:如果读到文件末尾,返回 0。
    • 出错:返回负数或小于请求个数的值(通常配合 feofferror 判断)。
  • 特点
    • 不同于 getcharfgets 默认限制大小,fread 允许自定义每次读取的大小和数量。
    • 原理是读取字节对应的 ASCII 值或二进制值。

2.2 fwrite 函数详解

  • 函数原型
    size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
    
  • 参数说明
    1. const void *ptr:要写入内容的对象首地址(注意加了 const 修饰,表示写入过程中内容不可被修改)。
    2. size_t size:每个对象的大小。
    3. size_t nmemb:要写入的对象个数。
    4. FILE *stream:文件流指针(指定写入到哪个文件)。
  • 返回值
    • 成功:返回成功写入的对象个数。
    • 出错:返回小于请求个数的值。

3. 代码实战:fread 测试

3.1 测试文本文件

  • 准备:创建一个名为 test.txt 的文本文件,内容为 "Hello World" 等字符。
  • 代码逻辑
    1. 定义文件流指针 FILE *fp 并初始化。
    2. 定义缓冲区 char buffer[10] 并初始化。
    3. 使用 fopen 打开文件,模式为 "r",并进行 NULL 判断。
    4. 调用 fread(buffer, 1, 10, fp):每次读 1 字节,读 10 个。
    5. 判断返回值 read_size,若小于 0 则报错并 fclose
    6. 使用 printf 输出 buffer 内容。
    7. 再次调用 fread 读取剩余内容。
    8. 最后 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;
    };
    
  • 测试逻辑
    1. 定义结构体数组 struct Stu stus[2] 并初始化数据(如 ID, 姓名,成绩)。
    2. 计算对象大小:sizeof(struct Stu)
    3. 使用 fopen 打开文件,模式建议使用 "a" (追加) 或 "w",避免覆盖原有内容以便观察。
    4. 调用 fwrite(&stus[0], sizeof(struct Stu), 1, fp) 写入第一个对象。
    5. 缓冲问题演示
      • 写入后加入 sleep(5) 暂停程序。
      • 此时查看文件,发现内容尚未写入。
      • 原因:标准 I/O 文件流通常是 全缓冲 (Full Buffered) 的,数据先存在缓冲区,未填满或未关闭/刷新前不会写入磁盘。
      • 解决:调用 fflush(fp) 强制刷新缓冲区,数据会立即写入文件。
    6. 继续写入第二个对象 stus[1]
    7. 最后 fclose 关闭文件。

4.2 读写指针位置问题

  • 现象:如果在写入后立即尝试读取同一文件流,可能读不到数据。
  • 原因:写入操作后,文件流指针位于文件末尾 (EOF)。
  • 解决方案
    1. 使用 rewind(fp) 将指针重置到文件开头。
    2. 或者关闭文件后,重新以 "r" 模式打开文件进行读取。
  • 读取验证
    • 使用 fread 读取刚才写入的结构体。
    • 必须使用与写入时相同的结构体定义和大小,才能正确解析二进制数据。
    • 如果直接用文本方式打开该文件,看到的是二进制乱码;必须用对应的 fread 逻辑读回结构体变量后才能正常显示内容。

5. 实战案例:文件拷贝程序

5.1 需求与设计

  • 目标:实现一个文件拷贝程序,将源文件内容完整复制到目标文件。
  • 参数:通过命令行参数 argv[1] (源文件) 和 argv[2] (目标文件) 传递路径。
  • 流程
    1. 定义两个文件流指针 fp_src, fp_dest
    2. 打开源文件("r""rb"),判断是否成功。
    3. 打开目标文件("w""wb"),判断是否成功。
    4. 定义缓冲区 char buffer[512]
    5. 定义变量记录实际读取字节数 size_t bytes_read
    6. 进入循环进行读写操作。

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_readbytes_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 代码演示逻辑

  1. fprintf 测试:打开文件,使用 fprintf(fp, "%s", "Hello") 写入格式化字符串。
  2. fscanf 测试:结合标准输入 stdin 或文件,使用 fscanf(stdin, "%s", buffer) 读取用户输入,再写入文件。
  3. sscanf 测试
    • 定义字符串 "1 2"
    • 使用 sscanf(str, "%d %d %d", &a, &b, &c) 尝试解析 3 个整数。
    • 结果:a=1, b=2, c 未被赋值(因为源字符串数据不足)。
  4. sprintf 测试
    • 定义 buffer。
    • 使用 sprintf(buffer, "A=%d, B=%d", a, b) 生成格式化字符串。
    • 输出 buffer 验证内容。

7. 应用场景总结

  • 网络编程
    • 在设计通信协议时,可使用格式化 I/O 组织数据包格式,方便解析。
  • 日志系统 (Logging)
    • 使用 fprintfsprintf 将程序运行数据格式化为特定结构的日志字符串,存入日志文件,便于后续阅读和分析。
  • 数据持久化
    • 使用 fread/fwrite 保存结构体对象(二进制方式),效率高但兼容性差(依赖平台字节序)。
    • 使用 fprintf/fscanf 保存文本格式数据,兼容性好在不同系统间交换,但效率略低。
  • 字符串处理
    • sprintf/sscanf 常用于复杂的字符串解析和构建,替代手动字符串操作。

8. 注意事项

  • 缓冲区刷新:写文件时注意全缓冲机制,及时使用 fflush 或确保 fclose 被调用,以免数据丢失。
  • 文件指针复位:同一文件流进行写后读操作前,务必使用 rewindfseek 重置指针。
  • 二进制兼容性:使用 fread/fwrite 读写结构体时,需注意不同平台间的字节对齐和大小端问题,跨平台传输建议转为文本或网络字节序。
  • 错误检查:所有 I/O 操作(open, read, write, close)都必须检查返回值,确保程序健壮性。
  • 资源清理:程序退出前确保所有打开的文件流都已关闭,防止资源泄漏。
  • Buffer 安全:使用 bzeromemset 清空缓冲区,避免脏数据干扰。
标准IO:字符、行 读写 2026-02-27
标准 IO:刷新、定位 2026-03-02

评论区