尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

VS2013中用C++和jsoncpp解析JSON文件:配置、读取与避坑指南

VS2013中用C++和jsoncpp解析JSON文件:配置、读取与避坑指南 简介面向Visual Studio 2013环境下的C开发者这份资源演示了如何集成jsoncpp库并完成JSON文件的读取与解析。内容涵盖库的配置方式、工程搭建要点以及一个可运行的解析示例适合需要在C项目中引入JSON处理能力的初中级开发者参考。压缩包共33个文件包含11个头文件与1个cpp源文件可用于查看接口定义与示例实现同时附带编译生成的lib、exe、pdb等文件便于直接对照运行效果或二次编译。另有sln/vcxproj等VS工程文件可帮助快速还原项目结构。包体整体约3.99MB内容精炼。目前已有1580人学习下载。资源价值在于通过实际工程文件展示jsoncpp在VS2013中的完整配置路径省去自行搜索头文件和库依赖的麻烦读者既能运行现成程序观察解析效果也能按需修改代码以适配自己的JSON数据从而快速掌握C解析JSON的基本流程。1. VS2013 里用 C 解析 json 文件jsoncpp 是最省事的老工程方案VS2013、C、jsoncpp这几个词凑一起通常意味着手里有个不能升级编译器的老工程却必须把 json 文件干净利落地读进来。jsoncpp 是这类场景里少有的能把源码直接拖进工程、不引入额外运行时依赖的解析库但在网上搜到的教程大多停留在旧版 API照抄就翻车。下面这套流程按我自己的工程习惯整理从版本选择、工程配置到读文件、取值、写回和验证全程用 VS2013 能编过的写法适合接手遗留代码的开发者也适合 C 入门阶段被过时资料带偏的新手。2. 选对 jsoncpp 版本并把库接进 VS2013新旧 API 差异与两种配置路径接 jsoncpp 之前先得把版本这关过了。这个库从 0.x 走到 1.xAPI 形态变化很大网上大量教程、博客和 PDF 课件还停留在 0.x 时代。如果你按老教程的写法去配新源码轻则编译告警重则行为不一致排查起来非常费劲。2.1 Reader 和 CharReaderBuilderjsoncpp 0.x 与 1.x 的分水岭jsoncpp 0.x 时代的标准解析姿势是Json::ReaderJson::Reader reader; Json::Value root; std::string jsonStr ...; bool ok reader.parse(jsonStr, root);这段代码在 1.x 里其实还能编过但它已经是官方不推荐的旧接口。1.x 推荐的做法是Json::CharReaderBuilder。两种写法的核心区别在于解析器对象的构造方式Reader直接实例化CharReaderBuilder则负责按配置构建一个解析器实例顺带把重复键、注释、特殊浮点数这些行为开关都统一到了配置项里。对比项Json::Reader0.x 风格Json::CharReaderBuilder1.x 风格解析入口reader.parse(string, root)parseFromStream(builder, stream, root, errs)错误信息返回 bool细节有限可通过 errs 拿到行号和列号配置能力需要单独构造 Featuresbuilder[rejectDupKeys] true 这类键值配置对 VS2013老源码包会触发 C4996 警告1.x 源码基本无 C 风格字符串函数适用场景只读简单 json配置解析、格式校验、二次开发这里有个现实问题很多转载站提供的 jsoncpp 库下载包还是 0.5.0 这种老版本压缩包里除了源码还混着旧版工程文件。VS2013 打开以后编译json_value.cpp会冒出一堆strcpy、sprintf相关的 C4996。这不是你代码的问题是库本身太老。所以第一步就是定版本能用 1.x 就不用 0.x。2.2 把 jsoncpp 源码直接拖进工程四步配置与最小编译验证常见做法是源码直接进工程不编 lib、不配链接路径省掉一堆运行时库匹配问题。1.x 的源码包解压后通常包含include/json头文件目录和src/lib_json实现目录有些发布包还会在 dist 目录下提供一个合并好的单文件jsoncpp.cpp那种更省事。操作步骤把src/lib_json下的json_reader.cpp、json_value.cpp、json_writer.cpp通过「右键项目 → 添加 → 现有项」加进工程如果有合并好的jsoncpp.cpp只加这一个文件。项目属性 → C/C → 常规 → 附加包含目录指向源码包里的include目录。写一段最小代码验证头文件路径和源文件是否都接上了。编译运行能打印出版本号就说明工程配置完成。#include json/json.h #include iostream int main() { // jsoncpp 在 json/version.h 里定义了这个宏 std::cout JSONCPP_VERSION_STRING std::endl; return 0; }这段代码的作用是验证两件事第一附加包含目录配对了没有json/json.h能不能被找到第二实现文件有没有真正参与编译链接如果只配了头文件路径而没把.cpp加进工程链接时会报一堆Json::Value::...无法解析的外部符号。打印出的版本号形如 1.9.x说明当前工程实际用的就是新接口那一套。提示不要从转载站下载来路不明的 jsoncpp 库包版本混杂、目录结构被改过是大量编译报错的源头。优先走官方发布渠道的源码包或者直接用 VS2013 里的 NuGet。2.3 不想手动配置就上 NuGetVS2013 里搜 jsoncpp 包VS2013 自带 NuGet 包管理器省去手动添加源文件的步骤。菜单里打开「工具 → NuGet 包管理器 → 管理解决方案的 NuGet 程序包」联机搜索 jsoncpp选到包以后点安装引用会自动出现在项目里include 路径也被配好。NuGet 方式的好处是版本统一、升级方便团队里其他人拿到工程后还原包即可。坏处是 VS2013 自带的 NuGet 版本较老某些新包的依赖解析策略可能不友好而且离线环境下根本装不了。我的建议是单机开发或离线环境用源码进工程团队协作且网络稳定就用 NuGet。两种方式接进来的库本体是同一份源码使用代码完全一致后面讲的解析写法不受影响。2.4 字符集、运行库和 C4996三项影响编译结果的工程设置源码加进来以后还有三项工程设置值得提前确认省得编译期来回折腾。第一项是字符集。项目属性 → 常规 → 字符集里默认可能是「使用 Unicode 字符集」。jsoncpp 本身只处理 UTF-8 字节流和字符集设置没有直接关系但如果后面要混用 Win32 API 或读写中文路径建议改成「使用多字节字符集」减少宽窄字符转换带来的麻烦。第二项是运行库。源码进工程的方式下jsoncpp 会跟随当前工程的运行库设置Debug 默认/MTdRelease 默认/MT。如果项目里还引用了其他预编译库务必让所有库的 CRT 运行库一致否则链接阶段会出现_ITERATOR_DEBUG_LEVEL不匹配之类的错误。第三项是_CRT_SECURE_NO_WARNINGS。1.x 源码已经不怎么依赖fopen、sprintf这些函数但如果你自己的业务代码里用了VS2013 会报 C4996。项目属性 → C/C → 预处理器 → 预处理器定义里加上_CRT_SECURE_NO_WARNINGS或者干脆把 SDL 检查关掉眼不见心不烦。3. 从磁盘读到内存解析 json 文件的完整代码路径工程配置好以后真正写解析代码时第一个坑往往不在解析本身而在文件读取。读文件这步看着简单玄学往往就藏在这里。最常见的是编码问题你用 Windows 记事本把配置存成 UTF-8文件开头悄悄多了 3 个字节的 BOMjsoncpp 解析器从第一个字节开始期望看到{或[结果撞上EF BB BF直接判失败。3.1 带 BOM 的 UTF-8 文件为什么第一行永远报错BOMByte Order Mark是 Unicode 文本文件开头的不可见标记UTF-8 的 BOM 是十六进制EF BB BF。Windows 记事本的「另存为 UTF-8」会默认带上它而很多 Linux 工具生成的 json 文件不带。jsoncpp 的解析器不认识这个头于是报错的定位信息永远在文件的最开头看起来像* Line 1, Column 1这种毫无头绪的内容。解决办法是在读取阶段就把 BOM 剥离掉。用二进制模式读文件先读前 3 个字节判断是 BOM 就让文件流停在 BOM 之后开始解析不是 BOM 就回到文件开头重新解析。std::ifstream ifs(config.json, std::ios::binary); if (!ifs.is_open()) { // 文件打不开记日志或者直接返回错误 return; } char bom[3] { 0 }; ifs.read(bom, 3); bool hasBom (bom[0] (char)0xEF bom[1] (char)0xBB bom[2] (char)0xBF); if (!hasBom) { ifs.clear(); // 清掉可能的 failbit ifs.seekg(0, std::ios::beg); // 没 BOM 就回到文件头 }这段代码的逻辑分三个分支文件不存在时直接返回有 BOM 时文件流已经自动停在第四个字节后续解析直接从当前位置继续没有 BOM 时先clear()再seekg回到头部。clear()这步不是多余的如果文件本身不足 3 字节read会把流置为 fail 状态不清理的话后面的解析调用会直接无效。3.2 读文件并解析用 parseFromStream 走通最小链路BOM 处理完解析本身反而简单。1.x 风格的最小链路是把std::ifstream直接交给Json::parseFromStream中间不用把整个文件拼成字符串省一次拷贝也省去字符串结束符的边界问题。#include json/json.h #include fstream #include iostream #include string Json::Value loadJsonFile(const std::string path, std::string errs) { std::ifstream ifs(path, std::ios::binary); if (!ifs.is_open()) { errs cannot open file: path; return Json::Value(); } char bom[3] { 0 }; ifs.read(bom, 3); bool hasBom (bom[0] (char)0xEF bom[1] (char)0xBB bom[2] (char)0xBF); if (!hasBom) { ifs.clear(); ifs.seekg(0, std::ios::beg); } Json::CharReaderBuilder builder; builder[rejectDupKeys] true; // 重复键直接报错避免配置被静默覆盖 builder[allowComments] false; // 配置文件里不允许注释更严格 Json::Value root; if (!Json::parseFromStream(builder, ifs, root, errs)) { return Json::Value(); } return root; }几个关键参数值得说明。std::ios::binary确保 Windows 文本模式不会把\r\n转成\nJSON 字符串里的\r才能原样保留。rejectDupKeys设为 true 后同一个对象里出现重复键会直接解析失败这个开关在老项目中特别实用能挡住不少手误拼错配置的低级问题。allowComments默认是允许注释的如果你希望配置文件严格符合 JSON 标准把它关掉。errs是解析失败时最有价值的排查入口。parseFromStream返回 false 时errs 里会带* Line和Column信息直接告诉你在哪一行哪一列出的错。返回的Json::Value()是空值接收方用isNull()判断即可。3.3 isMember 与 getjsoncpp 里最顺手的 json 查询函数组合解析成功后取值的需求通常分为两类确定存在的字段和可能缺失的字段。对第一类直接用root[key]就够对第二类建议用isMember先问一句再用get带默认值兜底。if (root.isMember(server)) { const Json::Value server root[server]; if (server.isObject()) { int timeout server.get(timeout, 30).asInt(); bool retry server.get(retry, false).asBool(); std::string ip server.get(ip, 127.0.0.1).asString(); // 后续业务处理... } }这里的isMember和get组合就是 jsoncpp 里最常用的查询函数搭配。get的第一个参数是键名第二个参数是默认值键不存在时返回默认值存在但类型不对时按目标类型兜底返回。这样写的好处是省掉一长串if else坏处是默认值选错时问题会被静默吞掉。比如字段缺失时你期望取到 30结果拼错默认值传成 0那整个业务逻辑都会跑偏。所以默认值不是随手填的要按配置项的真实语义选。注意get返回的是 const 引用默认值参数也是 const 引用。如果root里没有这个 key返回的默认值引用的生命周期绑定到当前表达式安全但如果要把返回值长期保存到函数外部记得按值拷贝成Json::Value不要一直拿引用。4. 处理 json 数组和嵌套对象类型判断、遍历与深层取值json 文件里单纯的键值对好处理真正让解析代码变复杂的是数组和嵌套对象。jsoncpp 的Json::Value是动态类型容器同一个节点下可能是整数、字符串、数组或另一个对象。如果你不做类型判断直接按猜测的类型取值Debug 下可能触发断言Release 下拿到一个默认值数据错了还没人知道。4.1 取数前先问类型isString、isInt 对应哪把钥匙jsoncpp 为每种类型都提供了对应的isXxx判断和asXxx取值函数。判断先行取值跟上这是解析代码稳定的基础。判断函数取值函数边界说明isString()asString()数字和布尔不会自动转成字符串isBool()asBool()字符串true不算布尔isInt() / isUInt()asInt() / asUInt()浮点 2.0 不会自动截断成 2isArray()size() / 下标访问空数组仍然是数组isObject()isMember() / get()空对象仍然是对象isNull()——空根、缺失节点都返回 true有一个容易踩的边界JSON 里的数字可能是整数也可能是浮点。isInt()只对整数类型的节点返回 true浮点 2.5 会落到isDouble()分支。如果你只是想要一个数值不关心它是整数还是浮点常见写法是这样的const Json::Value count root[count]; if (count.isInt()) { int n count.asInt(); } else if (count.isDouble()) { double d count.asDouble(); }如果把顺序反过来先调asInt()再判断或者连判断都不做遇到浮点节点时行为就是未定义的。Debug 下 jsoncpp 会主动断言Release 下可能返回 0这种「看运气」的取值方式在生产环境迟早出事。4.2 遍历 json 数组范围 for 与下标循环的取舍数组是 JSON 里最常见的容器。jsoncpp 的数组遍历有三种写法实际使用中按场景选一种就够。const Json::Value arr root[items]; if (!arr.isArray()) { // 类型不符合预期提前返回或者记日志 return; } // 写法一范围 for最简洁适合只读场景 for (const auto item : arr) { if (item.isObject()) { std::string name item.get(name, ).asString(); // 处理单个元素 } } // 写法二下标循环适合需要索引位置的场景 for (Json::ArrayIndex i 0; i arr.size(); i) { const Json::Value item arr[i]; // 通过 i 你知道元素在数组里的位置 } // 写法三迭代器适合配合 STL 算法 for (Json::Value::const_iterator it arr.begin(); it ! arr.end(); it) { const Json::Value item *it; }范围 for 依赖 C11 的 range-based for 语法VS2013 对这个特性的支持是完整的放心用。arr.size()返回的类型是Json::ArrayIndex本质上是unsigned int不要拿它和 int 直接比较避免符号转换告警。下标访问越界时jsoncpp 会返回一个空 Value 引用而不是崩溃但你不能依赖这个特性循环前先isArray()判一下才是正路。数组里类型判断更重要。JSON 的数组不像 C 数组那样元素类型必须一致同一个数组里可能既有对象又有数字遍历时每个元素单独判断不要假设第一个元素是对象后面就都是对象。4.3 深层取值三大写法与 operator[] 的隐藏副作用嵌套对象取值时root[a][b][c]这种链式写法最直观但有个隐藏副作用很多人不知道非 const 的operator[]在键不存在时会把这个键插入对象。也就是说你只是想读一个可能不存在的深层字段结果这个字段被写进了原来的 Value 里。如果后面再把 root 写回文件原本干净的 JSON 里凭空多出一堆key: null。深层取值的常见做法有三种。第一种逐层get每层都带默认值const Json::Value server root.get(server, Json::Value()); if (server.isObject()) { std::string ip server.get(ip, 127.0.0.1).asString(); }第二种确认存在后再链式访问适合确定结构不会变的情况if (root.isMember(server) root[server].isMember(ip)) { std::string ip root[server][ip].asString(); }第三种把整段路径封装成一个函数内部用逐层get实现对外只暴露键路径和默认值。这种封装在配置项特别多的老工程里很实用能显著减少重复代码。三种写法里逐层get是最稳的。operator[]链式写法的前提是每一层都已经存在否则中间某个节点不存在下一层的[]调用就会在一个空 Value 上操作。你可以用 const 引用来避免operator[]的插入副作用对const Json::Value对象调用[]时jsoncpp 走的是 const 重载键不存在时不会插入但这样会失去修改能力读写两用的场景要格外小心。5. 避坑VS2013 配 jsoncpp 最常见的五个翻车现场与排查方法这几条是血泪经验攒出来的。每个问题都按「现象 → 原因 → 解决」的顺序写方便你直接对照排查。5.1 翻车一旧博客源码包拖进 VS2013编译报一堆 C4996现象按博客教程把 jsoncpp 0.5.0 的源码文件夹整个拖进工程编译json_value.cpp时冒出十几条 C4996 警告有的直接配成了错误内容全是strcpy、sprintf这类 C 函数被标记为不安全。原因老版 jsoncpp 源码大量使用 C 风格字符串函数VS2013 的默认安全校验把它们的调用视为编译错误。新版 1.x 源码已经清理得差不多但转载站提供的库包不会跟着更新。解决优先换 1.x 源码如果因为某些原因必须留在老库项目属性 → C/C → 预处理器定义里加_CRT_SECURE_NO_WARNINGS同时把 SDL 检查关掉。这两种手段治标不治本能编过但老库本身的解析行为和新版有差异后面 check 代码时很容易踩另一组坑。5.2 翻车二parse 失败且报错位置在 Line 1 Column 1现象json 文件用编辑器打开看完全正常jsoncpp 返回 falseerrs 里的信息指向第一行第一列没有任何具体提示。整个解析过程像个黑匣子不知道错在哪。原因文件带 UTF-8 BOM文件流把EF BB BF三个字节送给了解析器。解析器期望看到{或[结果收到 0xEF直接认定第一个字符非法所以报错位置永远是文件起点。解决用二进制模式读文件先读 3 个字节判断 BOM命中就把文件流留在第四个字节开始解析没命中就seekg(0)回到文件头。具体代码见第 3 章这里不再重复。另外补充一句团队里如果有人用记事本编辑配置这个坑一定会反复出现建议在文件加载函数里统一处理不要再依赖每个人手动转编码。5.3 翻车三asString() 在 Debug 下断言崩溃现象调试运行到root[name].asString()时程序直接崩输出窗口显示 jsoncpp 的断言信息Release 下不崩溃但拿到的字符串是空的或不对。原因name字段在 JSON 里的类型根本不是字符串可能是数字、布尔或 null。asString()对非字符串类型不会自动转换内部走断言分支Debug 下直接引爆Release 下静默返回空串。解决取值前先调isString()判断。如果你能容忍字段缺失用root.get(name, ).asString()这一步就把判断兜住了如果你需要知道字段是否存在先isMember再isString。记住一条原则jsoncpp 的asXxx是「在类型匹配的前提下给你值」不是「把任意类型转成你要的类型」。5.4 翻车四Value 引用悬空解析完的数据随机变脏现象解析完以后把某个节点的引用存进std::vector函数返回后用的时候数据已经变了有时还有崩溃。原因const Json::Value item root[i]拿到的只是 root 内部某个节点的引用。如果 root 在后续代码里被重新赋值、析构或大改引用指向的内存已经失效。解决要长期持有的节点按值拷贝Json::Value copy root[i];。jsoncpp 的Value拷贝是深拷贝性能上有开销但比悬空引用安全得多。只在明确的生命周期内使用引用比如边遍历边处理不要跨函数、跨容器保存引用。这条对新手尤其重要很多人被 C 的引用语义坑一次就记住了。5.5 翻车五查询时不存在的 key 被 operator[] 写进了文件现象程序只是读取 json 配置没有任何修改逻辑但输出文件时发现原本没有的字段出现了值还是 null。原因非 const 的Json::Value调用operator[]访问不存在的 key 时jsoncpp 会自动插入这个 key。你的代码可能写着root[debug][log]查询过程中debug和log就都被创建了。根节点是函数参数传入的非 const 引用时这个副作用会一路带出去。解决只读场景全部用get加默认值或者定义 const 引用再访问。修改场景里明确要给某个 key 赋值时才使用operator[]。如果一定要用链式查询且不能保证中间节点存在就先逐层isObject()判断把道路确认清楚了再走过去。6. 解析只是开始把修改后的 Value 转换回 json 文本并验证结果写完解析和取值很多人的代码就到这了。但如果你的场景是「读配置 → 改配置 → 写回文件」那序列化这一步同样有讲究。6.1 用 StreamWriterBuilder 控制缩进、压缩和 UTF-8 输出jsoncpp 1.x 里写回文件用的是Json::StreamWriterBuilder对应 0.x 时代的StyledWriter。它的好处是所有行为都能通过配置项控制。Json::StreamWriterBuilder wb; wb[indentation] ; // 两个空格缩进json 文件给人看 wb[emitUTF8] true; // 中文字符串按 UTF-8 原样输出 std::unique_ptrJson::StreamWriter writer(wb.newStreamWriter()); std::ofstream ofs(out.json, std::ios::binary); if (ofs.is_open()) { writer-write(root, ofs); ofs std::endl; // 末尾补一个换行避免 Linux 工具告警 }indentation设成空字符串就是压缩模式整个 JSON 变成一行省空间适合接口传输和日志输出设成 是格式化模式适合给人看的配置文件。emitUTF8在高版本 jsoncpp 里默认就是 true这里显式写出来是为了提醒自己VS2013 的控制台默认是 GBK 代码页cout直接打印中文字符串会乱码不要误以为是 jsoncpp 输出错了。6.2 写回后再读一遍用 Value 相等比较给流程上保险写回文件以后最稳的验证方式是再读一遍和内存里的 Value 做一次深比较。jsoncpp 的Json::Value重载了operator会逐层比较节点内容。Json::Value reloaded; std::string errs2; std::ifstream ifs2(out.json, std::ios::binary); // 这里复用前面写的 BOM 处理逻辑 Json::CharReaderBuilder rb; if (Json::parseFromStream(rb, ifs2, reloaded, errs2)) { if (reloaded root) { // 写回成功数据完整 } else { // 序列化和反序列化不一致排查编码或浮点精度问题 } }这个 round-trip 验证是我个人写配置文件落盘前的固定动作。以前有一次在发布脚本里直接改线上配置没做读回验证结果一个浮点精度截断让数值从 1.0 变成了 0.999999线上报错查了半天。后来养成的习惯是改配置后先落盘落盘后马上读回来和内存里的值比对一致了才算完成。这个习惯帮我拦下过好几次低级错误也希望帮到你。本文还有配套的精品资源点击获取
返回列表