1. 引言
序列化 (Serialization)是指将程序中的内存对象(如结构体、类实例、列表等)转换成一种可以存储或传输的格式(通常是字节流或文本)的过程。常见的序列化格式包括:
- JSON(文本,人类可读)
- XML
- Protocol Buffers(二进制,高效)
- MessagePack
- YAML
- 甚至自定义的二进制格式
反序列化 (Deserialization)是序列化的逆过程:将序列化后的数据 (如 JSON 字符串)重新转换回程序中的内存对象。
2. 原因
那么,为什么需要序列化和反序列化呢?笔者的体会是不序列化/反序列化也行,只不过在进行相应的读取和写入操作的时候会非常繁琐。比如一个配置文件更新:
假设我们有一个博客系统的配置,包含站点标题、作者名、是否启用评论、默认封面地址等多个字段。如果不使用序列化机制,每次读取配置时,就得手动打开 JSON(或 YAML、INI)文件,逐行解析,判断每个字段是否存在、类型是否正确,再一一赋值给程序中的变量;而当用户修改了某个设置、需要保存回文件时,又得手动拼接字符串、处理引号转义、数组格式、缩进对齐......稍有不慎,就会生成格式错误的文件,导致程序下次启动失败。
更麻烦的是,一旦配置结构发生变化------比如新增一个 theme 字段,或者把 enableComments 拆成 enablePostComments 和 enablePageComments------我们就不得不同时修改读取逻辑、写入逻辑、默认值初始化、错误处理等多处代码,极易遗漏或出错。
而有了序列化和反序列化,这一切就变得简洁而可靠:定义好结构体(或类),注册对应的转换函数,一行代码就能从 JSON 构造出完整的配置对象,另一行代码就能把修改后的对象写回文件。字段增减只需调整结构体,读写逻辑几乎无需改动,既减少了重复劳动,也大大提升了健壮性和可维护性。
因此,序列化/反序列化并非"必须",但它是对抗复杂性、提升开发效率和系统可靠性的重要工具。
3. 实现
3.1 示例
这里就举例实现一下 C++ 中 JSON 序列化和反序列化。如果要反序列化成 C++ 可以使用的内存对象,最好的数据容器就是 struct 。当然 class 也可以,不过我们都知道 struct 和 class 的区别:struct 的成员一般默认是公有的,而 class 的成员一般默认是私有的。因此,struct 适合以数据成员为主的,比较简单的对象;class 适合以方法成员为主的,比较复杂的对象。
笔者使用 nlohmann/json 中序列化/反序列化一个表达博客的数据对象:
BlogMeta.h:
cpp
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
#include "PostStats.h"
namespace DataTransfer {
/// @brief 一篇博客的元数据
struct BlogMeta {
std::string id;
std::string title;
std::string summary;
std::string createdTime;
std::string updatedTime;
std::string coverAddress;
int64_t isDraft;
std::vector<std::string> tagNames;
std::vector<std::string> categoryNames;
PostStats postStats;
// 提供序列化接口
friend void to_json(nlohmann::json& j, const BlogMeta& s);
// 提供反序列化接口
friend void from_json(const nlohmann::json& j, BlogMeta& s);
};
BlogMeta.cpp:
cpp
#include "BlogMeta.h"
#include "Util/GetTime.h"
namespace DataTransfer {
// 提供序列化接口
void to_json(nlohmann::json& j, const BlogMeta& s) {
j = nlohmann::json{{"id", s.id},
{"title", s.title},
{"summary", s.summary},
{"createdTime", s.createdTime},
{"updatedTime", s.updatedTime},
{"coverAddress", s.coverAddress},
{"isDraft", s.isDraft},
{"tagNames", s.tagNames},
{"categoryNames", s.categoryNames},
{"postStats", s.postStats}};
}
// 提供反序列化接口
void from_json(const nlohmann::json& j, BlogMeta& s) {
s.id = j.at("id").get<std::string>();
s.title = j.at("title").get<std::string>();
s.summary = j.at("summary").get<std::string>();
s.createdTime = j.at("createdTime").get<std::string>();
s.updatedTime =
j.value("updatedTime", Util::GetCurrentTime()); //提供默认值,增加稳定性
s.coverAddress = j.at("coverAddress").get<std::string>();
s.isDraft = j.at("isDraft").get<int64_t>();
s.tagNames = j.at("tagNames").get<std::vector<std::string>>();
s.categoryNames = j.at("categoryNames").get<std::vector<std::string>>();
s.postStats = j.at("postStats").get<PostStats>();
}
} // namespace DataTransfer
其中 to_json / from_json 就是对应的 序列化 / 反序列化接口。只需要填充这一对接口,就可以通过赋值运算符=实现自动序列化和反序列化:
cpp
#include <iostream>
#include <nlohmann/json.hpp>
#include "BlogMeta.h" // 包含你的 BlogMeta 定义
int main() {
using namespace DataTransfer;
using json = nlohmann::json;
// ----------------------------
// 1. 创建一个 BlogMeta 对象(内存中的数据)
// ----------------------------
BlogMeta blog;
blog.id = "blog-001";
blog.title = "深入理解 C++ 序列化";
blog.summary = "本文介绍如何使用 nlohmann/json 进行安全高效的序列化。";
blog.createdTime = "2025-12-22T10:00:00Z";
blog.updatedTime = "2025-12-22T17:00:00Z"; // 可省略,反序列化时会用默认值
blog.coverAddress = "/images/cpp_serialization.jpg";
blog.isDraft = 0;
blog.tagNames = {"C++", "JSON", "Serialization"};
blog.categoryNames = {"Programming", "Tutorial"};
blog.postStats = {1200, 85, 23};
// ----------------------------
// 2. 序列化:BlogMeta → JSON
// ----------------------------
json j = blog; // 自动调用 to_json
std::cout << "【序列化结果】\n" << j.dump(2) << "\n\n";
// ----------------------------
// 3. 反序列化:JSON → BlogMeta
// ----------------------------
// 模拟从文件或网络接收到的 JSON(故意省略 updatedTime)
std::string jsonStr = R"({
"id": "blog-002",
"title": "反序列化的健壮性测试",
"summary": "测试缺失 updatedTime 字段时的行为",
"createdTime": "2025-12-20T08:30:00Z",
"coverAddress": "/images/robust.png",
"isDraft": 1,
"tagNames": ["C++", "Robustness"],
"categoryNames": ["Engineering"],
"postStats": {
"viewCount": 450,
"likeCount": 30,
"commentCount": 5
}
})";
BlogMeta restoredBlog = json::parse(jsonStr); // 自动调用 from_json
// ----------------------------
// 4. 验证反序列化结果
// ----------------------------
std::cout << "【反序列化结果】\n";
std::cout << "ID: " << restoredBlog.id << "\n";
std::cout << "Title: " << restoredBlog.title << "\n";
std::cout << "UpdatedTime (应为当前默认值): " << restoredBlog.updatedTime << "\n";
std::cout << "Is Draft: " << restoredBlog.isDraft << "\n";
std::cout << "Tags: ";
for (const auto& tag : restoredBlog.tagNames) {
std::cout << tag << " ";
}
std::cout << "\n";
return 0;
}
3.2 能力
序列化 / 反序列化的能力取决于所使用的库实现的程度。例如这个例子中,std 容器 std::vector<std::string> 也可以直接使用赋值运算符=来序列化/反序列化吗?确实是可以的,nlohmann/json 支持所有基础类型以及绝大多数 std 容器类型的直接 序列化/ 反序列化。不过默认情况下,C++ 数据类型与JSON的对应关系是:std::string 对应字符串, int64_t, uint64_t 或 double 对应数字, std::map 对应对象, std::vector 对应数组,且 bool 对应布尔值。
另外,nlohmann/json 还支持嵌套序列化。比如这里的 PostStats 其实也是个自定义的 struct ,也实现了 to_json / from_json 接口,因此可以直接通过赋值运算符=相互转换。
PostStats.h:
cpp
#pragma once
#include <nlohmann/json.hpp>
namespace DataTransfer {
/// @brief 一篇博客的统计信息
struct PostStats {
int64_t viewCount = 0; // 阅读量
int64_t likeCount = 0; // 点赞量
int64_t commentCount = 0; // 评论量
// 提供序列化接口
friend void to_json(nlohmann::json& j, const PostStats& s);
// 提供反序列化接口
friend void from_json(const nlohmann::json& j, PostStats& s);
};
} // namespace DataTransfer
PostStats.cpp:
cpp
#include "PostStats.h"
namespace DataTransfer {
// 提供序列化接口
void to_json(nlohmann::json& j, const PostStats& s) {
j = nlohmann::json{{"viewCount", s.viewCount},
{"likeCount", s.likeCount},
{"commentCount", s.commentCount}};
}
// 提供反序列化接口
void from_json(const nlohmann::json& j, PostStats& s) {
s.viewCount = j.at("viewCount").get<int64_t>();
s.likeCount = j.at("likeCount").get<int64_t>();
s.commentCount = j.at("commentCount").get<int64_t>();
}
} // namespace DataTransfer
3.3 兼容
如果 JSON 配置文件进行了升级,比如增加了一个新的配置字段,那么新的程序读取旧的配置文件就有可能出错,所以在反序列化的时候最好使用 value() 提供默认值:
cpp
void from_json(const nlohmann::json& j, BlogMeta& s) {
s.id = j.value("id", std::string{});
s.title = j.value("title", std::string{});
s.summary = j.value("summary", std::string{});
s.createdTime = j.value("createdTime", std::string{});
s.updatedTime = j.value("updatedTime", std::string{}); // 若缺失则为空字符串
s.coverAddress = j.value("coverAddress", std::string{});
s.isDraft = j.value("isDraft", int64_t{0});
s.tagNames = j.value("tagNames", std::vector<std::string>{});
s.categoryNames = j.value("categoryNames", std::vector<std::string>{});
// 对于嵌套结构如 PostStats,如果它也支持 from_json,可这样处理:
if (j.contains("postStats") && j["postStats"].is_object()) {
s.postStats = j["postStats"].get<PostStats>();
} else {
s.postStats = PostStats{}; // 默认构造
}
}
另外,如果业务逻辑允许,能分清楚哪些是必须字段,哪些是可选字段,那么可以使用std::optional:
cpp
/// @brief 一篇博客的元数据(健壮版,支持可选字段)
struct BlogMeta {
std::string id; // 必填
std::string title; // 必填
std::optional<std::string> summary;
std::string createdTime; // 通常必填,也可改为 optional
std::optional<std::string> updatedTime;
std::optional<std::string> coverAddress;
std::optional<int64_t> isDraft; // 可选,默认 false 或 0
std::optional<std::vector<std::string>> tagNames;
std::optional<std::vector<std::string>> categoryNames;
std::optional<PostStats> postStats;
// 序列化/反序列化友元声明
friend void to_json(nlohmann::json& j, const BlogMeta& s);
friend void from_json(const nlohmann::json& j, BlogMeta& s);
};
void to_json(nlohmann::json& j, const BlogMeta& s) {
j = nlohmann::json{
{"id", s.id},
{"title", s.title},
{"summary", s.summary},
{"createdTime", s.createdTime},
{"updatedTime", s.updatedTime},
{"coverAddress", s.coverAddress},
{"isDraft", s.isDraft},
{"tagNames", s.tagNames},
{"categoryNames", s.categoryNames},
{"postStats", s.postStats}
};
}
void from_json(const nlohmann::json& j, BlogMeta& s) {
// 必填字段:使用 at(),缺失时报错
s.id = j.at("id").get<std::string>();
s.title = j.at("title").get<std::string>();
s.createdTime = j.at("createdTime").get<std::string>(); // 若你也想让它可选,改用 value 或 optional
// 可选字段:直接赋值,nlohmann 自动处理存在性
if (j.contains("summary")) s.summary = j["summary"].get<std::string>();
if (j.contains("updatedTime")) s.updatedTime = j["updatedTime"].get<std::string>();
if (j.contains("coverAddress")) s.coverAddress = j["coverAddress"].get<std::string>();
if (j.contains("isDraft")) s.isDraft = j["isDraft"].get<int64_t>();
if (j.contains("tagNames")) s.tagNames = j["tagNames"].get<std::vector<std::string>>();
if (j.contains("categoryNames")) s.categoryNames = j["categoryNames"].get<std::vector<std::string>>();
if (j.contains("postStats")) s.postStats = j["postStats"].get<PostStats>();
}