必须用两阶段引用ID映射表解决循环引用:序列化时分配稳定ID并记录首次出现对象,反序列化时先构造后填指针;需手动实现serialize/deserialize、预分配内存、placement new、显式析构、版本控制及基础类型宽度锁定。

序列化前必须解决循环引用,否则 std::shared_ptr 会无限递归
二进制序列化跨对象引用结构时,最常卡在对象 A 持有 std::shared_ptr<b></b>、B 又持有 std::shared_ptr<a></a> 这类循环。直接遍历指针会栈溢出或死循环,不是数据损坏就是崩溃。
真正可行的做法是引入“引用 ID 映射表”:序列化时给每个首次遇到的对象分配唯一整数 ID(比如用 std::map<const void uint32_t></const>),遇到已见过的地址就只写 ID;反序列化时先构造所有对象(不设指针),再第二遍遍历填入指针。这个两阶段过程不能跳过。
- 别用
std::addressof(obj)当 ID —— 移动语义下地址可能变,必须用稳定标识(如自增 ID + 地址哈希双重校验) -
std::weak_ptr不参与 ID 分配,只记录其指向的shared_ptr的 ID,避免强引用干扰生命周期判断 - 若对象含虚函数,
typeid(obj).hash_code()不能用于跨进程复原,仅限单次序列化/反序列化对内使用
手写 serialize() 和 deserialize() 比依赖 Boost.Serialization 更可控
Boost.Serialization 默认走 RTTI + 运行时类型注册,一旦对象继承链深、模板嵌套多(比如 std::vector<:shared_ptr std::string>>></:shared_ptr>),序列化输出体积大、加载慢,且无法跳过某些字段(如缓存、临时句柄)。
自己实现能精准控制字节布局:每个类显式定义 serialize(Writer& w) 写字段顺序,deserialize(Reader& r) 按相同顺序读。关键点在于字段偏移和长度必须固定,不能依赖编译器填充。
立即学习“C++免费学习笔记(深入)”;
- 用
#pragma pack(1)或[[gnu::packed]]防止结构体对齐导致读错位 - 字符串统一存为
uint32_t length+char data[],不存std::string::c_str()地址 - 容器大小必须前置写入(如
w.write_uint32(vec.size())),不能靠结束符——二进制里没有“自然结束”概念
反序列化时 new 出的对象必须用 placement new 放进预分配内存块
如果每反序列化一个对象就调用一次 new,碎片化严重,且无法保证对象间相对位置——而跨对象指针恢复依赖地址差值(比如父对象末尾到子对象开头的距离)。直接 new 出来的地址不可预测,memcpy 过去的指针值全失效。
正确做法是:先算出整个快照所有对象总字节数(序列化时就统计好),malloc 一块连续内存;反序列化时用 placement new 把每个对象构造在这块内存里,保持原始布局。
- 别用
std::vector<char></char>承载原始内存——它的data()可能在 resize 时移动,指针立刻悬空 - 对象析构必须显式调用
obj.~T(),不能依赖 delete —— 内存不是 new 出来的 - 若对象含
std::mutex或std::thread,禁止序列化,这类资源无法跨进程/跨时间点重建
版本兼容靠手动字段标记,别信自动 schema 推导
下次加个字段、删个字段、改个字段类型,旧快照一读就崩。所谓“向后兼容”必须靠显式版本号 + 字段存在性检查,而不是指望序列化库自动跳过未知字段。
每个结构体头部固定写入 uint16_t version,反序列化时 switch 分支处理不同版本逻辑。新增字段默认初始化,删除字段直接跳过读取——但跳过的字节数必须精确,否则后续字段全错位。
- 字段名变更不算兼容,
user_id改成uid就得在 v2 分支里做映射赋值 - 基础类型宽度要锁死:
int改成int32_t,size_t改成uint64_t,否则 32/64 位平台读写出错 - 浮点数用
memcpy(&i32, &f, sizeof(f))存 IEEE754 整形位表示,不存文本或平台相关格式
最难的不是写序列化,是让所有对象的构造函数不抛异常、不访问外部状态、不依赖全局单例——因为反序列化发生在任意线程、任意时机,连日志系统都可能还没起来。



















