本章导读
file 模块三章里的第一章,讲句柄主线:xrtOpen(标志组合式权限)→ WriteFull/ReadFull(读满写满语义)→ xrtSeek/Tell(三基准定位)→ xrtClose。Full 后缀族把短读写的重试样板收进库(与第 41 章 IO 抽象同语义——本章是它在文件句柄上的直连形态);ReadAtFull 绝对偏移读不扰动游标——预读文件头的专用姿势。中章(第 46 章)讲映射/锁/原子写,下章(第 47 章)讲目录与遍历——三章合成文件操作的完整地图。
45.1引入
C 标准库的文件 API 是历史沉积层:fopen 的模式字符串("rb+" 与 "w+b" 谁记得清)、read/write 的短读写语义(返回值要自己循环核对)、跨平台的行为差异(文本模式换行转换的坑)。每个 C 程序员都写过 fread 的 while 循环与 fflush 的时机猜测——这不是你的错,是接口年龄的错;本章的任务就是把这些历史包袱一次性放下。
XRT 的文件层把这五十年的坑收编:权限用标志组合(读/写/创建/截断/追加五个标志按位或——一眼可读,永远不用再查 "rb+" 与 "w+b" 谁是谁)、Full 语义内建(读满写满或失败——循环样板从此消失)、平台差异抹平(二进制语义统一、换行不转换、错误走第 4 章结构化报告而非 errno 猜谜)。xrtOpen 返回 xfile 句柄——与第 41 章 IO 抽象衔接(适配器一行把它变成 xreader/xwriter)。
45.2概念
句柄生命周期(先背这条主线)
标志组合的例子:XFILE_READ | XFILE_WRITE(读写打开不创建)、XFILE_WRITE | XFILE_CREATE | XFILE_TRUNCATE(重建式写入——临时文件标准姿势)。创建与截断显式分离——"打开已存在文件却把它清空了"这类事故在标志组合下不可能无意发生。
Full 语义的文件版(样板的终结)
第 41 章的 Full 语义在文件上的两个具体形态:WriteFull 磁盘满——写到一半空间不足,整体失败、错误槽留下结构化详情(第 4 章错误对象携带诊断信息);ReadFull 提前 EOF——要读 100 字节只到 80 就 EOF,失败("文件被截断了"是真实故障信号,不是"读到 80 也行")。
ReadAtFull:不扰动游标的旁路读(预读专用)
流式读取大文件时想偷看文件头(magic number 校验)——普通 Read 会移动游标、读完再 Seek 回来的写法既绕又易错。ReadAtFull(句柄, 绝对偏移, 缓冲, 长度) 在不移动游标的前提下完成旁路读(范例用 Tell 验证游标仍在原位 3)——预读校验、索引查询的标准姿势。
与 IO 抽象的衔接(直连还是适配)
xrtReaderFromFile 把 xfile 适配成第 41 章的统一 Reader(写侧同族)——文件参与"一段序列化代码三栖"(内存/文件/网络)的通道。直连(本章 API)与适配(IO 接口)的选择:单文件一次性操作直连更短;同一逻辑要对接多种目的地(内存/文件/网络)时走适配——选择本身与第 41 章"三宿主"的判断一脉相承。
45.3示例
完整程序:句柄主线走通
来自仓库范例 examples/file/basic/main.c——创建、写入、定位、旁路读、关闭、删除:
#include <stdio.h> #include <xrt.h> */ /* 展示二进制文件最常用的一次创建、完整写入和完整读取。 */ int main(void) { static const char sText[] = "xrt file"; char arrText[sizeof(sText)]; uint64 iPosition; xfile File = xrtOpen("xrt-file-example.tmp", XFILE_READ | XFILE_WRITE | XFILE_CREATE | XFILE_TRUNCATE); if ( File == NULL ) { return 1; } if ( !xrtWriteFull(File, sText, sizeof(sText), NULL) || !xrtSeek(File, 3, XSEEK_START, NULL) || !xrtReadAtFull(File, 0, arrText, sizeof(arrText), NULL) || !xrtTell(File, &iPosition) || (iPosition != 3u) || !xrtSeek(File, 0, XSEEK_START, NULL) || !xrtReadFull(File, arrText, sizeof(arrText), NULL) ) { (void)xrtClose(File); return 1; } printf("%s\n", arrText); if ( !xrtClose(File) ) { return 1; } return xrtFileDelete("xrt-file-example.tmp") ? 0 : 1; }
刚才发生了什么。 ① xrtOpen(路径, 写|创建|截断)——标志组合声明"重建式写入",无模式字符串。② WriteFull 整段写入(8 字节恰好写满——短写即失败的内建保障)。③ Seek 回头部后 ReadFull 读回——写读闭环。④ ReadAtFull 旁路读验证:游标 Tell 仍在原位 3——预读不扰动流式位置,这个数字就是证明。⑤ xrtClose 检查返回值——缓冲落盘失败(磁盘满的最后一批)在这里而非崩溃时暴露。⑥ xrtFileDelete 按路径删除临时文件——句柄已关、路径删除是独立操作,两步清理收尾。
完整程序:IO 适配三栖
来自 examples/io/file/main.c——同一序列化逻辑对接内存与文件:
*/ #include <stdio.h> #include <xrt.h> int main(void) { static const char sPath[] = "xrt-io-example.bin"; unsigned char arrData[7]; xwriter* pWriter = xrtWriterOpen(sPath); /* 拥有式:Destroy 关文件 */ xreader* pReader; int iResult = 1; /* 写 7 字节 "xrt io\n" 后立即关闭(Destroy 含落盘检查)。 */ if ( (pWriter == NULL) || !xrtWriterWriteFull(pWriter, "xrt io\n", sizeof(arrData), NULL) || !xrtWriterDestroy(pWriter) ) { return 1; } /* 重新打开读取:ReadFull 保证读满 7 字节,否则失败。 */ pReader = xrtReaderOpen(sPath); if ( (pReader != NULL) && xrtReaderReadFull(pReader, arrData, sizeof(arrData), NULL) ) { fwrite(arrData, 1u, sizeof(arrData), stdout); iResult = 0; } xrtReaderDestroy(pReader); /* 范例产物随用随删,不污染工作目录。 */ (void)xrtFileDelete(sPath); return iResult; }
刚才发生了什么。 ① 文件适配器把 xfile 包装成 xwriter——序列化代码(第 41 章写的那段)零改动直落文件。② 与内存适配器跑同一逻辑:产物逐字节一致——"三栖"承诺的文件侧验证。③ 适配器归还时分层清晰:适配器 Destroy 与文件 Close 各归各——与第 41 章"借用适配"纪律一致。
45.4契约
- 标志组合:读/写/创建/截断/追加五个标志按位或;创建与截断显式分离,杜绝无意清空。
- Full 语义:恰好 N 字节才成功;磁盘满与提前 EOF 都整体失败并留结构化错误详情。
- 游标三基准:XSEEK_START/CURRENT/END(第 3 章 xseek 枚举);Tell 报告当前字节位置。
- 旁路读:
ReadAtFull绝对偏移不动游标——预读校验与索引查询的专用通道。 - 关闭核对:Close 返回值必查——缓冲落盘问题在关闭时定案。
- 删除显式:
xrtFileDelete独立于句柄——按路径删除已关闭的文件,与句柄生命周期无关。
从示例到工程:文件句柄的三个宿主
配置与小文件:整读整写一气呵成——Open/ReadAll 或 Open/WriteFull/Close 三步,第 32 章 JSON 文件解析就是这么用它的。流式处理:大文件分块循环(ReadFull 4KB 一块)——分块拷贝练习的完全体;配合第 41 章行读取器做逐行扫描。随机访问:ReadAtFull 按索引跳跃读(索引文件、日志按偏移定位)——游标不动的旁路读在这里规模化。三宿主对应文件访问的三种模式(整存整取/顺序流/随机跳)——先想访问模式再选 API,与容器选型的思路(第 23 章三问定型)同构。
错误分类:文件失败的三种身份(恢复策略的分诊台)
文件操作的失败有三种身份,处置各不相同——分诊错了,重试与报告都会跟着错。权限类(不存在/无权限/被锁):向用户报告并要求修正——重试无意义;空间类(磁盘满/配额超):向运维报告——清理或扩容后可重试;一致性类(预读到截断的文件/校验失败):向开发者报告——这通常是并发修改或数据损坏的信号。第 4 章错误类别在文件场景的映射:XERR_NOT_FOUND/PERMISSION → 权限类、XERR_IO 的系统码 → 空间类、业务校验 → 一致性类。分类的意义在恢复策略:盲目重试对权限类是浪费、对一致性类是掩盖。
一个习惯:临时文件的三步收尾
写文件的三步收尾值得成为习惯:写完 Flush 语义核对 Close(落盘定案)→ 重命名到最终名(原子可见——读者要么看到旧文件要么看到新文件,第 46 章原子写的简化版)→ 失败路径删除临时文件(不留垃圾)。直接写最终名的问题:写一半崩溃留下截断文件,读者把它当完整数据。三步收尾的产物只有两种状态(不存在/完整)——"部分写入"状态被结构消灭。这个习惯在配置保存、日志切换、缓存更新场景全部适用——第 46 章会给出它的完整工业版(临时文件 + 原子重命名 + fsync 语义)。
常见错误
症状:程序"成功"退出但文件尾部数据丢失——最后一次缓冲落盘失败(磁盘满、配额超)被静默吞掉。
原因:Close 是数据定案点——多数实现的缓冲冲刷在关闭时发生,不核对等于签收了没看内容的包裹。
xrtClose(File); /* 返回值丢弃——落盘失败无感知 */ return 0;
if ( !xrtClose(File) ) { ReportIoError(Path); /* 第 4 章错误槽有详情:磁盘满/配额 */ return 1; } return 0;
症状:流式读取大文件时"偶尔从错误位置继续"——Seek 回退的偏移算错(或被并发分支的读打乱)。
原因:预读头部的 Seek-Read-Seek 回退写法把游标状态暴露给后续逻辑——任何一步算错偏移全盘错位。
xrtSeek(File, 0, XSEEK_START); /* 跳到头 */ xrtReadFull(File, Magic, 4); /* 读 magic */ xrtSeek(File, SavedPos, XSEEK_START); /* 跳回来——SavedPos 从哪来?算错即灾难 */
if ( !xrtReadAtFull(File, 0, Magic, 4) ) { /* 绝对偏移旁路读 */ return false; } CheckMagic(Magic); /* 游标从未移动——流式位置天然安全 */
编程练习
用五种标志组合(只读/读写/重建/追加/创建不截断)各做一次打开-操作-关闭,记录每组合对"文件存在/不存在"两种情形的行为——十格矩阵先笔填再实测。
实现 copy(源, 目标):分块 4KB 循环 ReadFull/WriteFull(尾块用实际读到量——普通 Read 收尾);源与目标各算 Hash64(第 12 章)指纹比对。提示:最后一块不足 4KB 是唯一允许短读的位置。
实现 sniff(路径):ReadAtFull 旁路读前 16 字节,识别 gzip(1F 8B)/PNG/ZIP 三种魔数并输出判定;随后不影响地流式读全文做行数统计(游标未受预读扰动的实证)。验收标准:三种格式各构造一个测试文件判定正确;行数与无嗅探的直读版一致。
小结
| 知识点 | 速查 |
|---|---|
| 打开 | 标志按位组合(读/写/创建/截断/追加五标志);无模式字符串密码本 |
| Full 族 | 读满写满或失败;磁盘满与提前 EOF 整体报错 |
| 定位 | Seek 三基准(START/CURRENT/END,第 3 章 xseek)+ Tell 查询 |
| 旁路读 | ReadAtFull 绝对偏移不动游标(Tell 可证)——预读/索引专用 |
| 关闭 | Close 必查返回值——落盘定案点,失败在此暴露而非上线崩溃时 |
| 三栖 | Reader/WriterFromFile 适配进第 41 章统一接口 |
| 三宿主 | 整存整取(配置小文件)/ 顺序流(大文件分块)/ 随机跳(索引偏移)——访问模式先于 API |
| 错误三分 | 权限类报告用户 / 空间类报告运维 / 一致性类报告开发者——分诊先于重试 |
| 三步收尾 | Close 定案 → 重命名原子可见 → 失败删临时文件——部分写入状态被结构消灭 |
📖 本章涉及的 API 参考手册:file(132 个条目)io(62 个条目)
点击查看完整签名、参数约束、返回值与错误说明