彻底搞懂 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. 函数原型与头文件#includestdio.hsize_tfread(void*ptr,size_tsize,size_tnmemb,FILE*stream);所属库标准输入输出库stdio.h功能从文件流中读取数据到内存缓冲区返回值类型size_t无符号整数通常为unsigned long2. 四个参数详细拆解参数类型名称含义比喻ptrvoid *缓冲区指针数据读到哪里去一个空篮子sizesize_t每个元素的大小字节每个物品占多大空间每个鸡蛋的大小nmembsize_t要读取的元素个数想读多少个物品想拿多少个鸡蛋streamFILE *文件流指针从哪里读从哪个鸡窝拿关键理解size和nmemb共同决定要读的总字节数总字节数 size × nmemb但返回值不是总字节数而是成功读到的完整元素个数。3. 返回值的秘密size_tnfread(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 个int28 字节 1 个多余字节还是 9 个int少了 7 字节通过size和nmemb分离你可以直接知道“我读到了3 个完整结构体”。4.2 原子性保证fread保证要么读到一个完整的元素要么完全不读这个元素。绝不会出现“半个元素”的情况。例如size sizeof(Student) 64文件还剩 100 字节你请求nmemb 10fread会先计算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按字节读取文件最常用#includestdio.hintmain(){FILE*fpfopen(hello.txt,rb);if(!fp){perror(fopen);return1;}charbuf[256];size_tnfread(buf,1,sizeof(buf)-1,fp);// 保留一个字节给 \0buf[n]\0;// 手动添加字符串结束符printf(读取了 %zu 个字节\n,n);printf(内容%s\n,buf);fclose(fp);return0;}说明size1时返回的n就等于实际读取的字节数。示例 2读取整数数组#includestdio.hintmain(){// 先写入一些整数FILE*fpfopen(ints.bin,wb);intdata[]{100,200,300,400,500};fwrite(data,sizeof(int),5,fp);fclose(fp);// 再读取回来fpfopen(ints.bin,rb);intarr[10]{0};size_tnfread(arr,sizeof(int),10,fp);printf(成功读取 %zu 个整数\n,n);for(size_ti0;in;i){printf(%d ,arr[i]);}printf(\n);fclose(fp);return0;}示例 3读取结构体数组核心应用#includestdio.h#includestring.htypedefstruct{intid;charname[32];floatscore;}Student;intmain(){// 写入 3 个学生Student stu[]{{101,Alice,88.5},{102,Bob,92.0},{103,Charlie,76.5}};FILE*fpfopen(students.dat,wb);fwrite(stu,sizeof(Student),3,fp);fclose(fp);// 读取学生fpfopen(students.dat,rb);Student buffer[10];size_tnfread(buffer,sizeof(Student),10,fp);printf(实际读取了 %zu 个学生记录\n,n);for(size_ti0;in;i){printf(ID: %d, Name: %s, Score: %.1f\n,buffer[i].id,buffer[i].name,buffer[i].score);}fclose(fp);return0;}示例 4处理不完整读取文件提前结束#includestdio.hintmain(){// 创建一个只有 5 字节的文件FILE*fpfopen(tiny.bin,wb);fwrite(ABCDE,1,5,fp);fclose(fp);// 尝试读取 3 个 int每个 4 字节需要 12 字节fpfopen(tiny.bin,rb);intarr[3];size_tnfread(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循环读取直到文件结束处理任意剩余字节#includestdio.hintmain(){FILE*fpfopen(largefile.bin,rb);if(!fp)return1;charbuf[4096];size_tn;while((nfread(buf,1,sizeof(buf),fp))0){// 处理读到的 n 个字节fwrite(buf,1,n,stdout);// 直接输出到屏幕}fclose(fp);return0;}解释当size1时fread返回的就是实际字节数非常适合流式拷贝。示例 6使用fread读取网络数据模拟#includestdio.h#includestring.hintmain(){// 模拟从网络收到的数据包内存流charfake_network_data[]\x01\x00\x00\x00Hello\x02\x00\x00\x00World;FILE*memstreamfmemopen(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结合#includestdio.h#includestdlib.hintsafe_read(FILE*fp,void*buf,size_telem_size,size_telem_count){size_tnfread(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*fpfopen(maybe_corrupt.bin,rb);if(!fp)return1;doubledata[100];intretsafe_read(fp,data,sizeof(double),100);if(ret0){printf(读取失败\n);}else{printf(成功读取 %d 个 double\n,ret);}fclose(fp);return0;}示例 8大文件块读取性能对比示意#includestdio.h#includetime.hintmain(){FILE*fpfopen(bigfile.bin,rb);if(!fp)return1;charbuf[1024*1024];// 1 MB 缓冲区clock_tstartclock();size_ttotal0;size_tn;while((nfread(buf,1,sizeof(buf),fp))0){totaln;}clock_tendclock();printf(读取 %zu 字节耗时 %.2f 秒\n,total,(double)(end-start)/CLOCKS_PER_SEC);fclose(fp);return0;}6. 常见错误与最佳实践❌ 错误 1误解返回值// 错误size_tnfread(buf,4,10,fp);if(n40){...}// 永远不可能成立因为 n 最大是 10✅正确if(n10){/* 没读够 10 个 int */}❌ 错误 2忘记检查文件结束或错误fread(buf,1,100,fp);// 如果只读了 50 字节可能不知道是文件尾还是错误✅正确size_tnfread(buf,1,100,fp);if(n100){if(feof(fp))puts(到达文件尾);elseif(ferror(fp))puts(读取出错);}❌ 错误 3混用fread和read于同一描述符fread有用户态缓冲区直接混用会导致数据错位。如果必须混用调用fflush或fileno但极其危险不建议。✅ 最佳实践读取二进制文件时总是用rb模式打开避免 Windows 下换行符转换问题。用size1处理任意字节流用sizesizeof(T)处理固定类型数组。每次调用后检查返回值并配合feof/ferror判断原因。为大文件使用足够大的缓冲区通常 4KB ~ 1MB减少实际 I/O 次数。7. 总结速查表场景调用方式返回值含义读取任意字节流如拷贝文件fread(buf, 1, count, fp)实际读取的字节数读取固定大小元素如 intfread(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!