C 语言文件 IO 全攻略:从基础函数到实战踩坑(BMP 读取 + 词典查询)

本文为 C 语言软件编程学习 Day03 笔记,系统梳理标准 IO 库函数、核心易混点对比,并结合「BMP 图片宽高读取」「英汉词典查询」两个实战项目,总结实操中高频踩坑点

文章目录

  • 前言
  • [一、文件 IO 核心函数全解](#一、文件 IO 核心函数全解)
    • [1. fgetc:读取单个字符](#1. fgetc:读取单个字符)
    • [2. fputs:写入字符串](#2. fputs:写入字符串)
    • [3. fgets:读取一行字符串](#3. fgets:读取一行字符串)
    • [4. fprintf:格式化写入文件](#4. fprintf:格式化写入文件)
    • [5. fwrite:整块数据写入](#5. fwrite:整块数据写入)
    • [6. fread:整块数据读取](#6. fread:整块数据读取)
    • [7. fseek:设置文件偏移位置](#7. fseek:设置文件偏移位置)
    • [8. ftell:获取当前偏移量](#8. ftell:获取当前偏移量)
    • [9. rewind:回到文件开头](#9. rewind:回到文件开头)
  • 二、易混函数深度对比
    • [1. gets 与 fgets 核心区别](#1. gets 与 fgets 核心区别)
    • [2. puts 与 fputs 核心区别](#2. puts 与 fputs 核心区别)
  • 三、实战项目与踩坑复盘
    • [项目 1:读取 24 位 BMP 图片的宽度和高度](#项目 1:读取 24 位 BMP 图片的宽度和高度)
    • [项目 2:英汉词典查询系统](#项目 2:英汉词典查询系统)
  • 四、高频易错点速查表
  • 五、课后练习:文件拷贝

前言

第三天的学习核心围绕标准文件 IO 操作展开,从单字符读写到整块数据读写,再到文件位置偏移控制,最终落地到两个实战场景。这部分知识点函数多、细节碎,坑点密集,尤其字符串与指针结合的地方非常容易出错。本文按「函数大全 → 易混对比 → 实战项目 → 易错汇总」的结构整理,方便复习和避坑。


一、文件 IO 核心函数全解

1. fgetc:读取单个字符

复制代码
int fgetc(FILE *stream);
  • 功能:从文件流中读取一个字符
  • 返回值 :成功返回字符的 ASCII 码值;失败或读到文件末尾返回 EOF
  • 注意 :返回值用int接收而不是char,因为EOF是超出 char 范围的负值

2. fputs:写入字符串

复制代码
int fputs(const char *s, FILE *stream);
  • 功能:将字符串写入指定文件流
  • 返回值 :成功返回非负整数;失败返回 EOF
  • 特点不会自动追加换行符\n,原样输出字符串内容

3. fgets:读取一行字符串

复制代码
char *fgets(char *s, int size, FILE *stream);
  • 功能 :从流中读取一行字符串,最多读取 size-1 个字符,末尾自动补\0
  • 返回值 :成功返回缓冲区首地址;失败或读到文件末尾返回NULL
  • 核心特点
    • 自带长度保护,不会缓冲区溢出
    • 会保留行尾的换行符\n,不会自动丢弃
    • 既可读取标准输入stdin,也可读取普通文件

4. fprintf:格式化写入文件

复制代码
int fprintf(FILE *stream, const char *format, ...);
  • 功能:按格式化字符串将数据写入指定文件流
  • 返回值:成功返回实际写入的字符个数;失败返回负数
  • 对比printf等价于fprintf(stdin, ...),只是默认输出到终端

5. fwrite:整块数据写入

复制代码
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
  • 功能 :向文件写入nmemb个对象,每个对象占size字节
  • 参数
    • ptr:待写入数据的内存首地址
    • size:单个对象的字节大小
    • nmemb:要写入的对象数量
  • 返回值 :成功返回实际写入的完整对象个数;失败返回 0

6. fread:整块数据读取

复制代码
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
  • 功能 :从文件中读取nmemb个对象,每个对象占size字节,存入 ptr 指向的内存
  • 返回值 :成功返回实际读到的完整对象个数;失败或读到文件末尾返回 0
  • 两种常用写法
    • 读单个结构体 / 整数:fread(&var, sizeof(var), 1, fp) → 返回值判断是否等于 1
    • 读字节缓冲区:fread(buf, 1, sizeof(buf), fp) → 返回值就是读到的字节数

7. fseek:设置文件偏移位置

复制代码
int fseek(FILE *stream, long offset, int whence);
  • 功能:重新定位文件读写光标的位置
  • 基准参数 whence
    • SEEK_SET:从文件开头计算偏移
    • SEEK_CUR:从当前光标位置计算偏移
    • SEEK_END:从文件末尾计算偏移
  • 返回值:成功返回 0;失败返回 - 1

8. ftell:获取当前偏移量

复制代码
long ftell(FILE *stream);
  • 功能:获取当前光标距离文件开头的字节偏移量

  • 经典用法 :配合 fseek 获取文件总大小

    复制代码
    fseek(fp, 0, SEEK_END);
    long file_size = ftell(fp);

9. rewind:回到文件开头

复制代码
void rewind(FILE *stream);
  • 功能:将文件光标直接重置到文件开头
  • 等价于fseek(fp, 0, SEEK_SET);

二、易混函数深度对比

1. gets 与 fgets 核心区别

这是考试最高频考点,也是实操最容易踩坑的点。

表格

对比维度 gets() fgets()
缓冲区长度 无长度检查,输入多长写多长 强制指定 size,最多读 size-1 个字符
溢出风险 极高,C11 标准已正式删除 安全,不会越界
换行符处理 读到换行直接丢弃,替换为\0 换行符保留在字符串中,再补\0
读取来源 只能从标准输入读取 可从任意文件流读取
推荐程度 禁止使用 标准推荐写法

踩坑提醒 :用fgets从键盘读取单词后,直接和文件内单词做strcmp会永远失败,因为字符串末尾多了一个\n。必须先清除换行符:

复制代码
str[strcspn(str, "\n")] = '\0';

2. puts 与 fputs 核心区别

表格

对比维度 puts() fputs()
换行符 自动在末尾追加一个\n 不会额外添加任何字符,原样输出
输出目标 只能输出到标准输出 可输出到任意文件流

三、实战项目与踩坑复盘

项目 1:读取 24 位 BMP 图片的宽度和高度

需求

从 BMP 文件中读取图片分辨率,已知宽度在文件偏移 18 字节处,占 4 字节;高度在偏移 22 字节处,占 4 字节。

核心代码
复制代码
#include <stdio.h>

int main(void)
{
    FILE *fp = fopen("src.bmp", "rb"); // 二进制文件必须用rb打开!
    if (NULL == fp) {
        perror("fopen failed");
        return -1;
    }

    unsigned int width = 0, height = 0;
    fseek(fp, 18, SEEK_SET);  // 定位到宽度字段
    fread(&width, 4, 1, fp);
    fread(&height, 4, 1, fp); // 读完宽度光标自动后移4字节,正好对准高度

    printf("宽度:%u\n高度:%u\n", width, height);
    fclose(fp);
    return 0;
}
高频踩坑点
  1. 偏移计算错误 :误以为宽度从 17 字节开始 正确逻辑:文件头占 14 字节(0-13)+ biSize 占 4 字节(14-17)= 18,宽度从 18 号字节开始。字段占 N 字节,下一字段起始位置 = 起始偏移 + N。
  2. 用文本模式打开二进制文件 :BMP 是二进制文件,必须用rb模式,否则数据会错乱。
  3. 小端序问题:BMP 采用小端存储,x86/x64CPU 原生小端,直接 fread 到 int 变量即可,无需手动翻转字节。

项目 2:英汉词典查询系统

需求

终端输入单词,从词典文件(每行格式:单词 释义,释义内部含空格)中匹配并打印对应释义。

正确实现(strtok + 指针遍历)
复制代码
#include <stdio.h>
#include <string.h>

int main(void)
{
    FILE *fp = fopen("dict.txt", "r");
    if (NULL == fp) {
        perror("fopen failed");
        return -1;
    }

    char str[100] = {0};
    char tmpbuff[4096] = {0};
    gets(str); // 仅作业演示使用,实际开发禁用gets

    while (fgets(tmpbuff, sizeof(tmpbuff), fp) != NULL) {
        // 第一步:strtok只调用一次,切出第一个单词
        char *word = strtok(tmpbuff, " ");
        if (word == NULL) continue;

        // 第二步:遍历指针越过单词末尾的\0,找到释义区域
        char *meaning = word;
        while (*meaning != '\0') meaning++; // 走到单词结尾
        meaning++; // 跨过strtok写入的\0

        // 第三步:跳过释义开头连续的空格
        while (*meaning != '\0' && *meaning == ' ') {
            meaning++;
        }

        // 第四步:字符串比较必须用strcmp
        if (strcmp(str, word) == 0) {
            printf("含义为:%s", meaning);
            break;
        }
    }

    fclose(fp);
    return 0;
}
踩坑大汇总
  1. 低级错误:文件名加双引号

    复制代码
    fopen("argv[1]", "r"); // ❌ 去找名叫argv[1]的文件,永远打开失败
    fopen(argv[1], "r");   // ✅ 正确,访问命令行参数
  2. 致命错误:用==比较字符串 ==比较的是指针地址,不是字符串内容。内容比较必须用strcmp,相等返回 0。

  3. 经典错误:多次调用 strtok 切碎释义 strtok(NULL, " ")会把释义内部的空格也当分隔符切开,只能拿到半截释义。正确做法是只调用一次 strtok 切单词,剩余内容靠指针遍历获取。

  4. 隐形错误:fgets 带换行符 输入单词和词典行尾都可能带\n,不处理会导致 strcmp 永远匹配失败。


四、高频易错点速查表

易错点 错误写法 正确写法
字符串比较 if(str1 == str2) if(strcmp(str1, str2) == 0)
命令行参数传文件名 fopen("argv[1]","r") fopen(argv[1],"r")
二进制文件打开 fopen("a.bmp","r") fopen("a.bmp","rb")
gets 安全问题 gets(str) fgets(str, sizeof(str), stdin)
fgets 换行符 直接拿去 strcmp str[strcspn(str,"\n")]='\0'
strtok 切释义 strtok(NULL, " ") 只切一次单词,剩余内容指针遍历
BMP 偏移计算 宽度偏移 17 宽度偏移 18
ftell 返回值 %d打印 %ld打印(long 类型)
指针遍历边界 先判断字符再判断 \0 先判断!='\0'再判断字符,利用短路保护

五、课后练习:文件拷贝

利用freadfwrite实现二进制文件拷贝(图片、文档均可),核心框架:

复制代码
#include <stdio.h>

int main(int argc, char *argv[])
{
    if (argc < 3) {
        printf("usage: ./a.out src_file dst_file\n");
        return -1;
    }

    FILE *fp_src = fopen(argv[1], "rb");
    FILE *fp_dst = fopen(argv[2], "wb");
    char buf[4096] = {0};
    size_t n = 0;

    while ((n = fread(buf, 1, sizeof(buf), fp_src)) > 0) {
        fwrite(buf, 1, n, fp_dst);
    }

    fclose(fp_src);
    fclose(fp_dst);
    return 0;
}
相关推荐
老当益壮梁奶奶1 小时前
Linux软件编程学习笔记(二)Linux文件IO编程入门:从fopen到fgetc
linux·c语言·c++·笔记·学习
Shinner欣儿1 小时前
React18 并发渲染小记
前端
用户921080262861 小时前
AI 对话里的消息时间线分页:上滑加载更多历史记忆的实现与坑点
前端
PedroQue991 小时前
Vue-Router 2.4.0 新增可控重定向功能
前端·uni-app
草莓熊Lotso1 小时前
【Linux网络加餐】手动部署:SSH 与 Web 服务实战 + 底层原理全解析
linux·运维·网络·人工智能·python·langchain·ssh
CHHH_HHH1 小时前
【Linux系统篇】深入解析Linux文件系统:从磁盘寻址到软硬链接
linux·服务器·开发语言·后端·ubuntu
蒸蒸yyyyzwd1 小时前
cpp选手秋招学习笔记 day12
笔记·学习·oracle
Wang's Blog2 小时前
PostgreSQL笔记38:慢查询定位与优化的系统方法论
数据库·笔记·postgresql
阿萨德528号2 小时前
npm 包发布实战指南:从零发布、更新迭代到版本治理
前端·npm·策略模式