前言
把内存中的std::map、std::vector之类容器序列化成 JSON,是服务端和配置读写里最常见的操作之一。rapidjson 是一个只有头文件的 C++ JSON 库,解析和生成都快、依赖少,但它的对象模型和「随手拼字符串」很不一样:所有值都挂在rapidjson::Value上,字符串、对象、数组的成员在添加时都要显式传入分配器(allocator)。
初学者最容易踩的坑是字符串生命周期。rapidjson::StringRef只保存指针、不复制内容,如果传入的是临时std::string的c_str(),表达式结束后指针就悬垂了,序列化时读到的是已被释放的内存。另一个常见问题是忘记doc.SetObject()或SetArray(),直接AddMember得到的是未定义行为。
本文按「对象模型 → map 转对象 → 数组与嵌套 → 序列化输出」的顺序讲,代码以 C++17 为基准。rapidjson 是第三方库,具体接口以其官方文档为准;下文的调用均采用其稳定版本中长期存在的形式。
一、对象模型:Document、Value 与 Allocator
rapidjson::Document本身继承自rapidjson::Value,可以当作根节点用。一个Value有类型:kNullType、kFalseType、kTrueType、kObjectType、kArrayType、kStringType、kNumberType。对象和数组在添加成员前必须先确定类型:
// 适用于 C++17
rapidjson::Document doc;
doc.SetObject(); // 根节点是对象
rapidjson::Document::AllocatorType& alloc = doc.GetAllocator();分配器负责所有内部内存(成员数组、字符串副本)。它的生命周期必须覆盖doc的使用期,所以通常直接从doc取引用,不要自己另建一个。所有会「新增内容」的调用——AddMember、PushBack、用Value构造字符串——都要把alloc传进去。
需要牢记的一点:Value的拷贝是浅拷贝语义上的「移动式」赋值,把子Value加进父节点后,子对象的所有权归父节点,不应再独立使用。这也是 rapidjson 用分配器统一管理内存的原因。
二、把 std::map 组装成 JSON 对象
std::map的键是std::string,值可能是整数、字符串或更复杂的结构。转成 JSON 对象时有两件事要注意:键必须复制,值按类型分派。
// 适用于 C++17
#include <map>
#include <string>
#include "rapidjson/document.h"
void addScores(rapidjson::Document& doc, const std::map<std::string, int>& scores) {
rapidjson::Document::AllocatorType& alloc = doc.GetAllocator();
for (const auto& kv : scores) {
// 正确:Value 的字符串构造函数会按 alloc 复制一份
rapidjson::Value key(kv.first.c_str(), alloc);
doc.AddMember(key, kv.second, alloc);
}
}为什么不能写doc.AddMember(rapidjson::StringRef(kv.first.c_str()), kv.second, alloc)?因为StringRef带有「不复制」的标记,只记住指针。循环进行到下一轮时kv.first仍有效,但函数返回后 map 一旦被修改或销毁,JSON 里所有键都会指向失效内存。用Value(const char*, Allocator&)构造则会立刻复制,代价是一次字符串拷贝,换来的是安全。
值类型是字符串时同理:
// 适用于 C++17
rapidjson::Value val(kv.second.c_str(), alloc);
doc.AddMember(key, val, alloc);整数、double、bool有直接接受标量的AddMember重载,doc.AddMember(key, 42, alloc)即可,不需要先包一层Value。
如果 map 的值本身是std::vector<int>,就要先构造一个数组Value,再把它作为成员加入:
// 适用于 C++17
rapidjson::Value nums(rapidjson::kArrayType);
for (int x : kv.second) {
nums.PushBack(x, alloc);
}
doc.AddMember(key, nums, alloc);PushBack对基本类型有重载,对字符串则需要显式构造Value,或使用接受StringRef的重载——同样要注意生命周期,字面量是安全的,临时std::string不安全。
三、数组与嵌套结构
数组用kArrayType初始化,然后逐个PushBack。数组元素是对象时,先构造局部对象,再把整个对象推进数组。
// 适用于 C++17
#include <string>
#include <vector>
#include "rapidjson/document.h"
struct Student {
std::string name;
int score;
std::vector<std::string> tags;
};
void addStudents(rapidjson::Document& doc, const std::vector<Student>& list) {
rapidjson::Document::AllocatorType& alloc = doc.GetAllocator();
rapidjson::Value arr(rapidjson::kArrayType);
arr.Reserve(list.size(), alloc); // 可选:预分配,减少扩容
for (const Student& s : list) {
rapidjson::Value obj(rapidjson::kObjectType);
obj.AddMember("name", rapidjson::Value(s.name.c_str(), alloc), alloc);
obj.AddMember("score", s.score, alloc);
rapidjson::Value tags(rapidjson::kArrayType);
for (const std::string& t : s.tags) {
tags.PushBack(rapidjson::Value(t.c_str(), alloc), alloc);
}
obj.AddMember("tags", tags, alloc);
arr.PushBack(obj, alloc);
}
doc.AddMember("students", arr, alloc);
}这里的"name"、"score"、"tags"都是字符串字面量,具有静态存储期,用StringRef语义传入是安全的。obj、tags、arr都是局部变量,PushBack/AddMember之后内容已经被搬进父节点,局部变量离开作用域只是析构一个空壳,不会影响结果。
嵌套层次不要过深。rapidjson 内部使用递归处理,极端深的嵌套会消耗栈空间;同时业务上超过三四层的 JSON 通常意味着数据结构该重新设计了。
如果要往已有对象里追加而不是新建,注意AddMember遇到同名键不会覆盖,而是产生重复键。需要覆盖时应先RemoveMember,或改用doc["key"] = newValue这种下标赋值方式(下标赋值在键不存在时会创建,存在时替换)。
四、序列化输出
组装完成后,用Writer或PrettyWriter把Document写进StringBuffer:
// 适用于 C++17
#include <iostream>
#include <string>
#include "rapidjson/stringbuffer.h"
#include "rapidjson/writer.h"
#include "rapidjson/prettywriter.h"
std::string toJson(const rapidjson::Document& doc) {
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
doc.Accept(writer);
return std::string(buffer.GetString(), buffer.GetSize());
}
std::string toPrettyJson(const rapidjson::Document& doc) {
rapidjson::StringBuffer buffer;
rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
writer.SetIndent(' ', 2);
doc.Accept(writer);
return std::string(buffer.GetString(), buffer.GetSize());
}Accept是Value的 const 成员函数,因此可以在 const 引用上调用。它的模板参数是处理器类型,Writer与PrettyWriter都满足要求。buffer.GetSize()比strlen更可靠:JSON 字符串理论上可以包含被转义的内容,用显式长度构造std::string是更稳妥的习惯。
五、完整示例
// 适用于 C++17
#include <iostream>
#include <map>
#include <string>
#include <vector>
#include "rapidjson/document.h"
#include "rapidjson/stringbuffer.h"
#include "rapidjson/prettywriter.h"
int main() {
rapidjson::Document doc;
doc.SetObject();
rapidjson::Document::AllocatorType& alloc = doc.GetAllocator();
// 1. map -> 对象
std::map<std::string, int> scores{{"alice", 90}, {"bob", 85}, {"carol", 77}};
rapidjson::Value scoreObj(rapidjson::kObjectType);
for (const auto& kv : scores) {
rapidjson::Value key(kv.first.c_str(), alloc);
scoreObj.AddMember(key, kv.second, alloc);
}
doc.AddMember("scores", scoreObj, alloc);
// 2. vector -> 数组
std::vector<std::string> names{"alice", "bob", "carol"};
rapidjson::Value nameArr(rapidjson::kArrayType);
for (const std::string& n : names) {
nameArr.PushBack(rapidjson::Value(n.c_str(), alloc), alloc);
}
doc.AddMember("names", nameArr, alloc);
// 3. 数组套对象
rapidjson::Value list(rapidjson::kArrayType);
for (const auto& kv : scores) {
rapidjson::Value item(rapidjson::kObjectType);
item.AddMember("name", rapidjson::Value(kv.first.c_str(), alloc), alloc);
item.AddMember("score", kv.second, alloc);
list.PushBack(item, alloc);
}
doc.AddMember("list", list, alloc);
rapidjson::StringBuffer buffer;
rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
writer.SetIndent(' ', 2);
doc.Accept(writer);
std::cout << buffer.GetString() << std::endl;
return 0;
}按标准语义和 rapidjson 的文档描述,输出应当是一个包含scores、names、list三个成员的对象,其中list的顺序由std::map的键序决定(alice、bob、carol)。若把std::map换成std::unordered_map,键的顺序就不再保证,JSON 文本会随实现变化——这对需要做文本比对或缓存的场景很关键。
常见坑点
1. 用 StringRef 绑定临时字符串
❌doc.AddMember(rapidjson::StringRef(name.c_str()), 1, alloc);,name是局部std::string。 ✅rapidjson::Value key(name.c_str(), alloc); doc.AddMember(key, 1, alloc);,让分配器复制。
2. 忘记设置根节点类型
❌rapidjson::Document doc; doc.AddMember("a", 1, alloc);,默认是 null 类型。 ✅ 先doc.SetObject();或doc.SetArray();。
3. 把同一把分配器用在不同 Document 上
❌ 用docA.GetAllocator()给docB添加成员。 ✅ 每个 Document 的元素只使用自己的分配器,跨文档要复制值而不是共享分配器。
**4. 直接把 std::string 传给需要 const char* 的接口**
❌rapidjson::Value v(s, alloc);其中s是std::string,没有这个重载。 ✅rapidjson::Value v(s.c_str(), s.size(), alloc);或rapidjson::Value v(s.c_str(), alloc);。
5. 认为 AddMember 会覆盖同名键
❌ 连续两次AddMember("id", ...),结果 JSON 里出现两个id。 ✅ 覆盖用doc["id"] = value;,或先doc.RemoveMember("id");。
6. 序列化时缓冲区提前销毁
❌ 返回const char*指向局部StringBuffer的内容。 ✅ 返回std::string(buffer.GetString(), buffer.GetSize());,把内容拷出来。
7. 用 strlen 计算 JSON 长度
❌std::string s(buffer.GetString());,遇到内嵌零字节会截断。 ✅ 用GetSize()显式给出长度。
8. 忽略浮点数的输出精度
❌ 以为double会原样输出所有十进制位。 ✅ rapidjson 默认按最短可回读的形式输出;需要固定精度时用Writer::SetMaxDecimalPlaces,并注意它影响的是所有浮点成员。
总结
| 任务 | 关键调用 | 注意点 |
|---|
| 根节点设为对象 | doc.SetObject() | 忘记会得到 null 类型 |
| 添加整数成员 | doc.AddMember(key, 42, alloc) | key 用 Value 拷贝 |
| 添加字符串成员 | Value(s.c_str(), alloc) | 不要用悬垂的 StringRef |
| 构造数组 | Value v(kArrayType) | 逐个PushBack |
| 嵌套对象 | 先建局部 Value 再加入父节点 | 加入后不再单独使用 |
| 输出紧凑文本 | Writer<StringBuffer> | 用GetSize()取长度 |
| 输出可读文本 | PrettyWriter+SetIndent | 缩进不影响语义 |
rapidjson 的组装逻辑可以概括成一句话:先定类型,再逐层添加,字符串一律让分配器复制。把字符串生命周期这件事处理对,剩下的接口都很直白。对性能敏感的场景可以复用StringBuffer、预先Reserve,但不要为了省一次拷贝把生命周期问题引进来。