不能直接用 memcpy 序列化结构体,因成员对齐、字节序、指针、虚函数表及非POD类型(如 std::string)会导致跨平台/编译器失败、崩溃或乱码;可靠序列化需显式控制字段顺序、长度与编码,统一网络序、字符串带长度前缀、容器先写 size 再逐元素序列化,含虚函数或继承时须用类型 ID + 工厂反序列化。

为什么不能直接用 memcpy 序列化结构体
因为结构体成员对齐、字节序、指针字段、虚函数表、非POD类型(如 std::string、std::vector)都会让 memcpy 失效。常见错误现象是:本地序列化后写文件,换平台或换编译器就反序列化失败;含 std::string 的结构体一读就崩溃;或者只读出前几个字段,后面全乱码。
真正能跨版本、跨平台、可维护的二进制序列化,必须显式控制每个字段的读写顺序、长度和编码方式。
- POD 类型(如
int32_t、float)需统一字节序(推荐网络序,即大端),用htons/htonl或std::byteswap(C++23)处理 - 字符串必须带长度前缀(如
uint32_t len+char[len]),不能存裸指针 - 容器(如
std::vector<T>)要先写 size,再循环序列化每个元素 - 类内含虚函数或继承关系时,禁止直接序列化 this 指针——得靠类型 ID + 工厂反序列化
如何为一个类定义可序列化的接口
最轻量又可控的方式是手动实现两个成员函数:serialize 和 deserialize,参数传入一个可读写的字节缓冲区抽象(比如 std::vector<uint8_t>& 或自定义 Serializer 类)。
不建议模板特化或宏注入(如 Boost.Serialization 那套),它们隐藏控制流、调试困难、编译依赖重。手动写清楚每一步,反而容易加日志、插断点、做校验。
立即学习“C++免费学习笔记(深入)”;
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
-
serialize函数负责把当前对象所有字段按确定顺序追加到 buffer 末尾(注意:不是覆盖) -
deserialize函数从 buffer 当前偏移开始逐个读取,**必须严格匹配 serialize 的顺序和大小** - 读写过程应返回
bool表示是否成功(比如 buffer 不够长就 return false),不要抛异常——序列化层不该承担业务异常语义 - 示例片段:
struct Person { std::string name; int32_t age; std::vector<double> scores; <pre class='brush:php;toolbar:false;'>bool serialize(std::vector<uint8_t>& buf) const { // 写 name: len + data uint32_t len = static_cast<uint32_t>(name.size()); uint32_t net_len = htonl(len); buf.insert(buf.end(), reinterpret_cast<const uint8_t*>(&net_len), reinterpret_cast<const uint8_t*>(&net_len) + sizeof(net_len)); buf.insert(buf.end(), name.begin(), name.end()); // 写 age (network byte order) int32_t net_age = htonl(age); buf.insert(buf.end(), reinterpret_cast<const uint8_t*>(&net_age), reinterpret_cast<const uint8_t*>(&net_age) + sizeof(net_age)); // 写 scores size & elements uint32_t sz = static_cast<uint32_t>(scores.size()); uint32_t net_sz = htonl(sz); buf.insert(buf.end(), reinterpret_cast<const uint8_t*>(&net_sz), reinterpret_cast<const uint8_t*>(&net_sz) + sizeof(net_sz)); for (double d : scores) { uint64_t bits = std::bit_cast<uint64_t>(d); // C++20, or use memcpy uint64_t net_bits = bswap_64(bits); // host to big-endian buf.insert(buf.end(), reinterpret_cast<const uint8_t*>(&net_bits), reinterpret_cast<const uint8_t*>(&net_bits) + sizeof(net_bits)); } return true; }};
如何安全地反序列化并避免越界/错位
反序列化比序列化危险得多:输入数据不可信,buffer 可能被截断、篡改或故意构造。核心原则是——**所有读操作前必须检查剩余长度,且每个字段读完后更新读位置**。
- 别用全局偏移变量;用引用传入一个
size_t& offset,每次读完自动推进它 - 字符串长度字段本身也要先校验(比如
len > 10*1024*1024就拒绝,防内存爆炸) - 容器 size 字段若过大(如 > 1M),立刻返回 false;否则后续循环可能 OOM 或耗时过长
- 对浮点数、整数做简单合理性校验(如 age 在 0–150 之间),不是必须,但能早发现数据损坏
- 示例关键检查:
bool deserialize(const std::vector<uint8_t>& buf, size_t& offset) { if (offset + sizeof(uint32_t) > buf.size()) return false; uint32_t net_len; memcpy(&net_len, &buf[offset], sizeof(net_len)); offset += sizeof(net_len); uint32_t len = ntohl(net_len); if (len > 1024 * 1024) return false; // too big if (offset + len > buf.size()) return false; name.assign(reinterpret_cast<const char*>(&buf[offset]), len); offset += len; <pre class='brush:php;toolbar:false;'>if (offset + sizeof(int32_t) > buf.size()) return false; int32_t net_age; memcpy(&net_age, &buf[offset], sizeof(net_age)); offset += sizeof(net_age); age = ntohl(net_age); // ... 后续同理}
版本兼容性怎么处理(新增字段、删字段、改类型)
没有银弹。最务实的做法是在序列化头部固定写入一个 uint16_t version,并在反序列化时根据 version 分支处理。不要试图“自动跳过未知字段”——那会让 bug 隐蔽且难以调试。
- 新增字段:version 加 1,在
deserialize中对老 version 填默认值(如new_field = 0或new_field.clear()) - 删除字段:保留读逻辑(但不存入成员),或在某个 version 后彻底移除读代码(前提是确认旧数据已清空)
- 改类型(如
int32_t→int64_t):version 升级,老版读int32_t后零扩展;新版始终写int64_t - 永远不要重用字段编号或 magic number;如果协议要长期演进,建议把 version 扩展为
uint32_t并预留 flag 位
最容易被忽略的是:version 字段本身也必须用网络序写入,并且反序列化时第一个就读它——否则连判断分支的机会都没有。


















