彻底搞懂 C 语言 `fread` 函数:从入门到精通
文章目录
- 彻底搞懂 C 语言 `fread` 函数:从入门到精通
- 📌 目录
- 1. 函数原型与头文件
- 2. 四个参数详细拆解
- 3. 返回值的秘密
- 4. 为什么这样设计?(设计哲学)
- 4.1 面向数据块,而非原始字节
- 4.2 原子性保证
- 4.3 与 `fwrite` 完全对称
- 5. 完整代码示例(8 个场景)
- 示例 1:按字节读取文件(最常用)
- 示例 2:读取整数数组
- 示例 3:读取结构体数组(核心应用)
- 示例 4:处理不完整读取(文件提前结束)
- 示例 5:循环读取直到文件结束(处理任意剩余字节)
- 示例 6:使用 `fread` 读取网络数据(模拟)
- 示例 7:错误处理与 `ferror` / `feof` 结合
- 示例 8:大文件块读取性能对比(示意)
- 6. 常见错误与最佳实践
- ❌ 错误 1:误解返回值
- ❌ 错误 2:忘记检查文件结束或错误
- ❌ 错误 3:混用 `fread` 和 `read` 于同一描述符
- ✅ 最佳实践
- 7. 总结速查表
- 最后的话
彻底搞懂 C 语言fread函数:从入门到精通
一篇博客带你掌握
fread的设计思想、参数细节、完整示例和避坑指南
📌 目录
- 函数原型与头文件
- 四个参数详细拆解
- 返回值的秘密
- 为什么这样设计?(设计哲学)
- 完整代码示例(8 个场景)
- 常见错误与最佳实践
- 总结速查表
1. 函数原型与头文件
#include<stdio.h>size_tfread(void*ptr,size_tsize,size_tnmemb,FILE*stream);- 所属库:标准输入输出库
<stdio.h> - 功能:从文件流中读取数据到内存缓冲区
- 返回值类型:
size_t(无符号整数,通常为unsigned long)
2. 四个参数详细拆解
| 参数 | 类型 | 名称 | 含义 | 比喻 |
|---|---|---|---|---|
ptr | void * | 缓冲区指针 | 数据读到哪里去 | 一个空篮子 |
size | size_t | 每个元素的大小(字节) | 每个物品占多大空间 | 每个鸡蛋的大小 |
nmemb | size_t | 要读取的元素个数 | 想读多少个物品 | 想拿多少个鸡蛋 |
stream | FILE * | 文件流指针 | 从哪里读 | 从哪个鸡窝拿 |
关键理解:
size和nmemb共同决定要读的总字节数:总字节数 = size × nmemb- 但返回值不是总字节数,而是成功读到的完整元素个数。
3. 返回值的秘密
size_tn=fread(ptr,size,nmemb,stream);- 成功时:
n等于实际读到的完整元素个数。- 如果
n == nmemb,说明全部读完了。 - 如果
0 < n < nmemb,说明文件提前结束(或出错),但至少读到了n个完整元素。
- 如果
- 失败或文件尾:
- 若文件一开始就到达末尾,则
n == 0。 - 若发生读取错误,
n可能小于nmemb,需要配合ferror(stream)判断。
- 若文件一开始就到达末尾,则
重要:不要用返回值去判断读了多少字节,除非你明确知道size == 1。
4. 为什么这样设计?(设计哲学)
4.1 面向数据块,而非原始字节
C 语言经常用来处理结构体、数组等复合数据类型。如果fread只返回字节数,你读到 29 个字节时,很难判断这是完整的 7 个int(28 字节)+ 1 个多余字节,还是 9 个int少了 7 字节?
通过size和nmemb分离,你可以直接知道“我读到了3 个完整结构体”。
4.2 原子性保证
fread保证:要么读到一个完整的元素,要么完全不读这个元素。绝不会出现“半个元素”的情况。
例如size = sizeof(Student) = 64,文件还剩 100 字节,你请求nmemb = 10:
fread会先计算64 × 10 = 640字节,然后尝试读取。- 实际只读到 100 字节 → 只能提供 1 个完整结构体(64 字节)和 36 字节残留。
- 返回值
n = 1,那 36 字节被丢弃(文件指针移动到 64 字节位置)。 - 这样你永远不会拿到一个残缺的
Student。
4.3 与fwrite完全对称
fwrite(ptr,size,nmemb,stream);// 写入 nmemb 个元素fread(ptr,size,nmemb,stream);// 读取 nmemb 个元素对称的接口让代码易于理解和维护。
5. 完整代码示例(8 个场景)
以下所有示例均可在 GCC / Clang 下编译运行。
示例 1:按字节读取文件(最常用)
#include<stdio.h>intmain(){FILE*fp=fopen("hello.txt","rb");if(!fp){perror("fopen");return1;}charbuf[256];size_tn=fread(buf,1,sizeof(buf)-1,fp);// 保留一个字节给 '\0'buf[n]='\0';// 手动添加字符串结束符printf("读取了 %zu 个字节\n",n);printf("内容:%s\n",buf);fclose(fp);return0;}说明:size=1时,返回的n就等于实际读取的字节数。
示例 2:读取整数数组
#include<stdio.h>intmain(){// 先写入一些整数FILE*fp=fopen("ints.bin","wb");intdata[]={100,200,300,400,500};fwrite(data,sizeof(int),5,fp);fclose(fp);// 再读取回来fp=fopen("ints.bin","rb");intarr[10]={0};size_tn=fread(arr,sizeof(int),10,fp);printf("成功读取 %zu 个整数\n",n);for(size_ti=0;i<n;i++){printf("%d ",arr[i]);}printf("\n");fclose(fp);return0;}示例 3:读取结构体数组(核心应用)
#include<stdio.h>#include<string.h>typedefstruct{intid;charname[32];floatscore;}Student;intmain(){// 写入 3 个学生Student stu[]={{101,"Alice",88.5},{102,"Bob",92.0},{103,"Charlie",76.5}};FILE*fp=fopen("students.dat","wb");fwrite(stu,sizeof(Student),3,fp);fclose(fp);// 读取学生fp=fopen("students.dat","rb");Student buffer[10];size_tn=fread(buffer,sizeof(Student),10,fp);printf("实际读取了 %zu 个学生记录\n",n);for(size_ti=0;i<n;i++){printf("ID: %d, Name: %s, Score: %.1f\n",buffer[i].id,buffer[i].name,buffer[i].score);}fclose(fp);return0;}示例 4:处理不完整读取(文件提前结束)
#include<stdio.h>intmain(){// 创建一个只有 5 字节的文件FILE*fp=fopen("tiny.bin","wb");fwrite("ABCDE",1,5,fp);fclose(fp);// 尝试读取 3 个 int(每个 4 字节,需要 12 字节)fp=fopen("tiny.bin","rb");intarr[3];size_tn=fread(arr,sizeof(int),3,fp);if(n!=3){if(feof(fp)){printf("文件提前结束:只读到 %zu 个完整的 int\n",n);}elseif(ferror(fp)){printf("读取过程中发生错误\n");}}fclose(fp);return0;}输出:文件提前结束:只读到 0 个完整的 int
示例 5:循环读取直到文件结束(处理任意剩余字节)
#include<stdio.h>intmain(){FILE*fp=fopen("largefile.bin","rb");if(!fp)return1;charbuf[4096];size_tn;while((n=fread(buf,1,sizeof(buf),fp))>0){// 处理读到的 n 个字节fwrite(buf,1,n,stdout);// 直接输出到屏幕}fclose(fp);return0;}解释:当size=1时,fread返回的就是实际字节数,非常适合流式拷贝。
示例 6:使用fread读取网络数据(模拟)
#include<stdio.h>#include<string.h>intmain(){// 模拟从网络收到的数据包(内存流)charfake_network_data[]="\x01\x00\x00\x00Hello\x02\x00\x00\x00World";FILE*memstream=fmemopen(fake_network_data,sizeof(fake_network_data)-1,"rb");if(!memstream)return1;intlen1,len2;charstr1[100],str2[100];// 读取第一个字符串的长度和内容fread(&len1,sizeof(int),1,memstream);fread(str1,1,len1,memstream);str1[len1]='\0';// 读取第二个字符串fread(&len2,sizeof(int),1,memstream);fread(str2,1,len2,memstream);str2[len2]='\0';printf("str1 = %s\n",str1);printf("str2 = %s\n",str2);fclose(memstream);return0;}示例 7:错误处理与ferror/feof结合
#include<stdio.h>#include<stdlib.h>intsafe_read(FILE*fp,void*buf,size_telem_size,size_telem_count){size_tn=fread(buf,elem_size,elem_count,fp);if(n!=elem_count){if(feof(fp)){fprintf(stderr,"警告:文件意外结束,只读了 %zu/%zu 个元素\n",n,elem_count);}elseif(ferror(fp)){fprintf(stderr,"错误:读取文件时发生 I/O 错误\n");return-1;}}return(int)n;}intmain(){FILE*fp=fopen("maybe_corrupt.bin","rb");if(!fp)return1;doubledata[100];intret=safe_read(fp,data,sizeof(double),100);if(ret<0){printf("读取失败\n");}else{printf("成功读取 %d 个 double\n",ret);}fclose(fp);return0;}示例 8:大文件块读取性能对比(示意)
#include<stdio.h>#include<time.h>intmain(){FILE*fp=fopen("bigfile.bin","rb");if(!fp)return1;charbuf[1024*1024];// 1 MB 缓冲区clock_tstart=clock();size_ttotal=0;size_tn;while((n=fread(buf,1,sizeof(buf),fp))>0){total+=n;}clock_tend=clock();printf("读取 %zu 字节,耗时 %.2f 秒\n",total,(double)(end-start)/CLOCKS_PER_SEC);fclose(fp);return0;}6. 常见错误与最佳实践
❌ 错误 1:误解返回值
// 错误size_tn=fread(buf,4,10,fp);if(n<40){...}// 永远不可能成立,因为 n 最大是 10✅正确:
if(n<10){/* 没读够 10 个 int */}❌ 错误 2:忘记检查文件结束或错误
fread(buf,1,100,fp);// 如果只读了 50 字节,可能不知道是文件尾还是错误✅正确:
size_tn=fread(buf,1,100,fp);if(n<100){if(feof(fp))puts("到达文件尾");elseif(ferror(fp))puts("读取出错");}❌ 错误 3:混用fread和read于同一描述符
fread有用户态缓冲区,直接混用会导致数据错位。如果必须混用,调用fflush或fileno但极其危险,不建议。
✅ 最佳实践
- 读取二进制文件时,总是用
"rb"模式打开(避免 Windows 下换行符转换问题)。 - 用
size=1处理任意字节流,用size=sizeof(T)处理固定类型数组。 - 每次调用后检查返回值,并配合
feof/ferror判断原因。 - 为大文件使用足够大的缓冲区(通常 4KB ~ 1MB),减少实际 I/O 次数。
7. 总结速查表
| 场景 | 调用方式 | 返回值含义 |
|---|---|---|
| 读取任意字节流(如拷贝文件) | fread(buf, 1, count, fp) | 实际读取的字节数 |
| 读取固定大小元素(如 int) | fread(arr, sizeof(int), N, fp) | 成功读取的 int 个数 |
| 读取结构体数组 | fread(stu, sizeof(Student), N, fp) | 成功读取的结构体个数 |
| 判断是否全部读完 | if (n == nmemb) | 全部成功 |
| 判断文件尾 | if (n < nmemb && feof(fp)) | 文件提前结束 |
| 判断 I/O 错误 | if (n < nmemb && ferror(fp)) | 发生错误 |
最后的话
fread是 C 语言中处理二进制 I/O 的核心函数。它的设计巧妙地将字节流抽象为元素流,让程序员能直接操作高级数据类型。理解size和nmemb的分离,以及返回值的真实含义,是写出健壮文件操作代码的关键。
希望这篇博客能帮你彻底掌握fread。如果在项目中遇到奇怪的读取问题,记得先检查返回值,再用feof和ferror诊断!
Happy Coding!🚀
