C语言文件操作(后篇):二进制读写与文件指针的高级跳跃
- 欢迎来到数据存储的“深水区”
-
- 1. 文本 vs 二进制:到底存哪个?
- 2. 暴力美学:`fwrite` 与 `fread`
-
- 2.1 批量写入:`fwrite`
- 2.2 批量读取:`fread`
- 3. 内存刷新:`fflush` 的救场
- 4. 文件指针的“凌波微步”
-
- 4.1 定位函数:`fseek`
- 4.2 报点函数:`ftell`
- 4.3 瞬间回城:`rewind`
- 5. 灵魂拷问:到底读完没有?
-
- `feof` 与 `ferror`
- `perror`:错误翻译官
- 总结
欢迎来到数据存储的“深水区”
你好!填坑来了。
在上一篇文章《文件操作(前篇)》中,我们学会了用 fprintf、fgets 等函数把数据转换成我们肉眼能看懂的字符,存进普通的文本文件中。这种方法简单直观,但如果你在做气象观测项目,每天有成千上万条风速、温度数据需要高频存取,一行行写字符串不仅速度慢,而且浪费硬盘空间。
今天我们就来点硬核的:跳过字符转换,直接把内存里的原始二进制数据“砸”进硬盘;并且学会如何像操作数组下标一样,在文件里自由跳转。
准备好,我们要开始 C 语言文件操作的进阶之旅了。
1. 文本 vs 二进制:到底存哪个?
在讲具体的函数前,先搞明白两种存储方式的区别:
- 文本文件:把数据转换成 ASCII 码存进去。比如整数 10000,会变成 '1', '0', '0', '0', '0' 这 5 个字符,占 5 个字节。用记事本打开看得懂。
- 二进制文件:内存里长什么样,硬盘上就原封不动存成什么样。整数 10000 在内存里直接以 4 个字节的二进制形式(0x2710)存进去。用记事本打开是一堆乱码,但程序读写极快。
对于海量数据和复杂的结构体,强烈推荐二进制读写。
2. 暴力美学:fwrite 与 fread
这对兄弟专门用来处理二进制数据,特别是数组和结构体。
假设我们定义了一个气象观测数据的结构体:
struct WeatherData {
int day; // 观测天数
float temp; // 温度
float wind_speed; // 风速
};
2.1 批量写入:fwrite
把结构体数组直接倒进文件里。注意,打开文件的模式要加上 b(代表 binary),比如 "wb"。
#include <stdio.h>
int main() {
struct WeatherData data[3] = {
{1, 14.5, 2.5},
{2, 18.2, 4.1},
{3, 12.3, 3.8}
};
FILE *pf = fopen("weather.dat", "wb"); // wb: 只写二进制
if (pf == NULL) return 1;
// 参数:数据源地址, 每个元素大小, 元素个数, 文件指针
size_t written = fwrite(data, sizeof(struct WeatherData), 3, pf);
printf("成功写入了 %zu 条观测数据。\\n", written);
fclose(pf);
pf = NULL;
return 0;
}
2.2 批量读取:fread
怎么存的,就怎么读出来。把 "weather.dat" 里的原始字节直接按块搬回内存。
#include <stdio.h>
int main() {
struct WeatherData read_data[3]; // 准备一个空数组接数据
FILE *pf = fopen("weather.dat", "rb"); // rb: 只读二进制
if (pf == NULL) return 1;
// 读出的数据直接填充到 read_data 中
size_t read_count = fread(read_data, sizeof(struct WeatherData), 3, pf);
printf("成功读取了 %zu 条数据。第一天风速:%.2f\\n", read_count, read_data[0].wind_speed);
fclose(pf);
pf = NULL;
return 0;
}
踩坑预警: fread 和 fwrite 的返回值是成功读写的元素个数,而不是字节数!如果文件里只剩下 2 条数据,你非要读 3 条,fread 会返回 2。
3. 内存刷新:fflush 的救场
有时候你会遇到一个诡异的现象:代码明明执行了 fprintf 或 fwrite,程序也没报错,但打开文件一看,大小是 0 KB!
这是因为 C 语言为了效率,数据不会立刻写进硬盘,而是先暂存在内存缓冲区里。只有等缓冲区满了,或者调用 fclose 正常关闭文件时,数据才会被真正“刷”到硬盘上。
如果你程序中途崩溃了(比如死循环或者野指针报错),缓冲区里的数据就会丢失。
想立刻保存怎么办?用 fflush 强制刷新:
fprintf(pf, "这是一条重要日志\\n");
fflush(pf); // 不管缓冲区满没满,立刻写进硬盘!
4. 文件指针的“凌波微步”
默认情况下,文件都是从头开始读写的,读完一个字节,内部指针自动往后挪一个字节。但如果我们想直接读取第 100 个字节,或者从末尾往前读,就需要手动操作文件指针。
4.1 定位函数:fseek
用来把内部读写指针移动到指定位置。
// fseek(文件指针, 偏移量, 起始位置);
fseek(pf, 100, SEEK_SET); // 从文件开头往后走 100 个字节
三个核心基准点:
- SEEK_SET:文件开头
- SEEK_CUR:当前指针位置
- SEEK_END:文件末尾(配合负数偏移量可以往前走)
#mermaid-svg-4u4F1hXhnGqgUzid{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4u4F1hXhnGqgUzid .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4u4F1hXhnGqgUzid .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4u4F1hXhnGqgUzid .error-icon{fill:#552222;}#mermaid-svg-4u4F1hXhnGqgUzid .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4u4F1hXhnGqgUzid .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4u4F1hXhnGqgUzid .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4u4F1hXhnGqgUzid .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4u4F1hXhnGqgUzid .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4u4F1hXhnGqgUzid .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4u4F1hXhnGqgUzid .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4u4F1hXhnGqgUzid .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4u4F1hXhnGqgUzid .marker.cross{stroke:#333333;}#mermaid-svg-4u4F1hXhnGqgUzid svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4u4F1hXhnGqgUzid p{margin:0;}#mermaid-svg-4u4F1hXhnGqgUzid .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-4u4F1hXhnGqgUzid .cluster-label text{fill:#333;}#mermaid-svg-4u4F1hXhnGqgUzid .cluster-label span{color:#333;}#mermaid-svg-4u4F1hXhnGqgUzid .cluster-label span p{background-color:transparent;}#mermaid-svg-4u4F1hXhnGqgUzid .label text,#mermaid-svg-4u4F1hXhnGqgUzid span{fill:#333;color:#333;}#mermaid-svg-4u4F1hXhnGqgUzid .node rect,#mermaid-svg-4u4F1hXhnGqgUzid .node circle,#mermaid-svg-4u4F1hXhnGqgUzid .node ellipse,#mermaid-svg-4u4F1hXhnGqgUzid .node polygon,#mermaid-svg-4u4F1hXhnGqgUzid .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4u4F1hXhnGqgUzid .rough-node .label text,#mermaid-svg-4u4F1hXhnGqgUzid .node .label text,#mermaid-svg-4u4F1hXhnGqgUzid .image-shape .label,#mermaid-svg-4u4F1hXhnGqgUzid .icon-shape .label{text-anchor:middle;}#mermaid-svg-4u4F1hXhnGqgUzid .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4u4F1hXhnGqgUzid .rough-node .label,#mermaid-svg-4u4F1hXhnGqgUzid .node .label,#mermaid-svg-4u4F1hXhnGqgUzid .image-shape .label,#mermaid-svg-4u4F1hXhnGqgUzid .icon-shape .label{text-align:center;}#mermaid-svg-4u4F1hXhnGqgUzid .node.clickable{cursor:pointer;}#mermaid-svg-4u4F1hXhnGqgUzid .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4u4F1hXhnGqgUzid .arrowheadPath{fill:#333333;}#mermaid-svg-4u4F1hXhnGqgUzid .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4u4F1hXhnGqgUzid .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4u4F1hXhnGqgUzid .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4u4F1hXhnGqgUzid .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4u4F1hXhnGqgUzid .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4u4F1hXhnGqgUzid .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4u4F1hXhnGqgUzid .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4u4F1hXhnGqgUzid .cluster text{fill:#333;}#mermaid-svg-4u4F1hXhnGqgUzid .cluster span{color:#333;}#mermaid-svg-4u4F1hXhnGqgUzid div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4u4F1hXhnGqgUzid .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4u4F1hXhnGqgUzid rect.text{fill:none;stroke-width:0;}#mermaid-svg-4u4F1hXhnGqgUzid .icon-shape,#mermaid-svg-4u4F1hXhnGqgUzid .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4u4F1hXhnGqgUzid .icon-shape p,#mermaid-svg-4u4F1hXhnGqgUzid .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4u4F1hXhnGqgUzid .icon-shape .label rect,#mermaid-svg-4u4F1hXhnGqgUzid .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4u4F1hXhnGqgUzid .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4u4F1hXhnGqgUzid .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4u4F1hXhnGqgUzid :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
偏移量 > 0
偏移量 > 0
偏移量 < 0
偏移量 < 0
文件开头SEEK_SET
目标位置
当前位置SEEK_CUR
往后找
往前找
文件末尾SEEK_END
目标位置
4.2 报点函数:ftell
告诉你当前指针距离文件开头有多少个字节。
经典神技:求文件总字节数
fseek(pf, 0, SEEK_END); // 1. 先一脚踹到文件末尾
long size = ftell(pf); // 2. 问问现在距离开头有多远
printf("这个文件的大小是 %ld 字节\\n", size);
4.3 瞬间回城:rewind
就是字面意思,让指针瞬间回到文件开头。等价于 fseek(pf, 0, SEEK_SET);。
5. 灵魂拷问:到底读完没有?
很多人在写 while 循环读文件时,经常会多读出一次乱码。这是因为对 EOF(End Of File)和错误检测理解不深。
读文件失败,其实有两种可能:
为了区分这两种情况,C 语言提供了两个判定函数:
feof 与 ferror
- feof(pf):如果已经读到了文件末尾,返回非零值。
- ferror(pf):如果在读写过程中发生了错误,返回非零值。
正确的使用姿势: (注意:feof 必须在一次读取操作之后调用才有效,不能把它放在 while 条件里提前判断。)
#include <stdio.h>
int main() {
FILE *pf = fopen("data.txt", "r");
if (!pf) return 1;
int ch;
// 每次先读,如果读到 EOF 就退出循环
while ((ch = fgetc(pf)) != EOF) {
putchar(ch);
}
// 退出循环后,再来追责:到底是为什么退出的?
if (ferror(pf)) {
puts("\\n读取过程中发生了错误!");
} else if (feof(pf)) {
puts("\\n文件已经安全读取完毕。");
}
fclose(pf);
return 0;
}
perror:错误翻译官
如果 fopen 或者读取出错了,我们怎么知道具体是什么错(权限不够?找不到文件?) 直接调用 perror("自定义提示"),它会自动帮你打印出系统底层的真实错误原因。
FILE *pf = fopen("not_exist.txt", "r");
if (pf == NULL) {
perror("打开文件报错了");
// 输出示例:打开文件报错了: No such file or directory
}
总结
到这里,C 语言的基础与进阶操作我们就全部盘完了。从内存里的指针乱飞,到硬盘上的二进制读写,现在你已经具备了独立开发一个小型本地管理系统(比如把你的气象观测数据持久化、或者写一个带存档功能的控制台小游戏)的能力。
多敲代码,多造轮子。如果在开发中遇到奇怪的 Segmentation fault,别慌,回头看看那些曾经踩过的坑。祝你写代码永无 BUG!

