C++ JSON数据处理:jsoncpp库的集成、核心API与实战应用
1. 项目概述为什么C项目需要jsoncpp在C项目里处理JSON数据这事儿听起来简单但真上手了你会发现C标准库对JSON是“零支持”。你没法像Python那样直接import json就开干。无论是从网络API接收配置还是把程序内部复杂的数据结构序列化后存盘或发送JSON都是绕不开的现代数据交换格式。这时候一个成熟、稳定、易用的第三方库就成了刚需。在众多C JSON库中jsoncpp以其悠久的历史、稳定的API和极佳的兼容性成为了无数项目的“默认选择”。它不像一些新兴库那样追求极致的解析速度比如用SIMD指令集而是把重点放在了可靠性、易用性和向后兼容上。对于大多数业务系统、工具软件、游戏配置管理来说jsoncpp提供的性能已经完全足够而它的稳定性和“不折腾”的特性才是我们开发者更看重的。简单说如果你需要一个“装上去就能用几年都不用管而且几乎不会出岔子”的JSON解决方案jsoncpp就是你的菜。它支持从字符串或流中解析JSON也能将内存中的对象树序列化成字符串或写入文件甚至还能在序列化/反序列化过程中保留JSON文件中的注释——这个特性对于需要用户手动编辑的配置文件来说非常贴心。2. 核心思路与项目集成方案2.1 为什么选择jsoncpp与其他方案的对比在决定使用jsoncpp之前我们通常会在脑子里过一遍几个备选RapidJSON、nlohmann/json还有Boost库里的Boost.PropertyTree虽然它不完全算纯JSON库。RapidJSON: 性能怪兽号称最快的JSON解析器之一。但它为了追求速度API设计上比较“C风格”内存管理需要手动介入使用“移动语义”来避免拷贝对新手不太友好代码写起来也稍显繁琐。如果你的项目对JSON解析性能有极致要求比如每秒要处理GB级的日志并且团队能驾驭其内存模型RapidJSON是首选。nlohmann/json: 现代CC11及以上的杰作API设计极其优雅和直观用起来就像脚本语言一样简单。它的核心优势是可以通过魔法般的模板元编程自动将你的自定义结构体序列化和反序列化。缺点是头文件巨大单头文件版本编译时间较长并且由于大量使用模板二进制体积和运行时内存开销可能稍大。Boost.PropertyTree: 它其实是一个通用的属性树容器支持XML、JSON、INI等多种格式。用它来处理简单的JSON配置没问题但功能比较基础对JSON标准的支持不完全比如不区分整数和浮点数性能也一般。通常只在项目已经重度依赖Boost且JSON处理需求极其简单时考虑。jsoncpp: 这就是我们今天的主角。它处在“性能”和“易用性”的平衡点上。API是直观的面向对象风格虽然不如nlohmann/json那么现代学习成本低。它采用经典的源码集成或动态库链接方式稳定可靠对C11之前的古老编译器也有良好支持0.y.z版本分支。它的维护模式是“成熟维护”意味着API稳定不会三天两头让你升级代码这对于需要长期稳定运行的企业级项目至关重要。选择jsoncpp的典型场景项目需要支持较老的编译器或平台如某些嵌入式环境。团队风格偏保守希望依赖一个变化少、bug少的库。JSON数据结构复杂但性能要求不是瓶颈网络I/O或磁盘I/O往往是更大的瓶颈。需要保留JSON文件中的注释信息。2.2 集成到你的项目三种主流方式jsoncpp提供了多种集成方式你可以根据项目的构建系统和部署需求来选择。2.2.1 源码集成推荐用于中小项目这是最直接、依赖关系最清晰的方式。你可以直接下载jsoncpp的源码将其作为项目子模块git submodule或直接拷贝源码文件到你的项目目录中。操作步骤获取源码从官方GitHub仓库open-source-parsers/jsoncpp克隆或下载发布版的源码包。放入项目在你的项目目录下比如third_party/jsoncpp放入jsoncpp的include和src目录内容。修改构建配置CMake: 使用add_subdirectory(third_party/jsoncpp)然后通过target_link_libraries(your_target PRIVATE JsonCpp::JsonCpp)来链接。Makefile/直接编译将src/lib_json下的所有.cpp文件加入你的编译列表并将include目录添加到头文件搜索路径-I参数。优点完全自包含无需担心目标系统是否安装了特定版本的jsoncpp。构建过程确定便于交叉编译。缺点增加了项目本身的代码量需要自己管理更新。2.2.2 使用Amalgamated单一文件极简集成如果你嫌源码文件太多jsoncpp贴心地提供了一个Python脚本amalgamate.py可以将所有源码合并成一个.cpp文件和一个.h文件。操作步骤在jsoncpp源码根目录运行python amalgamate.py这会在dist目录下生成jsoncpp.cpp、json/json.h和json/json-forwards.h。你只需要将jsoncpp.cpp加入你的项目编译并包含json/json.h头文件即可。优点集成超级简单只需要处理两个文件。非常适合小型项目或快速原型。缺点任何改动都需要重新生成合并文件不利于编译器进行模块化优化调试时文件巨大。2.2.3 使用系统包管理器或构建系统集成推荐用于大型项目/团队这是最“干净”的方式将jsoncpp作为外部依赖来管理。vcpkg:vcpkg install jsoncppConan: 在conanfile.txt中添加jsoncpp/1.9.8请检查最新版本。CMake的find_package: 如果你的系统已经通过包管理器如apt, yum, brew安装了jsoncpp的开发包例如libjsoncpp-dev可以直接在CMakeLists.txt中使用find_package(JsonCpp REQUIRED)然后链接JsonCpp::JsonCpp。优点依赖管理清晰便于持续集成和团队协作。可以享受系统级的更新和安全补丁。缺点要求部署环境也必须安装对应的库可能增加部署复杂度。个人经验之谈对于个人项目或小团队我强烈推荐源码集成CMake的add_subdirectory。它平衡了简单性和可控性。避免使用单一文件版本除非你非常确定项目规模永远很小。对于公司内部的大型项目使用Conan或vcpkg这类包管理器来统一管理所有第三方依赖是更专业的选择能有效解决“在我机器上能编译”的经典问题。3. jsoncpp核心API详解与基本使用集成好库之后我们来真正上手使用。jsoncpp的核心概念非常直观主要围绕Json::Value这个类展开。它就像一个万能容器可以表示JSON标准中的所有类型对象object、数组array、字符串string、数字int、uint、double、布尔值bool以及null。3.1 解析JSON从字符串或文件到内存对象解析就是将JSON格式的文本字符串转换成内存中的Json::Value对象树。#include iostream #include fstream #include json/json.h // 关键头文件 void parseFromString() { // 一个简单的JSON字符串 std::string jsonStr R({ name: 张三, age: 30, isStudent: false, skills: [C, Linux, Git], address: { city: 北京, street: 中关村 } }); Json::Value root; // 这是我们的根节点一个万能容器 Json::CharReaderBuilder readerBuilder; std::unique_ptrJson::CharReader reader(readerBuilder.newCharReader()); std::string errs; // 用于存储解析错误信息 // 开始解析 bool parsingSuccessful reader-parse(jsonStr.c_str(), jsonStr.c_str() jsonStr.length(), root, errs); if (!parsingSuccessful) { std::cerr 解析JSON失败: errs std::endl; return; } // 访问数据 std::string name root[name].asString(); // 获取字符串 int age root[age].asInt(); // 获取整数 bool isStudent root[isStudent].asBool(); // 获取布尔值 std::cout 姓名: name std::endl; std::cout 年龄: age std::endl; // 访问数组 const Json::Value skills root[skills]; if (skills.isArray()) { std::cout 技能: ; for (const auto skill : skills) { std::cout skill.asString() ; } std::cout std::endl; } // 访问嵌套对象 std::string city root[address][city].asString(); std::cout 城市: city std::endl; }关键点解析Json::CharReaderBuilder和Json::CharReader: 这是jsoncpp推荐的解析器工厂和解析器。使用Builder模式可以方便地设置解析选项比如是否允许尾随逗号、注释等。parse方法参数是字符串的起始指针和结束指针这样避免了不必要的字符串拷贝。root是解析结果的输出位置errs用于接收错误信息。类型安全访问root[name]返回的是一个Json::Value引用。直接调用.asString()、.asInt()等方法会进行类型转换。如果实际类型不匹配比如name字段的值是数字asString()会返回一个默认值空字符串或进行隐式转换数字转成字符串形式。更安全的做法是先检查类型if (root[age].isInt()) { int age root[age].asInt(); }检查键是否存在使用root.isMember(key)或root.get(key, defaultValue)。后者在键不存在时返回你提供的默认值非常方便。从文件解析更简单只需要把文件内容读入字符串然后使用上述方法即可。jsoncpp也提供了Json::parseFromStream的便捷函数。3.2 生成与序列化JSON从内存对象到字符串或文件生成JSON就是反向操作我们构建一个Json::Value树然后把它转换成字符串。void buildAndSerialize() { Json::Value root; // 添加基本类型 root[project] MyCppApp; root[version] 1.2; root[stable] true; // 添加数组 Json::Value dependencies(Json::arrayValue); // 显式创建数组类型的Value dependencies.append(cmake); dependencies.append(gcc); dependencies.append(jsoncpp); root[dependencies] dependencies; // 赋值给根对象 // 添加嵌套对象 Json::Value author; author[name] 李四; author[email] li.siexample.com; root[author] author; // 添加null值 root[optionalField] Json::nullValue; // 序列化为字符串带格式化的漂亮打印 Json::StreamWriterBuilder writerBuilder; writerBuilder[indentation] \t; // 设置缩进为制表符 std::string jsonStr Json::writeString(writerBuilder, root); std::cout 生成的JSON:\n jsonStr std::endl; // 序列化并写入文件 std::ofstream ofs(config.json); if (ofs) { // 使用同一个builder创建writer std::unique_ptrJson::StreamWriter writer(writerBuilder.newStreamWriter()); writer-write(root, ofs); ofs.close(); std::cout 已写入文件 config.json std::endl; } }关键点解析赋值操作符重载Json::Value重载了操作符你可以直接用将C基本类型int,double,bool,const char*,std::string赋值给它它会自动转换。创建数组Json::Value(Json::arrayValue)创建一个空的数组类型Value。然后使用.append(value)方法添加元素。创建对象直接声明一个Json::Value它默认是对象类型objectValue。然后通过[“key”] value的方式添加键值对。Json::StreamWriterBuilder: 类似于Reader的Builder用于配置序列化选项比如缩进风格、是否在JSON末尾添加换行等。Json::writeString: 将Json::Value序列化成格式化字符串的便捷函数。写入文件通过newStreamWriter()创建一个写入器然后调用write方法将数据写入输出流可以是文件流、字符串流等。4. 高级特性与实战技巧掌握了基本读写我们来看看jsoncpp一些能提升开发效率和代码健壮性的高级特性。4.1 类型检查与安全访问直接使用asXXX()进行转换虽然方便但在不确定数据结构时是危险的。健壮的代码应该进行类型检查。void safeAccess(const Json::Value config) { // 方法1检查类型和存在性 if (config.isObject() config.isMember(timeout) config[timeout].isInt()) { int timeout config[timeout].asInt(); std::cout Timeout: timeout std::endl; } else { std::cerr ‘timeout‘字段缺失或类型错误使用默认值。 std::endl; } // 方法2使用get方法提供默认值 (更简洁) int timeout config.get(timeout, 30).asInt(); // 如果timeout不存在或类型不可转换则返回Json::Value(30)再转为int std::cout Timeout (with default): timeout std::endl; // 遍历对象的所有键值对 if (config.isObject()) { std::cout Config entries: std::endl; for (auto it config.begin(); it ! config.end(); it) { std::cout Key: it.name() , Type: it-type() std::endl; // it-type() 返回枚举值如 stringValue, intValue, arrayValue等 } } }get(key, defaultValue)方法在实战中非常有用它能让你用一行代码处理字段缺失的情况使代码更简洁。4.2 保留注释这是jsoncpp一个非常独特的卖点。很多配置文件比如VS Code的settings.json允许注释但标准的JSON解析器会丢弃它们。jsoncpp可以选择性地保留这些注释。void preserveComments() { std::string jsonWithComments R({ // 这是用户名 name: 王五, /* 这是年龄 多行注释 */ age: 25 }); Json::Value root; Json::CharReaderBuilder readerBuilder; readerBuilder.settings_[allowComments] true; // 关键设置允许注释 readerBuilder.settings_[strictRoot] false; // 非严格模式对格式要求更宽松 std::string errs; std::unique_ptrJson::CharReader reader(readerBuilder.newCharReader()); bool ok reader-parse(jsonWithComments.c_str(), jsonWithComments.c_str() jsonWithComments.length(), root, errs); Json::StreamWriterBuilder writerBuilder; writerBuilder.settings_[indentation] ; writerBuilder.settings_[enableYAMLCompatibility] false; // 为了保留注释需要关闭一些兼容性选项 writerBuilder.settings_[commentStyle] All; // 保留所有注释 // 注意旧版jsoncpp的注释保留功能更直接。新版1.9.x中注释信息存储在Json::Value的元数据中 // 序列化时需要使用特定的Writer如StyledWriter的遗留模式或仔细配置StreamWriterBuilder才能输出。 // 对于需要严格保留注释往返的场景建议查阅对应版本的详细文档或测试。 std::string output Json::writeString(writerBuilder, root); std::cout output std::endl; // 理想情况下注释应该被保留在输出中 }重要提示jsoncpp保留注释的功能在不同版本间行为有所变化。在新版本中注释被视为附属于特定值的“元数据”。如果你项目的核心需求是无损往返读-改-写不丢失注释务必对你使用的jsoncpp版本进行详细的测试并查阅其include/json/features.h和文档中关于Features的设置。4.3 性能考量使用静态字符串与移动语义虽然jsoncpp不以性能为第一目标但在处理大型JSON时一些技巧仍能带来提升。void performanceTips() { Json::Value largeArray(Json::arrayValue); for (int i 0; i 10000; i) { Json::Value obj; // 避免每次构造临时std::string。如果键是固定的使用静态字符串。 // obj[id] std::to_string(i); // 会生成临时string // 更好对于固定键使用静态字符串视图C17或C风格字符串字面量。 // jsoncpp内部会复制字符串但使用字面量避免了std::string的构造和分配。 obj[id] std::to_string(i).c_str(); // 仍然有临时string但稍好 // 最佳实践对于大规模构建考虑重用Json::Value对象或使用reserve如果底层实现支持。 // jsoncpp的Value对象赋值是深拷贝要避免不必要的拷贝。 } // 使用移动语义C11及以上 Json::Value source; source[data] very large string content...; Json::Value target; // target source; // 深拷贝内存开销大 target std::move(source); // 移动赋值将source的资源转移给targetsource变为空 // 此时 source.isNull() true std::cout After move, target has data: !target[data].isNull() std::endl; }对于超大型JSON几十MB以上或对解析速度有严格要求的场景jsoncpp可能不是最优选。但对于99%的应用其性能是完全可接受的。真正的性能瓶颈往往在于文件I/O或网络传输而非内存中的解析。5. 常见问题排查与调试心得即使再稳定的库使用不当也会踩坑。下面是我在多年使用中总结的一些典型问题和解决方法。5.1 链接错误与头文件问题问题编译通过但链接时报错undefined reference toJson::Value::...。原因这是最常见的问题意味着编译器找到了jsoncpp的头文件声明但链接器没有找到对应的库文件实现。解决检查构建系统确保你的CMakeLists.txt或Makefile正确链接了jsoncpp库。对于源码集成确保所有*.cpp文件尤其是src/lib_json/*.cpp都被加入编译。检查库路径如果使用预编译库确保链接器搜索路径-L包含了库文件所在目录并且使用了正确的链接标志-ljsoncpp。区分调试/发布库在Windows下尤其要注意Debug构建需要链接jsoncpp.lib的Debug版本通常库名会带有d后缀如jsoncppd.lib。问题#include json/json.h报错 “No such file or directory”。解决检查头文件搜索路径-I或CMake中的include_directories是否正确添加了jsoncpp的include目录。如果是通过系统包管理器安装的可能需要安装libjsoncpp-dev或jsoncpp-devel这样的开发包它才会包含头文件。5.2 解析失败格式错误与严格性问题parse函数返回false错误信息errs提示诸如* Line 1, Column 10 Missing ‘,‘ or ‘}‘。原因JSON格式不符合规范。常见错误尾随逗号如{a:1,}单引号代替双引号{‘a‘:1}数字格式错误或含有无法识别的转义字符。解决使用在线JSON格式化工具验证将你的JSON字符串粘贴到在线验证器如 jsonlint.com中快速定位语法错误。调整解析器设置Json::CharReaderBuilder的settings_可以控制严格性。Json::CharReaderBuilder builder; builder.settings_[allowTrailingCommas] true; // 允许尾随逗号 builder.settings_[allowComments] true; // 允许注释 builder.settings_[strictRoot] false; // 对根元素类型要求更宽松 builder.settings_[collectComments] false; // 不收集注释信息提升性能根据你的数据源灵活调整这些设置。如果数据来自严格遵循标准的外部API就保持严格模式如果是人工编辑的配置文件可以放宽限制。5.3 类型转换陷阱与默认值问题从Json::Value中提取数字时如果该字段实际是字符串“123”asInt()会成功吗答案会。jsoncpp在类型转换上比较“宽松”。asInt()、asDouble()、asString()等方法会尝试进行合理的转换。例如数字类型的Value可以asString()成其字符串表示。字符串“123”可以asInt()成123。布尔值true可以asInt()成1。如果转换不合理如数组转整数会返回一个默认值0、0.0、空字符串、false等。潜在风险这种隐式转换可能导致难以察觉的逻辑错误。比如你期望一个字段是数组但它实际上是字符串而你直接遍历它程序可能不会立即崩溃但行为是未定义的迭代器可能无效。最佳实践始终进行显式类型检查或者在关键字段上使用isConvertibleTo()方法如果可用或者使用get()方法提供安全的默认值。5.4 内存与性能问题排查问题解析一个几百MB的JSON文件时程序内存占用飙升甚至崩溃。排查检查数据本身一个巨大的JSON数组或深层嵌套的对象树本身就会占用大量内存。jsoncpp的Json::Value是一个树形结构每个节点都有开销。使用流式解析jsoncpp主要是一次性将整个JSON文档加载到内存中构建成树DOM模式。对于巨型文件考虑使用其他支持SAX事件驱动模式的库如RapidJSON的Reader它可以在解析过程中逐步处理数据而不需要将整个文档载入内存。审视需求你真的需要一次性处理整个文件吗能否将大文件拆分成小块或者与服务端协商采用分页查询的方式获取数据使用工具分析在Linux/macOS下可以使用valgrind --toolmassif来分析程序运行时的内存堆快照精确查看是哪个Json::Value结构占用了大量内存。5.5 版本兼容性注意事项jsoncpp维护着良好的向后兼容性但不同大版本间仍有差异。1.y.z (master分支)需要C11支持。这是活跃开发分支。0.y.z为C11之前的编译器提供支持只接受关键的安全修复。API变化虽然核心API稳定但一些辅助类、工厂类如从Json::Reader过渡到Json::CharReaderBuilder在新版本中可能被推荐使用。在升级jsoncpp版本后建议仔细阅读其CHANGELOG.md并用编译器重新构建你的项目关注所有警告信息。一个典型的版本升级问题旧代码中广泛使用的Json::Reader和Json::FastWriter/StyledWriter在新版本中可能被标记为“遗留的”legacy。虽然它们仍然存在以保证兼容但官方推荐使用新的CharReaderBuilder和StreamWriterBuilder。新API更灵活功能也更强大。在编写新代码时应直接使用新的Builder API。6. 实战案例一个简单的配置文件管理器让我们用一个完整的例子来串联所有知识点实现一个程序配置管理器它从JSON文件读取配置允许在内存中修改并写回文件同时要求保留原文件中的注释。// ConfigManager.h #pragma once #include json/json.h #include string #include optional class ConfigManager { public: ConfigManager() default; ~ConfigManager() default; // 从文件加载配置 bool loadFromFile(const std::string filepath); // 保存配置到文件保留格式 bool saveToFile(const std::string filepath) const; // 获取配置项支持默认值 std::optionalint getInt(const std::string key, std::optionalint defaultValue std::nullopt) const; std::optionalstd::string getString(const std::string key, std::optionalstd::string defaultValue std::nullopt) const; // 设置配置项 void setInt(const std::string key, int value); void setString(const std::string key, const std::string value); // 检查键是否存在 bool hasKey(const std::string key) const; private: Json::Value m_root; // 存储配置的JSON根节点 Json::Value m_comments; // 用于存储注释简化示例实际更复杂 std::string m_currentFilePath; }; // ConfigManager.cpp #include ConfigManager.h #include fstream #include sstream #include iostream bool ConfigManager::loadFromFile(const std::string filepath) { std::ifstream ifs(filepath); if (!ifs.is_open()) { std::cerr 无法打开配置文件: filepath std::endl; return false; } Json::CharReaderBuilder readerBuilder; // 配置解析器以允许注释和尾随逗号适应人性化的配置文件 readerBuilder.settings_[allowComments] true; readerBuilder.settings_[allowTrailingCommas] true; readerBuilder.settings_[strictRoot] false; readerBuilder.settings_[collectComments] true; // 尝试收集注释 std::string errs; bool success Json::parseFromStream(readerBuilder, ifs, m_root, errs); ifs.close(); if (!success) { std::cerr 解析JSON配置文件失败: errs std::endl; m_root.clear(); // 清空无效数据 return false; } m_currentFilePath filepath; std::cout 配置文件加载成功: filepath std::endl; return true; } bool ConfigManager::saveToFile(const std::string filepath) const { std::string savePath filepath.empty() ? m_currentFilePath : filepath; if (savePath.empty()) { std::cerr 未指定保存路径且无当前文件路径。 std::endl; return false; } std::ofstream ofs(savePath); if (!ofs.is_open()) { std::cerr 无法创建或写入文件: savePath std::endl; return false; } Json::StreamWriterBuilder writerBuilder; writerBuilder.settings_[indentation] ; // 4空格缩进 writerBuilder.settings_[commentStyle] All; // 尝试输出所有注释 writerBuilder.settings_[enableYAMLCompatibility] false; writerBuilder.settings_[precision] 6; // 浮点数精度 std::unique_ptrJson::StreamWriter writer(writerBuilder.newStreamWriter()); writer-write(m_root, ofs); ofs.close(); std::cout 配置文件保存成功: savePath std::endl; return true; } std::optionalint ConfigManager::getInt(const std::string key, std::optionalint defaultValue) const { // 使用点分路径如 window.width const Json::Value* current m_root; size_t start 0, pos; std::string token; std::string path key; // 简单的路径解析支持嵌套对象访问 while ((pos path.find(., start)) ! std::string::npos) { token path.substr(start, pos - start); if (!current-isObject() || !current-isMember(token)) { return defaultValue; } current ((*current)[token]); start pos 1; } token path.substr(start); if (!current-isObject() || !current-isMember(token)) { return defaultValue; } const Json::Value target (*current)[token]; if (target.isInt() || target.isUInt() || target.isInt64()) { return target.asInt(); } else if (target.isString()) { // 尝试转换字符串为整数宽松处理 try { return std::stoi(target.asString()); } catch (...) { return defaultValue; } } return defaultValue; } // getString, setInt, setString, hasKey 的实现类似篇幅所限省略... // 它们会处理路径解析、类型检查和赋值。 // main.cpp 示例用法 #include ConfigManager.h #include iostream int main() { ConfigManager config; if (!config.loadFromFile(app_config.json)) { std::cerr 加载默认配置失败创建新配置。 std::endl; // 可以在这里初始化一些默认配置 config.setString(app.name, MyAwesomeApp); config.setInt(app.version, 1); config.setInt(window.width, 800); config.setInt(window.height, 600); config.saveToFile(app_config.json); } // 读取配置 auto width config.getInt(window.width, 1024); // 提供默认值 auto height config.getInt(window.height, 768); auto title config.getString(window.title, Default Title); std::cout 窗口配置: *width x *height , 标题: *title std::endl; // 修改并保存 config.setInt(window.width, 1920); config.setString(window.title, New Awesome Title); if (config.saveToFile()) { std::cout 配置已更新并保存。 std::endl; } return 0; }这个案例展示了如何用jsoncpp构建一个实用的配置管理模块。它处理了文件I/O、错误处理、带默认值的类型安全访问、嵌套键的路径解析并尝试保留注释。在实际项目中你可能还需要增加更多的功能比如配置变更监听、热重载、数组操作等但核心原理都基于对Json::Value的熟练操作。最后关于jsoncpp我的体会是它就像C世界里的瑞士军刀不是最锋利的也不是功能最花哨的但绝对是最可靠、最顺手的那一把。在项目技术选型时面对那些追求极致性能或极致语法糖的库问问自己我的项目真的需要那些特性吗还是更需要一个稳定、省心、十年后还能毫无障碍编译运行的依赖很多时候答案会指向jsoncpp。

相关新闻

最新新闻

日新闻

周新闻

月新闻