简介一套面向VS2013使用者的C JSON解析入门资源基于jsoncpp库实现JSON文件的读取与解析适合需要在Visual Studio中处理JSON数据的开发者参考学习。压缩包内含完整VS2013工程共33个文件涵盖jsoncpp的11个头文件、编译好的lib库、可运行exe、pdb调试文件以及sln/vcxproj等工程配置整体约3.99MB目录结构清晰拿到即可编译运行。包中还附带了demo001_json.cpp示例源码演示如何打开并解析data.json遍历JSON对象的键值对同时给出解析错误处理逻辑方便读者理解Json::Value、CharReaderBuilder等核心接口的调用方式。目前已有1580人学习下载可作为快速上手jsoncpp的实用参考也能直接迁移到实际项目中为进一步处理数组、嵌套对象等复杂结构打下基础。1. 用 vs2013 C 跑通 jsoncpp先把这套组合的老工程场景立起来VS2013 配 C 解析 JSON听起来像远古技术栈但实际搜索热度一直很高因为大量存量 MFC/ATL 工程还在用 VS2013 维护。接手这类工程时需求往往是把一份 JSON 配置文件读进来转成内部的 struct 或 map。jsoncpp 是侵入性最小的选择源码只有三个 cpp不引入新的依赖轻松编进老工程。这篇笔记会带你从复制源码、配置 VS2013 工程到写出能处理嵌套和数组的解析代码并列出编译报错、BOM、中文乱码这些真实发生的坑。适合正在维护老工程的开发也适合不得不按指定 VS 版本完成课程设计的同学。2. 在 VS2013 里集成 jsoncpp源码编译与工程配置的靠谱路径2.1 先确定要用的 jsoncpp 版本直接关系你该写 Reader 还是 CharReaderjsoncpp 的版本分水岭是 0.x 和 1.x。0.x 系列比如 0.5.0、0.6.0、0.7.0是很多老工程里已经在用的版本接口只有Json::Reader、Json::Value、Json::FastWriter。1.x 系列从 2015 年之后持续更新增加了Json::CharReaderBuilder、StreamWriterBuilder还改了一些错误处理行为。VS2013 是 MSVC 12.0对 C11 支持不完整但 jsoncpp 1.x 的源码基本只用了std::unique_ptr和std::move实测可以编过。区别在于如果你拿到手的源码包里同时有reader.h和charreader.h说明是 1.x只有reader.h那是 0.x。网上搜“jsoncpp库下载”会出来一堆打包好的 DLL 和 lib但我不建议直接下那种二进制。原因很简单你根本不知道它的运行库配置是什么VS2013 的 Debug/Release、/MT//MD 组合一不对就是 LNK2038。最可控的是去官方仓库下载源码包或者从现有老工程里拷一份json目录和lib_json目录。2.2 把源码文件拖进 VS2013 工程一个文件都不能少jsoncpp 的源码结构很固定只要确认这几个文件都进了工程编译就不会缺符号jsoncpp-src/ ├── include/ │ └── json/ │ ├── json.h // 总入口一般都只 include 它 │ ├── json_forwards.h // 前置声明 │ ├── reader.h // Reader 和 CharReader 声明 │ ├── value.h │ └── writer.h └── src/ └── lib_json/ ├── json_reader.cpp ├── json_value.cpp ├── json_writer.cpp ├── json_tool.h // 内部工具非顶层头文件 └── json_assert.h在 VS2013 里新建一个“Win32 控制台应用程序”向导走到“应用程序设置”时勾选“空项目”。然后把上面include/json和src/lib_json里的文件全部拖进解决方案资源管理器。注意json_tool.h和json_assert.h虽然在lib_json下但json_reader.cpp会通过相对路径#include json_tool.h引用所以这两个头文件也必须放在和 cpp 同一级目录不要移到 include/json 里去。如果你是从别的工程拷贝经常会出现只拷贝了json_reader.cpp、忘了拷贝json_writer.cpp这时链接错误清一色是Json::FastWriter::write未解析。其实三个 cpp 是一个整体value 负责存储reader 负责解析writer 负责输出。只加其中两个编译能过链接必败。2.3 三个 VS2013 编译配置包含目录、预处理宏、运行库第一个配置是附加包含目录。因为代码里写的是#include json/json.h所以附加目录应该指向include目录的上一级。项目菜单里依次打开项目属性 - 配置属性 - C/C - 常规 - 附加包含目录输入$(ProjectDir)third_party\jsoncpp\include注意这里third_party\jsoncpp\include是我建议你放置源码的位置实际以你目录为准。如果你把源码放在D:\libs\jsoncpp就填D:\libs\jsoncpp\include。第二个配置是预处理宏。在“预处理”里加上_CRT_SECURE_NO_WARNINGS。如果不加编译json_reader.cpp时必报 C4996因为源码里用了strcpy、sprintf。旧版 jsoncpp 甚至会用sscanfVS2013 对这些函数的安全检查很烦人。加这个宏是最省事的做法不要为了它去改源码。第三个配置是运行库。如果你直接把 cpp 文件加入到了 exe 工程那不用额外管运行库跟随 exe 的设置。但如果你做成静态库交付给别人用就必须确认配置属性 - C/C - 代码生成 - 运行库这一项和最终 exe 一致。Debug 用/MDdRelease 用/MD。如果库用/MT而 exe 用/MD链接时必然报 LNK2038。我在仓库里放 jsoncpp 静态库时会把库的配置和调用方约定为同一个 SLN 下的不同项目这样不会记错。2.4 一个简单的验证编译用 writer 反写测试集成成功加完文件后先写最小验证代码不要写解析先写写出writer因为 writer 不依赖输入文件能最快验证链接是否完整#include json/json.h #include iostream int main() { Json::Value obj; obj[test] 123; Json::FastWriter writer; std::string output writer.write(obj); std::cout output std::endl; return 0; }如果能编译运行并输出{test:123}以及换行说明 jsoncpp 的链接没问题。如果这里就报未解析的外部符号回去检查 2.2 节文件清单。这一步通过了再进入文件解析阶段可以避免把集成问题和解析问题混在一起。3. 把 JSON 文件喂给 jsoncpp最小可运行代码与 Reader/CharReader 的选择3.1 准备一个不折腾的 test.json 文件在工程目录下新建test.json内容用 ASCII先避开中文编码的坑{ app: demo, version: 2, enabled: true, server: {host: 127.0.0.1, port: 8080} }如果 JSON 文件是你手工写的记得保存成 UTF-8 编码。记事本另存为时默认带 BOM后面会有影响这里建议用 VS2013 的文件菜单“高级保存选项”选择“UTF-8 无签名”。如果没有这个选项就用 Notepad 之类的工具另存为 UTF-8 无 BOM。3.2 用 Json::Reader 从文件流解析最直观的老接口#include json/json.h #include fstream #include iostream bool parseFileByReader(const std::string path, Json::Value root) { std::ifstream ifs(path.c_str(), std::ios::binary); if (!ifs.is_open()) { std::cerr open file error: path std::endl; return false; } Json::Reader reader; bool ok reader.parse(ifs, root, false); if (!ok) { std::cerr parse error: reader.getFormattedErrorMessages() std::endl; return false; } return true; } int main() { Json::Value root; if (!parseFileByReader(test.json, root)) { return -1; } std::cout root[app].asString() std::endl; std::cout root[version].asInt() std::endl; std::cout root[server][port].asInt() std::endl; return 0; }Json::Reader::parse有多个重载这里传的是std::istream。第三个参数collectComments设为 false表示不收集 JSON 里的注释如果你的配置里有//这种注释旧版 jsoncpp 默认是允许的设置 false 后遇到注释会报错。JSON 标准不允许注释但很多配置文件会写。如果你需要兼容带注释的配置这里要传 true。getFormattedErrorMessages()返回的字符串包含行号和列号比如* Line 3, Column 1这是排查问题最重要的信息。注意ifs使用二进制模式打开是为了避免 Windows 下文本模式把\r\n转成\n。虽然 JSON 允许\r\n但转换后行号会偏移排错时对不上文件原文。3.3 用 CharReader 解析字符串新版接口的推荐写法如果你的 jsoncpp 是 1.x强烈建议用Json::CharReaderBuilder它的解析状态更可控错误信息也更详细。没有Json::Reader那种全局的collectComments开关而是通过builder的成员变量配置。代码#include json/json.h #include fstream #include sstream #include memory bool parseContentByCharReader(const std::string content, Json::Value root) { Json::CharReaderBuilder builder; builder[collectComments] false; std::unique_ptrJson::CharReader reader(builder.newCharReader()); std::string errs; bool ok reader-parse(content.data(), content.data() content.size(), root, errs); if (!ok) { std::cerr parse error: errs std::endl; return false; } return true; } int main() { std::ifstream ifs(test.json, std::ios::binary); std::stringstream ss; ss ifs.rdbuf(); std::string content ss.str(); Json::Value root; if (!parseContentByCharReader(content, root)) { return -1; } std::cout root[enabled].asBool() std::endl; return 0; }builder.newCharReader()返回一个Json::CharReader*用std::unique_ptr接管自动释放。parse接收输入字符串的起止指针所以必须确保content在reader-parse调用期间存在。这里用std::stringstream一次性把文件读进内存对配置文件通常是几百 KB完全没压力如果你的文件是几十 MB可以改用mmap或分块读但 jsoncpp 本身也需要完整 JSON 结构分块不适合。builder[collectComments] false;这一句是设置解析属性。1.x 的CharReaderBuilder默认是 false但还是建议显式写出来因为你的同事不一定清楚默认值。通过errs拿到错误消息后最好连带打印文件名和输入长度后面排查有用。3.4 取值安全asString 和 asInt 之前要先判断类型jsoncpp 的Json::Value是一个“万能类型”root[app].asString()在app是字符串时正常返回如果app不存在或值是数字asString()的行为是返回空字符串但asInt()对非整数类型会返回 0 或触发断言取决于版本。所以我总是这样写const std::string app root.get(app, unknown).asString(); int version root.get(version, 0).asInt(); bool enabled root.get(enabled, false).asBool();get的第二参数是Json::ValueasString会根据实际类型做“尽力转换”。如果字段缺失用默认值如果字段类型不对结果可能不符合预期所以对于关键配置还需要isString()、isInt()检查。比如if (!root[version].isInt()) { std::cerr version must be integer std::endl; return -1; }为什么这么啰嗦JSON 文件经常是人工手改的写成version: 2.0太常见了。isInt()检查能拦住一半手误。这是我在生产环境排错排多了的体会宁可解析时多说几句不要运行时再炸。4. 解析 JSON 数组和嵌套对象读取配置的索引、迭代与默认值4.1 用 const 引用遍历数组避免深拷贝和意外插入JSON 数组在 jsoncpp 里是Json::ArrayIndex类型下标。最常见的遍历方式有两种for (unsigned int i 0; i arr.size(); i)和迭代器。我建议用下标因为下标操作直观而且arr[i]在arr是 const 引用时是只读访问不会触发修改。如果写成非 const 的Json::Value arr root[items];那么arr[5]访问越界时jsoncpp 会默默把数组扩到大小 6并插入 null 值这样arr.size()变成了 6循环会多跑一次。这个问题很隐蔽日志里看不出是数据问题还是代码问题。解决办法就是全部使用const Json::Valueconst Json::Value items root[items]; if (!items.isArray()) { std::cerr items is not array std::endl; return; } for (unsigned int i 0; i items.size(); i) { const Json::Value item items[i]; std::string name item.get(name, ).asString(); int value item.get(value, 0).asInt(); std::cout i : name name , value value std::endl; }注意items[i]返回的是const Json::Value这需要items本身是 const。如果你是从root[items]直接取的root也建议声明为const Json::Value。一旦养成 const 习惯operator[]的副作用就与你无关。4.2 嵌套对象取值链逐层 isMember不要在中间断掉JSON 配置里经常长这样{ database: { connection: { host: localhost, ports: [3306, 3307] } } }如果直接写root[database][connection][host]当database缺失时root[database]是一个空 Value再往下[connection]也是空 Value直到[host]也是空 Value最终asString()返回空字符串。这看起来没崩溃但掩盖了一个事实配置缺失和配置为空字符串无法区分。正确做法是逐层判断bool loadDatabaseHost(const Json::Value root, std::string* host) { if (!root.isMember(database)) { std::cerr missing database section std::endl; return false; } const Json::Value db root[database]; if (!db.isMember(connection)) { std::cerr missing database.connection std::endl; return false; } const Json::Value conn db[connection]; if (!conn.isMember(host) || !conn[host].isString()) { std::cerr missing or invalid database.connection.host std::endl; return false; } *host conn[host].asString(); return true; }这里每层都打印具体缺失路径比最后笼统报一个“解析失败”好得多。尤其是别人接手你的工程时一眼能看出数据格式差了什么。4.3 数组元素里的嵌套对象用两重循环不迷路当数组元素又是对象时我的习惯是先取数组再在循环里取元素对象然后继续取字段不要写成链式一长条。链式不仅难调试而且一旦中间某个元素结构不一致错误信息无法定位到第几个元素。参考const Json::Value servers root[servers]; if (!servers.isArray()) return; for (unsigned int i 0; i servers.size(); i) { const Json::Value server servers[i]; if (!server.isObject()) { std::cerr servers[ i ] is not object std::endl; continue; } std::string ip server.get(ip, ).asString(); int port server.get(port, 0).asInt(); // 使用 ip 和 port }这里server.isObject()判断很重要因为 JSON 数组允许混合类型servers[0]是对象servers[1]可能是字符串。如果对字符串调用get会得到默认值但不会崩所以严格校验还是需要。5. 常见避坑VS2013 下 jsoncpp 编译解析的 5 个真实事故在 VS2013 和 jsoncpp 的组合里编译阶段踩坑的概率比解析阶段高得多而且报错信息往往不直接指向问题根源。下面五条是我在实际维护中遇到频率最高的每一条都按现象、原因、解决的顺序记录方便你对照排错。5.1 现象C4996 一堆警告开了“将警告视为错误”就编不过原因VS2013 默认启用安全函数检查SDL 相关jsoncpp 源码里用了strcpy、sprintf、sscanf等 CRT 函数编译器把这些标记为弃用。解决在预处理定义里加_CRT_SECURE_NO_WARNINGS。如果不想动全局宏可以在json_reader.cpp前加一行#define _CRT_SECURE_NO_WARNINGS但这样要改源码后续更新麻烦。还有一个做法是关闭项目属性里的“SDL 检查”但那是全局安全设置不建议。注意这个宏要加在所有 jsoncpp 相关文件被编译之前最好放在工程级预处理定义里而不是某个头文件里。5.2 现象链接时报 LNK2038RuntimeLibrary 不匹配原因用了别人预编译的 jsoncpp 库或者你自己编译库时没和调用工程统一运行库。VS2013 有四个运行库选项Debug / Release 各两个/MD和/MT必须一致。解决把 jsoncpp 作为源码加入工程最保险一定要用库就统一为/MDRelease并在库工程和调用工程属性里核对。另外注意 64 位和 32 位也不能混用x64的 lib 不能链接到win32工程。还有一个容易误判的是工具集版本用 VS2013 编译的 lib 不能用在 VS2010 的工程里除非源码兼容。5.3 现象解析带 BOM 的 UTF-8 文件时报Line 1, Column 1: Bad Value原因UTF-8 BOM 是三个字节EF BB BF文件被解析时这三个字节被当作第一个 token。jsoncpp 不认 BOM于是报错。解决读文件后手动去掉 BOM代码见第 3 章的stripBOM。我遇到的一个隐蔽情况是先用了std::ifstream文本模式BOM 被当字符读进来然后content.size()变大了 3但字符串看起来没区别。所以统一用二进制模式 手动去 BOM。如果你用Json::Reader直接解析流可以用ifs.seekg(3, std::ios::beg)跳过但前提是文件确实带 BOM不适合所有场景。5.4 现象解析成功但打印中文乱码把结果写到日志文件反而正常原因jsoncpp 内部只处理 UTF-8 字节流asString()返回的是 UTF-8 编码的std::string。VS2013 的控制台窗口默认用代码页 936GBK显示UTF-8 字节直接打印当然乱。这并不是玄学是编码转换问题。解决命令行程序可以先SetConsoleOutputCP(65001);但 VS2013 的 MSVC 运行库对这个支持不稳定有些机器还是乱。更靠谱的是把结果写到std::ofstream日志文件用 Notepad 或 VS Code 打开或者在 GUI 程序里用MultiByteToWideChar(CP_UTF8, ...)转成 UTF-16 再用MessageBoxW显示。优先级日志文件 转码 改控制台代码页。5.5 现象root[key]明明不存在代码却不报错最后数据全错原因jsoncpp 的operator[]对不存在的 key 会返回一个默认构造的空 Value对对象类型它还会插入 key 使对象越来越大对数组类型越界会扩展数组。这是设计行为不是 bug。解决判断存在用isMember或get不要用root[key].isNull()来判断因为root[key]本身就会触发插入。如果你只是想读取而不修改永远用const Json::Value绑定整个root这样operator[]不会修改数据。没有后悔药只能养成命名常量的习惯比如const Json::Value cfg root;一进函数就绑定。6. 进阶把 jsoncpp 解析封装成可复用模块并验证耗时与错误定位6.1 一个带 BOM 处理和错误上下文的加载函数把前面几章的坑集中收口封装成一个 20 行的函数#include json/json.h #include fstream #include sstream #include memory static std::string stripBOM(const std::string s) { return (s.size() 3 (unsigned char)s[0] 0xEF (unsigned char)s[1] 0xBB (unsigned char)s[2] 0xBF) ? s.substr(3) : s; } bool loadJsonFile(const std::string path, Json::Value out) { std::ifstream ifs(path.c_str(), std::ios::binary); if (!ifs.is_open()) return false; std::stringstream ss; ss ifs.rdbuf(); std::string content stripBOM(ss.str()); Json::CharReaderBuilder builder; std::unique_ptrJson::CharReader reader(builder.newCharReader()); std::string errs; if (!reader-parse(content.data(), content.data() content.size(), out, errs)) { fprintf(stderr, parse failed at %s, err%s, sample%.100s\n, path.c_str(), errs.c_str(), content.c_str()); return false; } return true; }错误日志里带上path和原始内容前 100 个字符排查线上配置问题很有用。content.data()在 C11 中返回const char*VS2013 支持。6.2 验证解析性能的简单做法如果你要解析的 JSON 很大可以用std::chrono包住loadJsonFile测耗时。jsoncpp 的解析速度在同类型库中不占优但对 KB 级配置无所谓。如果发现耗时异常优先检查是不是在循环里反复用了深拷贝赋值。我自己的习惯是把loadJsonFile放在一个名为JsonUtil的 namespace 里所有项目共用。这样如果以后要把Json::Value替换成rapidjson::Document只需要改这一个文件上层业务代码完全不动。6.3 收尾不要相信“默认值”我的血泪经验是默认值能掩盖结构变化。比如root.get(timeout, 30)当配置方把timeout写成30s时asInt()返回 0这不是你想要的结果。所以我在封装之外还会写一个校验函数检查关键字段的类型并把errs和原始片段写进日志。这样即使运维改了配置也能快速定位不用半夜翻日志猜。希望你养成“先验类型、再取默认值”的习惯能少踩很多坑。希望帮到你。本文还有配套的精品资源点击获取