二进制文件数据库必须用 file* + "rb" 模式配合 fseek/fread 精确字节寻址,避免 std::fstream 的 locale 干扰;记录需带长度头实现容错;小索引应 fread 到内存二分查找,禁用可写 mmap;每次 i/o 后须检查 ferror/feof。

用 fseek + fread 直接跳转到偏移量读记录,别碰 std::fstream 的 seekg 默认行为
二进制文件数据库的核心是“知道某条记录在第几个字节”,所以必须用 C 风格的偏移控制——fseek 精确到字节,且不带缓冲干扰。C++ 的 std::ifstream::seekg 在 binary 模式下虽可用,但默认关联的 locale 和宽字符处理可能悄悄改变实际偏移(尤其 Windows 下换行符或 BOM 处理)。
实操建议:
- 打开文件必须用
FILE*+"rb"模式,避免std::ios::binary与 locale 交互带来的不确定性 - 每次
fseek(fp, offset, SEEK_SET)后,检查返回值是否为0,非零说明偏移越界或文件不可读 - 读取前务必调用
clearerr(fp),否则上次 EOF 或 error 状态会卡住后续fread - 结构体写入文件前,确保用
#pragma pack(1)或alignas(1)消除 padding;否则内存布局 ≠ 文件布局
记录头设计必须含长度字段,否则无法安全跳过损坏/错位的数据块
只靠固定长度记录(比如每条 128 字节)看似简单,但一旦某条记录写半截崩溃、或磁盘位翻转,整个后续偏移全错——你没法判断“当前位置是不是一条合法记录的开头”。加一个紧凑的记录头(如 4 字节 uint32_t length)是成本最低的容错手段。
实操建议:
- 记录头放在每条记录最前面,紧跟着是 payload;读取时先
fread(&len, sizeof(len), 1, fp),再校验len 且 <code>len > 0 - 不要用 magic number 做校验(如 0xdeadbeef),它会被误当成合法长度;优先用长度自检:读完
len字节后,下一个字节应是下一条记录的长度字段 - 写入时用
fwrite分两次:先写头,再写 body;中间不要fflush,但写完整条后fflush(fp)可降低断电丢数据风险
索引文件不能直接 mmap,mmap 在小文件+频繁随机读场景下反而拖慢速度
有人想把索引(比如“key → offset”映射)用 mmap 加载进内存图,但实际中:如果索引只有几 MB,mmap 的页表开销和缺页中断比一次 lseek+read 更重;且 mmap 在文件被其他进程截断时会触发 SIGBUS,而普通 fread 只返回短读。
实操建议:
- 索引小于 50MB 时,直接
fread到std::vector<uint64_t></uint64_t>内存里,用std::lower_bound二分查找,比mmap+ 自己遍历快得多 - 若索引超大(>200MB),改用 memory-mapped *read-only* 视图,并设
MADV_RANDOM提示内核不要预读 - 永远不用
mmap映射可写的索引文件——并发写入需加文件锁,mmap写冲突无原子性保障
查询失败时别假设是“key 不存在”,先检查 ferror 和 feof
返回空结果,90% 情况不是逻辑问题,而是底层 I/O 出了状况:fseek 越界、磁盘满、权限变、NFS 挂掉……但多数人只查 key 是否在索引里,忽略系统错误。
实操建议:
- 每次
fread后立即检查:if (ferror(fp)) { /* 处理 errno */ };if (feof(fp)) { /* 偏移指向文件末尾之后 */ } - 不要用
errno判断fseek失败——POSIX 不保证它被设置;唯一可靠方式是看返回值是否为 -1 - 在调试阶段,对每个查询打日志:
offset=0x1a2b3c, read_len=128, actual_read=0, feof=1, ferror=0,比猜快十倍
偏移量引擎的脆弱点不在算法,而在对文件系统边界条件的漠视:越界偏移、部分写入、缓存不一致、字节序混用……这些不会报编译错误,只会让查询偶尔返回错数据,或者某天凌晨三点静默失败。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











