必须用fseek/ftell随机访问+预建索引实现高性能键值查找,禁用逐行遍历;记录格式需固定头长并#pragma pack(1)对齐;索引文件存"key:offset:length";删除仅打标记、复用空闲区;所有文件以"rb"/"wb"打开。

用 fseek 和 ftell 定位键值对记录,别直接遍历文件
文件太大时逐行读取 fgets 查键,性能会断崖式下跌。真实场景下,必须靠随机访问——先建索引,再用 fseek 跳转到精确字节偏移。关键不是“能不能读”,而是“能不能跳”。fseek 的第三个参数必须是 SEEK_SET(绝对偏移),不是 SEEK_CUR,否则索引失效。
- 每条记录格式建议固定头长:比如 4 字节键长度 + 4 字节值长度 + 原始键字节 + 原始值字节,这样可计算任意记录起始位置
- 索引文件单独存,每行存
"key:offset:length",用sscanf解析比字符串分割更稳 - 注意 Windows 下文本模式会把
\n转成\r\n,导致偏移错乱——所有文件必须用"rb"/"wb"打开
键冲突时用链地址法,但别在文件里真“连”指针
内存里能用 std::vector<:list>>,文件里不能存裸指针。所谓“链地址”,实际是把同 hash 值的记录连续写入文件,并在索引中记下第一个偏移和总数量。查键时只扫这一小段,而不是全盘扫描。
- hash 函数选
std::hash<:string>{}(key) % bucket_count即可,bucket_count初始设 64,负载因子超 0.75 就重建索引 - 重建时旧文件不能删,要
fopen("db.dat.new", "wb")写新文件,成功后再rename替换,防崩溃丢数据 - 别用
std::string直接序列化——它内部指针不跨进程有效;一律用char buf[256]存键,或手动 memcpy 字符串内容
fread 读结构体前必须确认内存布局对齐
定义 struct Record { int key_len; int val_len; char data[]; } 很自然,但编译器可能在 val_len 后加填充字节。一旦写入文件,读出来就错位。要么关对齐,要么手动 pack。
- 加
#pragma pack(1)在结构体前后,或用__attribute__((packed))(GCC/Clang) - 写入前用
static_assert(sizeof(Record) == 8, "Record size mismatch")卡死编译,比运行时报Segmentation fault强得多 - 读完
key_len和val_len后,立刻用fread(buf, 1, key_len, fp)分两次读键和值,别指望一次fread(&record, sizeof(record), 1, fp)能读对
删除键值对不物理擦除,只打标记并复用空闲区
文件里删数据 = 移动后面所有记录,IO 开销爆炸。正确做法是:在记录头加 1 字节状态位(0=有效,1=已删),查键时跳过;插入时优先填被删位置,用空闲链表管理(链表本身存在内存,不落盘)。
- 空闲链表节点只需存偏移和长度,
struct FreeNode { off_t offset; size_t len; }; - 每次
fwrite前检查空闲链表是否非空,有则复用,无则追加到文件末尾 - 定期 compact(如删了超 30% 记录后)才真正重写文件——这步必须另开线程或异步做,主线程不能卡住
最易忽略的是文件大小和逻辑记录数的分离:stat().st_size 是物理大小,index.size() 才是有效键数。统计、遍历、备份都得按后者走,否则 backup 出来一堆“已删”垃圾。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











