本文为 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;
}
高频踩坑点
- 偏移计算错误 :误以为宽度从 17 字节开始 正确逻辑:文件头占 14 字节(0-13)+ biSize 占 4 字节(14-17)= 18,宽度从 18 号字节开始。字段占 N 字节,下一字段起始位置 = 起始偏移 + N。
- 用文本模式打开二进制文件 :BMP 是二进制文件,必须用
rb模式,否则数据会错乱。 - 小端序问题: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;
}
踩坑大汇总
-
低级错误:文件名加双引号
fopen("argv[1]", "r"); // ❌ 去找名叫argv[1]的文件,永远打开失败 fopen(argv[1], "r"); // ✅ 正确,访问命令行参数 -
致命错误:用
==比较字符串==比较的是指针地址,不是字符串内容。内容比较必须用strcmp,相等返回 0。 -
经典错误:多次调用 strtok 切碎释义
strtok(NULL, " ")会把释义内部的空格也当分隔符切开,只能拿到半截释义。正确做法是只调用一次 strtok 切单词,剩余内容靠指针遍历获取。 -
隐形错误: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'再判断字符,利用短路保护 |
五、课后练习:文件拷贝
利用fread和fwrite实现二进制文件拷贝(图片、文档均可),核心框架:
#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;
}