news 2026/9/29 17:10:35

VS2013 + C++ 集成 jsoncpp:源码编译、配置与解析实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
VS2013 + C++ 集成 jsoncpp:源码编译、配置与解析实战

简介:一套面向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 还是 CharReader

jsoncpp 的版本分水岭是 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,对 C++11 支持不完整,但 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 甚至会用sscanf,VS2013 对这些函数的安全检查很烦人。加这个宏是最省事的做法,不要为了它去改源码。

第三个配置是运行库。如果你直接把 cpp 文件加入到了 exe 工程,那不用额外管,运行库跟随 exe 的设置。但如果你做成静态库,交付给别人用,就必须确认配置属性 -> C/C++ -> 代码生成 -> 运行库这一项和最终 exe 一致。Debug 用/MDd,Release 用/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_ptr<Json::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::Value,asString会根据实际类型做“尽力转换”。如果字段缺失,用默认值;如果字段类型不对,结果可能不符合预期,所以对于关键配置还需要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::Value&:

const 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 现象:链接时报 LNK2038:RuntimeLibrary 不匹配

原因:用了别人预编译的 jsoncpp 库,或者你自己编译库时没和调用工程统一运行库。VS2013 有四个运行库选项,Debug / Release 各两个,/MD和/MT必须一致。

解决:把 jsoncpp 作为源码加入工程最保险;一定要用库,就统一为/MD(Release),并在库工程和调用工程属性里核对。另外注意 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 的控制台窗口默认用代码页 936(GBK)显示,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_ptr<Json::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()在 C++11 中返回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和原始片段写进日志。这样即使运维改了配置也能快速定位,不用半夜翻日志猜。希望你养成“先验类型、再取默认值”的习惯,能少踩很多坑。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 17:10:18

InfiniteTalk:用稀疏帧采样实现低成本长时程数字人视频生成

做AI视频生成的人应该都有同感&#xff1a;生成一段几秒钟的短片容易&#xff0c;真正要做成能长时间对话的数字人视频&#xff0c;难点根本不在“生成”这一步&#xff0c;而在所有你以为理所当然的细节——音频和画面的对齐、长时程的稳定性、计算资源的合理分配。我最近把一…

作者头像 李华
网站建设 2026/9/29 17:09:51

FastExcel实战:搞定复杂表头与百万级数据导出

做导出需求做到快崩溃的时候&#xff0c;我把目光投向了FastExcel。前阵子接了一个月度销售报表导出的需求。拆开一看&#xff0c;三层表头、跨列合并、日期列动态生成、底部还有合计行&#xff0c;加上客户要求保留样式、不能乱码、百万级数据不能OOM。用POI硬写也不是不行&am…

作者头像 李华
网站建设 2026/9/29 17:09:04

生成式AI设计模式第三篇:Agent工具调用与上下文管理实战解析

做生成式AI应用的时间越长&#xff0c;越觉得这东西像搭积木。基础模型是积木本身&#xff0c;但怎么搭、连接处怎么处理、塌了怎么补救&#xff0c;才是决定项目能不能落地的关键。前面两篇我们聊了提示模板的基础模式和RAG检索增强模式&#xff0c;这一篇继续往下走&#xff…

作者头像 李华
网站建设 2026/9/29 17:06:11

QuickBlue:10分钟向导式AI微服务底座安装

1. 项目概述&#xff1a;这不是“一键安装”&#xff0c;而是把AI微服务底座的启动门槛从“工程师”拉回到“会点鼠标的人” “向导式安装——10 分钟从零跑起一套 AI 微服务底座”&#xff0c;这个标题里藏着三个被行业长期忽视却极其关键的痛点&#xff1a; 认知断层、环境…

作者头像 李华
网站建设 2026/9/29 17:04:22

同步相量计算全解析:FFT、窗函数、小波与HHT的Matlab实践

电力系统同步相量计算&#xff0c;说穿了就是实时估算电网中各节点的电压、电流相量——幅值、相角、频率&#xff0c;还有频率变化率&#xff08;ROCOF&#xff09;。这些年做PMU算法&#xff0c;我用过FFT、窗函数法&#xff0c;也被希尔伯特-黄变换和小波变换折腾过不少次。…

作者头像 李华
网站建设 2026/9/29 17:03:21

C++模板undefined reference:声明定义分离的根因与五种解法

1. 你好&#xff0c;undefined reference&#xff1a;一个让新手崩溃、让老手无语的老朋友先说个场景。你刚学模板&#xff0c;写了一个max函数模板&#xff0c;放在max.h里声明&#xff0c;max.cpp里定义&#xff0c;然后main.cpp里调用。编译时三个文件都乖乖通过&#xff0c…

作者头像 李华