从零实现C++轻量级JSON解析库:设计思路与核心代码详解

📅2026/7/13 7:39:37 👁️次浏览
从零实现C++轻量级JSON解析库:设计思路与核心代码详解
1. 项目概述为什么我们需要另一个C JSON库如果你用C做过网络通信、配置文件解析或者数据交换那你肯定绕不开JSON。这玩意儿现在是事实上的数据交换标准从Web API到游戏存档无处不在。市面上C的JSON库不少从老牌的rapidjson、nlohmann/json到新锐的simdjson、yyjson选择似乎很多。那为什么还要自己动手写一个“精简实用”的库呢这事儿我深有体会。几年前我接手一个嵌入式物联网项目设备资源紧张内存以KB计但又要通过MQTT和云端用JSON通信。当时试了rapidjson性能确实强但代码体积和内存占用对我们来说还是有点“奢侈”nlohmann/json的API用起来是真舒服像写现代C但依赖了C11的很多特性而且头文件一包含编译出来的固件大小就超标了。至于simdjson它为了极致解析速度用了SIMD指令但我们的芯片架构不支持而且它更偏向于“只读解析”我们还需要频繁地构建和修改JSON对象来组包。那时候我就想能不能有一个库它没有那么多“花活”就是踏踏实实地做好解析和生成代码量小到可以轻易放进资源受限的环境API直观到新手也能快速上手同时性能在常规场景下足够用。这就是我动手折腾这个“精简实用JSON解析库”的初衷。这个库的目标很明确轻量、自包含、零依赖、API直观、内存可控。它不追求在GB级JSON文件上比simdjson快也不追求像nlohmann/json那样提供近乎魔术般的语法糖。它的核心用户是嵌入式开发者、对可执行文件大小敏感的工具开发者、以及任何希望快速集成一个简单可靠JSON处理能力的C项目。接下来我会把这个库从设计思路到每一行关键代码的实现以及我踩过的坑和总结的经验毫无保留地分享出来。你会发现实现一个核心可用的JSON库并没有想象中那么复杂。2. 核心设计思路与数据结构选型写一个库尤其是基础工具库最忌讳一开始就埋头写代码。设计阶段多花一小时编码阶段能省十小时。我的设计思路围绕“精简”和“实用”两个核心展开。2.1 精简之道舍弃什么保留什么“精简”不是功能残缺而是精准定位。我首先明确了边界严格遵守JSON标准RFC 8259这是底线。必须正确解析null,true,false, 数字、字符串、数组、对象这六种类型。Unicode转义如\u4e2d、控制字符转义如\n,\t必须支持。只提供SAX流式解析和DOM树形解析两种最经典的接口模式。SAX适合只读、一次性的解析内存占用极小DOM适合需要随机访问和修改的场景。像JSON Path查询、JSON Schema验证、JSON Patch这些高级功能一概不做。需要的话用户可以在DOM之上自己实现或结合其他库。数字解析只支持IEEE 754双精度浮点数。JSON标准不区分整数和浮点数但内部用double存储足够覆盖绝大多数场景整数范围约为±2^53。我们不做int64_t的特化也不做任意精度数学如boost::multiprecision这能极大简化代码。内存管理简单明确。不引入复杂的内存池或自定义分配器初期版本。统一使用std::vector和std::string利用RAII管理生命周期避免内存泄漏。同时提供移动语义支持减少拷贝。2.2 实用之道API如何设计“实用”意味着API要符合直觉降低学习成本。模仿nlohmann/json的直观访问方式但更轻量。比如支持obj[key]、arr[0]这样的操作符重载。提供类似STL的迭代器方便遍历数组和对象。异常与错误码双轨制。默认使用C异常报告解析错误如格式错误、类型不匹配但对于禁用异常的嵌入式环境提供一个noexcept的解析接口和错误状态查询函数。统一的Value类型。用一个union或C17的std::variant来存储所有可能的JSON值类型通过一个enum Type来标识当前存储的实际类型。这是整个DOM模型的核心。2.3 核心数据结构JsonValue的设计经过权衡我选择了基于union和手工类型标签的传统方式而不是std::variant。原因是为了最大程度的兼容性C11即可和控制内存布局。std::variant有类型安全的好处但可能带来额外的存储开销取决于实现我们追求极致的精简。namespace simple_json { enum class ValueType { Null, Boolean, Number, String, Array, Object }; class JsonValue { public: // 构造函数、析构函数、拷贝/移动控制函数Rule of Five JsonValue(); // Null JsonValue(bool b); JsonValue(double num); JsonValue(const char* str); // 从C字符串构造 JsonValue(const std::string str); JsonValue(std::string str); // 移动构造 JsonValue(const std::vectorJsonValue arr); JsonValue(std::vectorJsonValue arr); // 移动构造 JsonValue(const std::mapstd::string, JsonValue obj); JsonValue(std::mapstd::string, JsonValue obj); // 移动构造 ~JsonValue(); JsonValue(const JsonValue other); JsonValue(JsonValue other) noexcept; JsonValue operator(const JsonValue other); JsonValue operator(JsonValue other) noexcept; // 类型查询 ValueType type() const noexcept { return type_; } bool is_null() const noexcept { return type_ ValueType::Null; } bool is_boolean() const noexcept { return type_ ValueType::Boolean; } // ... 其他 is_xxx 函数 // 值获取带类型检查错误时抛出异常 bool as_boolean() const; double as_number() const; const std::string as_string() const; const std::vectorJsonValue as_array() const; const std::mapstd::string, JsonValue as_object() const; // 用于数组和对象的操作符重载 JsonValue operator[](size_t index); // 数组访问 const JsonValue operator[](size_t index) const; JsonValue operator[](const std::string key); // 对象访问 const JsonValue operator[](const std::string key) const; // 序列化将JsonValue转换成JSON格式字符串 std::string serialize(bool pretty false, int indent 2) const; private: ValueType type_; union { bool boolean_; double number_; std::string string_; std::vectorJsonValue array_; std::mapstd::string, JsonValue object_; }; // 关键因为union里有非平凡类型std::string, vector, map // 我们需要手动管理它们的构造和析构。 void destroy() noexcept; void copy_from(const JsonValue other); void move_from(JsonValue other) noexcept; }; }设计要点与坑点union与非平凡类型这是最大的挑战。C11中union不能包含有非平凡构造/析构/拷贝/移动函数的成员如std::string。但C11放宽了限制允许了条件是必须由程序员自己管理这些成员的生命周期。这就是destroy(),copy_from(),move_from()函数存在的意义。在每个构造函数、赋值操作符和析构函数中我们必须精确地知道当前union里活跃的是哪个成员并调用对应的析构函数或placement new。移动语义为了实现高效的数据转移必须正确实现移动构造和移动赋值。移动时直接将内部数据的所有权从源对象“偷”过来并将源对象置为Null状态避免双重释放。std::mapvsstd::unordered_map对象字典的存储我选择了std::map而非std::unordered_map。虽然哈希表平均查找复杂度是O(1)而红黑树是O(log n)但std::map能保证键的遍历顺序是插入顺序C17起这对于某些需要保持JSON键顺序的场景虽然RFC标准说对象是无序的但很多解析器实际保持了顺序是友好的。且std::map的内存开销通常更稳定。在元素数量不多几百个的典型JSON场景下性能差异不大。如果你追求极致查找速度且不关心顺序可以很容易地改为std::unordered_map。const重载operator[]提供了const和非const版本这是必须的。非const版本用于修改或添加元素对于对象如果键不存在会创建一个Null值并返回引用模仿std::map的行为而const版本用于只读访问键不存在时会抛出异常。3. JSON解析器Parser的实现状态机与递归下降有了存储数据的JsonValue下一步就是如何把一段JSON文本字符串转换成这颗树。这就是解析器Parser的工作。我选择了递归下降解析器因为它结构清晰易于理解和调试特别适合JSON这种层次化的文法。3.1 解析器的工作流程与状态解析器核心是一个类它持有一个指向JSON字符串的指针或迭代器以及当前解析位置。它逐个字符地读取根据JSON语法规则将字符流转化为JsonValue。class Parser { public: JsonValue parse(const std::string json_str) { input_ json_str; pos_ 0; size_ json_str.size(); return parse_value(); } // 为禁用异常的环境提供接口 bool parse(const std::string json_str, JsonValue out, std::string err_msg) noexcept { try { out parse(json_str); return true; } catch (const ParseError e) { err_msg e.what(); return false; } } private: std::string input_; size_t pos_ 0; size_t size_ 0; // 核心解析函数 JsonValue parse_value(); JsonValue parse_null(); JsonValue parse_true(); JsonValue parse_false(); JsonValue parse_number(); JsonValue parse_string(); JsonValue parse_array(); JsonValue parse_object(); // 工具函数 void skip_whitespace() noexcept; char get_current_char() const; char get_next_char(); void expect_char(char expected); std::string parse_unicode_escape(); // 处理 \uXXXX };3.2 递归下降解析详解以parse_array为例我们以解析数组[...]为例看看递归下降是如何工作的。JsonValue Parser::parse_array() { expect_char([); // 确保当前字符是[ skip_whitespace(); std::vectorJsonValue arr; // 处理空数组 [] if (get_current_char() ]) { get_next_char(); // 消耗掉] return JsonValue(std::move(arr)); } while (true) { // 1. 解析一个数组元素值 arr.push_back(parse_value()); // 递归调用 parse_value! skip_whitespace(); // 2. 看下一个字符是逗号还是右中括号 char c get_current_char(); if (c ]) { get_next_char(); // 消耗掉] break; // 数组结束 } else if (c ,) { get_next_char(); // 消耗掉, skip_whitespace(); // 检查尾随逗号如 [1,2,] 是非法的 if (get_current_char() ]) { throw ParseError(Trailing comma in array at position std::to_string(pos_)); } continue; // 继续解析下一个元素 } else { // 既不是]也不是,语法错误 throw ParseError(Expected , or ] in array at position std::to_string(pos_)); } } return JsonValue(std::move(arr)); }关键点与技巧递归调用parse_value()是总入口它根据当前字符判断具体类型然后调用parse_array,parse_object等。在parse_array内部又调用parse_value来解析每个元素这就自然地形成了递归完美匹配JSON的嵌套结构。错误恢复与报告expect_char函数用于断言下一个字符是什么如果不是则抛出包含位置的错误信息。这是提供清晰错误提示的关键。尾随逗号处理JSON标准不允许对象或数组的最后一个元素后面有逗号如[1,2,]。很多解析器如JavaScript的JSON.parse也不允许。我们必须严格检查并报错。但有些库如nlohmann/json通过扩展支持了尾随逗号如果你需要这个便利可以在这里放宽检查。空白字符跳过skip_whitespace()在解析结构[,],{,},:和逗号后必须调用因为JSON允许在这些符号周围有任意空白空格、制表符、换行符。3.3 数字解析的魔鬼细节parse_number数字解析看似简单实则坑最多。JSON数字语法是[-]?(0|[1-9][0-9]*)(\.[0-9])?([eE][-]?[0-9])?。我们不能简单地用std::stod因为需要在解析过程中进行语法验证并且要自己处理可能出现的溢出虽然最终交给std::stod或strtod。JsonValue Parser::parse_number() { size_t start_pos pos_; // 1. 可选负号 if (get_current_char() -) { get_next_char(); } // 2. 整数部分 if (get_current_char() 0) { get_next_char(); } else if (std::isdigit(get_current_char())) { while (std::isdigit(get_current_char())) { get_next_char(); } } else { throw ParseError(Invalid number at position std::to_string(start_pos)); } // 3. 小数部分 if (get_current_char() .) { get_next_char(); if (!std::isdigit(get_current_char())) { throw ParseError(At least one digit required after decimal point at position std::to_string(pos_)); } while (std::isdigit(get_current_char())) { get_next_char(); } } // 4. 指数部分 if (get_current_char() e || get_current_char() E) { get_next_char(); if (get_current_char() || get_current_char() -) { get_next_char(); } if (!std::isdigit(get_current_char())) { throw ParseError(At least one digit required in exponent at position std::to_string(pos_)); } while (std::isdigit(get_current_char())) { get_next_char(); } } // 此时从 start_pos 到 pos_-1 的字符构成了一个合法的数字字符串 std::string num_str input_.substr(start_pos, pos_ - start_pos); // 使用标准库转换它会处理溢出转为inf等问题 char* endptr nullptr; double val std::strtod(num_str.c_str(), endptr); // 理论上endptr应该指向字符串末尾因为我们已经做了语法检查 if (errno ERANGE) { // 可能发生上溢或下溢但strtod会返回HUGE_VAL或0并设置errno // 我们可以选择抛出异常或接受inf/0 errno 0; // 清除errno } return JsonValue(val); }注意事项性能这里我们提取子字符串后调用std::strtod。在性能敏感的场合可以像yyjson那样实现一个手写的、不进行内存分配的数字解析器直接计算double值能提升不少速度。errno使用C标准库函数时要注意线程安全性。errno是线程局部的但在解析过程中如果被其他信号中断修改可能会出错。更稳健的做法是使用std::from_charsC17它不设置全局errno且性能更好。但为了兼容C11我们这里用了strtod。大整数精度如前所述超过2^53的整数用double存储会有精度损失。如果应用场景涉及大整数ID需要在设计数据格式时就避免或者在这个库的基础上扩展一个long long或字符串存储的选项。3.4 字符串解析与Unicode转义parse_string字符串解析需要处理转义字符\,\\,\/,\b,\f,\n,\r,\t, 以及Unicode转义\uXXXX。JsonValue Parser::parse_string() { expect_char(\); std::string str; str.reserve(32); // 预分配减少多次分配 while (true) { char c get_next_char(); if (c \) { break; // 字符串结束 } else if (c \\) { // 转义字符 c get_next_char(); switch (c) { case \: str.push_back(\); break; case \\: str.push_back(\\); break; case /: str.push_back(/); break; case b: str.push_back(\b); break; case f: str.push_back(\f); break; case n: str.push_back(\n); break; case r: str.push_back(\r); break; case t: str.push_back(\t); break; case u: { // Unicode转义需要解析4位十六进制 std::string utf8_seq parse_unicode_escape(); str.append(utf8_seq); break; } default: throw ParseError(Invalid escape sequence \\ std::string(1, c) at position std::to_string(pos_)); } } else if (static_castunsigned char(c) 0x20) { // JSON中不允许未转义的控制字符U0000 到 U001F throw ParseError(Unescaped control character in string at position std::to_string(pos_)); } else { // 普通字符直接追加 str.push_back(c); } } return JsonValue(std::move(str)); }parse_unicode_escape的实现这是另一个难点。\u4E2D代表Unicode码点U4E2D“中”字。我们需要将这4位十六进制数解析出来然后将其转换为UTF-8编码序列追加到结果字符串中。UTF-8编码规则是码点 0x7F: 1字节0xxxxxxx码点 0x7FF: 2字节110xxxxx 10xxxxxx码点 0xFFFF: 3字节1110xxxx 10xxxxxx 10xxxxxx码点 0x10FFFF: 4字节11110xxx 10xxxxxx 10xxxxxx 10xxxxxx此外JSON还支持代理对Surrogate Pair\uD83D\uDE00表示一个码点超过0xFFFF的字符如表情符号。我们需要将高位代理0xD800–0xDBFF和低位代理0xDC00–0xDFFF组合计算实际码点code_point 0x10000 (high - 0xD800) * 0x400 (low - 0xDC00)。实现parse_unicode_escape需要小心处理这些细节确保生成的UTF-8序列是正确的。这里代码较长但逻辑是读取4个十六进制字符-转换为整数-判断是否为代理-如果是高位代理则期待下一个转义是低位代理然后组合计算-最后将码点转换为UTF-8字节序列。4. JSON生成器Serializer与美化输出解析是把文本变树生成序列化则是把树变回文本。这相对简单但也有一些细节。4.1 基本序列化我们需要为每种ValueType实现对应的字符串转换。Null-nullBoolean-true或falseNumber- 使用std::to_string或std::snprintf转换为字符串。注意要避免输出不必要的尾随零如1.200变成1.2和科学计数法除非数字很大或很小。一个简单的方法是使用std::snprintf配合%.10g这样的格式它能自动选择合理的表示法。String- 需要反向转义将特殊字符,\, 控制字符等转换回\,\\等形式。非ASCII字符UTF-8多字节序列直接输出即可JSON字符串本身是UTF-8编码的。Array- 递归序列化每个元素用逗号连接外加[]。Object- 递归序列化每个键值对键需要加引号和转义用冒号分隔键值逗号分隔对外加{}。4.2 美化打印Pretty Print美化打印就是在序列化时插入换行和缩进让JSON更易读。这需要在递归序列化函数中传递一个当前缩进级别的参数。void serialize_string(const std::string str, std::string out) { out.push_back(\); for (char c : str) { switch (c) { case \: out.append(\\\); break; case \\: out.append(\\\\); break; // ... 其他转义 default: if (static_castunsigned char(c) 0x20) { // 控制字符必须转义 char buf[7]; std::snprintf(buf, sizeof(buf), \\u%04x, c); out.append(buf); } else { out.push_back(c); } } } out.push_back(\); } void serialize_value(const JsonValue val, std::string out, bool pretty, int indent_level, const std::string indent_str) { switch (val.type()) { case ValueType::String: serialize_string(val.as_string(), out); break; case ValueType::Array: { const auto arr val.as_array(); out.push_back([); if (pretty !arr.empty()) out.push_back(\n); for (size_t i 0; i arr.size(); i) { if (pretty) { out.append(indent_level 1, indent_str[0]); // 增加一级缩进 } serialize_value(arr[i], out, pretty, indent_level 1, indent_str); if (i ! arr.size() - 1) { out.push_back(,); } if (pretty) out.push_back(\n); } if (pretty !arr.empty()) { out.append(indent_level, indent_str[0]); // 回到当前级缩进 } out.push_back(]); break; } case ValueType::Object: { const auto obj val.as_object(); out.push_back({); if (pretty !obj.empty()) out.push_back(\n); size_t count 0; for (const auto [key, value] : obj) { if (pretty) { out.append(indent_level 1, indent_str[0]); } serialize_string(key, out); out.push_back(:); if (pretty) out.push_back( ); serialize_value(value, out, pretty, indent_level 1, indent_str); if (count ! obj.size()) { out.push_back(,); } if (pretty) out.push_back(\n); } if (pretty !obj.empty()) { out.append(indent_level, indent_str[0]); } out.push_back(}); break; } // ... 其他类型 } }性能考虑序列化时频繁的字符串拼接out ...会导致多次内存分配和拷贝。最佳实践是预先估算输出大小虽然对JSON很难精确估计或者使用std::ostringstream但更高效的做法是传入一个可追加的字符串引用并让其内部通过reserve预分配空间。对于超大JSON可以考虑流式输出避免在内存中构建整个字符串。5. 内存优化与性能调优实战一个“精简”的库也要在性能上过得去。虽然我们不追求极致但一些基本的优化能带来显著提升。5.1 解析器性能瓶颈分析用简单的性能测试如解析一个几MB的嵌套JSON文件并做profiling你会发现热点通常在内存分配std::string和std::vector的构造/析构、push_back导致的扩容。字符处理skip_whitespace()的循环、isdigit()等函数调用。数字解析std::strtod或手写解析循环。5.2 针对性优化策略减少临时字符串分配在parse_string中使用str.reserve(estimated_size)预分配。可以简单估计如果字符串不长直接预留一个固定大小如64或者第一遍扫描只计算长度不构建字符串第二遍再精确拷贝。对于数字解析我们创建了num_str子字符串。可以改为在输入字符串上直接操作用一个char*指针传给strtod但这需要确保数字部分以\0结尾或者使用std::from_chars它接受首尾指针。使用std::string_viewC17这是大杀器。解析过程中很多地方如对象键我们只需要引用原始JSON字符串中的一段而不需要拷贝。std::string_view能完美胜任零拷贝。但要注意生命周期必须确保原字符串在string_view使用期间有效。我们的解析器在解析过程中原字符串是始终存在的所以安全。对于C11可以自己实现一个简单的StringRef类包含指针和长度。优化空白跳过void skip_whitespace() noexcept { while (pos_ size_) { char c input_[pos_]; if (c || c \t || c \n || c \r) { pos_; } else { break; } } }这个循环很简洁。在某些编译器上手动展开循环或使用查找表可能有一点点提升但可读性会下降。对于JSON空白字符比例不高这个优化收益不大。手写数字解析替换std::strtod。实现一个double解析器并不简单但一个只处理十进制、没有复杂舍入的版本可以大幅提速。核心思路是分别解析整数部分、小数部分、指数部分然后通过数学运算组合成double值。这需要处理溢出、下溢和精度问题但比通用的strtod快。yyjson和simdjson都有高度优化的手写数字解析。使用移动语义在JsonValue的构造函数和解析器中对于临时创建的std::string和std::vector坚决使用移动语义std::move来转移数据所有权避免深拷贝。5.3 一个关键取舍解析时拷贝 vs 引用激进的内存优化是在解析时对于字符串值不立即创建std::string拷贝而是存储string_view指向原始输入。只有当用户修改这个字符串时才进行拷贝写时复制Copy-On-Write。这能极大减少解析阶段的内存分配和拷贝尤其是对于大型字符串值。rapidjson就采用了这种策略。但是这带来了复杂性需要管理原始输入字符串的生命周期。如果输入字符串在JsonValue使用前被销毁引用就悬垂了。JsonValue的接口变得复杂需要区分“引用”和“拥有”的状态。写时复制需要引用计数或标志位增加了开销。对于“精简实用”的目标我选择了更简单安全的做法解析时立即拷贝。这保证了JsonValue的完全独立性生命周期自管理API简单。内存开销的增加在大多数中小型JSON应用中是可接受的。如果你处理的是巨大的、只读的JSON配置文件可以考虑实现一个可选的“引用解析”模式。6. 常见问题、调试技巧与测试策略自己写的轮子一定要经过充分的测试和打磨。下面是我在开发和实际使用中遇到的一些典型问题及解决方法。6.1 编译与链接问题头文件包含与内联为了做到“头文件库”只需包含一个.hpp文件所有函数实现都必须放在头文件中并标记为inline。这可能导致编译时间变长但方便集成。确保函数定义在类定义之后或者直接写在类内隐式内联。C标准版本我们使用了C11的特性移动语义、基于范围的for循环等。在编译时需要指定-stdc11或更高。如果你的环境必须是C98那需要大幅重写避免使用std::map的初始化列表等特性。union与析构函数手动管理union中非平凡类型的析构是易错点。务必在JsonValue的析构函数、拷贝/移动赋值运算符中正确调用destroy()和copy_from/move_from。使用工具如valgrind或AddressSanitizer来检测内存错误。6.2 运行时解析错误“Expected value”错误通常是JSON格式错误比如多余的逗号、缺失的引号。仔细检查错误信息中提供的位置position在原始JSON字符串中找到对应位置注意位置是从0开始还是1开始查看附近的字符。“Invalid number”错误检查数字格式。常见错误数字以0开头如0123除非它是0本身指数部分缺少数字如1.2e小数点后缺少数字如1.。“Unescaped control character”错误JSON字符串中不能直接包含ASCII码小于0x20的控制字符如换行符\n、制表符\t。它们必须转义为\n,\t等。如果你从其他系统生成的JSON出现此错误可能是对方没有正确转义。内存耗尽特别是在嵌入式设备解析深度嵌套的JSON如[[[[...]]]]会导致递归调用parse_array/parse_object可能栈溢出。我们的递归下降解析器存在这个风险。解决方案改为迭代栈模拟递归这是最根本的解决方法。手动维护一个栈来保存解析状态而不是使用函数调用栈。这能彻底消除递归深度限制但代码会复杂很多。设置递归深度限制在parse_value中增加一个深度参数超过一定阈值如512就抛出异常。这是一种安全防护。对于已知数据格式如果嵌套不会太深可以忽略此问题。大多数实际应用的JSON嵌套在几十层以内。6.3 单元测试是生命线为解析库编写全面的单元测试至关重要。测试用例应该覆盖基础类型null,true,false, 各种数字整数、小数、负数、科学计数法、边界值字符串含转义、Unicode、空字符串。复合类型空数组[]、空对象{}、嵌套结构。错误案例格式错误的JSON如多余的逗号、缺失的括号、错误的转义、控制字符等。序列化往返解析一个JSON字符串然后序列化再解析确保结果一致。内存泄漏测试使用Valgrind或编译器插桩如-fsanitizeaddress运行测试确保没有内存泄漏。一个简单的测试框架示例使用Catch2或Google TestTEST_CASE(Parse basic types) { Parser p; CHECK(p.parse(null).is_null()); CHECK(p.parse(true).as_boolean() true); CHECK(p.parse(false).as_boolean() false); CHECK(p.parse(42).as_number() 42.0); CHECK(p.parse(\hello\).as_string() hello); } TEST_CASE(Parse error handling) { Parser p; CHECK_THROWS_AS(p.parse({), ParseError); // 未闭合的对象 CHECK_THROWS_AS(p.parse([1,2,]), ParseError); // 尾随逗号 }6.4 与现有库的兼容性考量虽然我们的库是独立的但有时需要与其他库交互。例如从rapidjson的Document转换到我们的JsonValue。可以编写适配器函数遍历rapidjson的DOM树构建我们的树。反之亦然。这通常用于项目迁移或混合使用不同库的模块。7. 进阶扩展与实战应用场景一个基础库完成后可以根据实际需求进行扩展。这里分享几个我实践过的方向。7.1 添加SAX流式解析接口DOM解析需要构建完整的树内存占用大。SAX解析是事件驱动的解析器在读取JSON时触发回调如onObjectStart,onString,onNumber应用程序在回调中处理数据不需要构建完整树。这对于只提取特定字段、过滤数据或处理超大JSON非常有用。实现SAX解析器可以在我们现有的递归下降解析器基础上修改不构建JsonValue而是在每个parse_xxx函数中调用对应的回调函数。我们需要定义一个SAX处理器接口抽象基类用户继承并实现感兴趣的回调。class SaxHandler { public: virtual ~SaxHandler() default; virtual bool onNull() 0; virtual bool onBoolean(bool val) 0; virtual bool onNumber(double val) 0; virtual bool onString(const std::string val) 0; virtual bool onArrayStart() 0; virtual bool onArrayEnd() 0; virtual bool onObjectStart() 0; virtual bool onObjectEnd() 0; // 对于对象键可以单独一个回调 virtual bool onKey(const std::string key) 0; }; class SaxParser { public: bool parse(const std::string json_str, SaxHandler handler); // 实现类似Parser但将值传递给handler };7.2 支持JSON Path查询简易版JSON Path是一种查询语言可以快速定位JSON树中的节点。实现一个完整的JSON Path引擎很复杂但可以实现一个常用子集比如只支持.操作符和[*]通配符。思路是将JSON Path表达式如$.store.book[0].title解析成一系列令牌$,.,store,.,book,[,0,],.,title。然后从根节点开始依次应用这些令牌来遍历JsonValue树。遇到[*]时需要对数组的每个元素递归应用剩余的路径。7.3 实战场景嵌入式设备配置解析这是我最初的需求场景。设备启动时从Flash读取一个JSON配置文件解析后设置各项参数。配置文件不大几KB但设备内存只有几十KB。我们的库如何适配禁用异常使用noexcept版本的parse函数通过返回值判断成功与否错误信息通过输出参数返回。静态内存分配可以定制JsonValue的内存分配。例如使用一个固定大小的内存池来分配std::string和std::vector的内部缓冲区避免堆碎片化。这需要替换std::allocator或者自己实现简单的字符串和向量类。简化类型如果配置文件中只有数字、字符串和布尔值没有嵌套数组或对象可以进一步简化JsonValue使用union只包含这几种类型去掉std::map和std::vector用C数组和固定大小的结构体代替。7.4 性能对比与选型建议虽然我们的库主打轻量但了解其在性能光谱中的位置也有帮助。可以用一个标准的JSON测试套件如nativejson-benchmark进行对比。通常结果会是解析速度simdjsonyyjsonrapidjson 我们的库 nlohmann/json。内存占用我们的库如果开启字符串引用优化可能接近rapidjson小于nlohmann/json。代码体积我们的库单头文件通常远小于rapidjson和nlohmann/json。选型建议追求极致解析速度处理GB级JSON选simdjson或yyjson。需要最友好、现代的API且不介意二进制体积和内存选nlohmann/json。成熟稳定功能全面性能均衡选rapidjson。资源极度受限嵌入式、需要零依赖、或想深入理解JSON解析原理用我们的这个精简库或者以其为起点进行定制。自己实现一遍最大的收获不是造出了一个多厉害的轮子而是彻底弄懂了JSON解析的每一个细节。以后再遇到任何JSON相关的问题你都能从原理层面去分析和解决。这种底层的掌控感是单纯使用第三方库无法带来的。