简介:JsonCpp是C++中常用的JSON解析与生成库,这份资源提供了不经过编译库、直接将源码嵌入项目的方案,适合需要在Visual Studio等C++环境中快速集成JSON功能的开发者。作者结合实际项目经验,整理出只需复制json目录下的头文件以及src/lib_json目录下4个核心源码文件即可完成接入的做法,省去配置lib或dll的环节。
资源包共38个文件,仅1.11MB,以14个.h头文件和5个.cpp源文件为主,附带Visual Studio工程文件(.sln、.vcproj)、编译中间文件(.obj、.pch)及辅助配置文件,便于对照完整的工程组织方式。已有961人浏览学习,说明这种免编译的源码接入方案对开发者有实际帮助。
对于刚接触JsonCpp或不想折腾库依赖的开发者,这份资源提供了可运行的工程示例和最小文件清单,照着拷贝即可完成集成,减少排错时间,适合作为快速入门的参考模板。
1. JsonCpp源码直用:不碰预编译库,把解析器直接编进工程
很多C++项目——不管是嵌入式设备、桌面客户端,还是某个服务端的插件模块——要解析JSON时的本能反应是去找一个编译好的.a/.lib/.dll链接进来。但真把预编译库放进构建链之后,问题就来了:换一个平台要重新找产物,换一个编译器版本可能还得换库,出了bug想进去看两眼,面前是个黑匣子。而JsonCpp源码直用这件事,恰恰是被低估的一条路:把它的实现文件直接编进你的工程,不经过任何“编译库”环节。这个标题真正的含义就一句话:JsonCpp不是非得做成库才能用,它的源码就是为集成而生的。适合谁?维护多条构建链、受不了二进制依赖、或者想随时在解析器内部打断点的开发者。这篇就按“怎么把源码搬进工程、怎么编、编完哪里会翻车”的顺序讲清楚。
2. 源码包结构:先认清哪些文件必须一起搬走
想绕开编译库,第一步不是写代码,而是弄明白JsonCpp的源码包里到底哪些文件是构建所必需的。很多人在这里就栽了跟头:拷了几个.cpp进工程,编译时报出一堆符号错误,原因就是漏了文件或者把版本混用了。
2.1 三个实现文件各管一块
JsonCpp源码解包后,核心实现集中在src/lib_json/目录,公开头文件在include/json/。抛开测试和构建脚本,真正参与编译的实现文件只有三个:
json_value.cpp:管理Json::Value的存储、类型判定、比较、拷贝与引用计数。Value是整个库的数据心脏,所有解析和序列化最终都落在它身上。json_reader.cpp:文本到Value的解析器。负责把字符串流拆成token,再按递归下降逻辑组装成Value。如果你想看JSON怎么被一步步吃进去,断点打在这个文件里。json_writer.cpp:Value到文本的序列化器。StyledWriter、StreamWriter、FastWriter的实现都在这里。
这三个文件不是互不相关的,而是互相咬合的关系:Value是中间模型,Reader在前端喂数据,Writer在后端吐文本。所以单独拿一个.cpp进工程一定会链接失败——我见过不止一次,有人只拷了json_value.cpp,结果所有解析相关的符号全报“无法解析的外部符号”。
2.2 头文件与两个隐藏依赖
公开头文件的分布也值得看一眼。include/json/json.h是个聚合头,它按需引入value.h、reader.h、writer.h、features.h、allocator.h等子头。你使用时写#include "json/json.h"就够,但前提是编译器能找到json这个目录,也就是把仓库根目录设进include路径,而不是把include/json本身设进去。
真正容易漏的是隐藏依赖。src/lib_json/下通常还有json_tool.h、json_valueiterator.inl这类内部文件:它们不是公开API,但.cpp文件会#include它们。如果你只复制了三个.cpp和include下的头文件,丢掉了这些内部头,编译会在预处理阶段直接失败,报找不到某个.inl文件。
所以“直用源码”的文件清单不是一个模糊概念,而是一份照单全收的名单:
| 路径 | 内容 | 是否必须 |
|---|---|---|
include/json/ | 全部公开头文件 | 必须 |
src/lib_json/json_value.cpp | Value实现 | 必须 |
src/lib_json/json_reader.cpp | 解析器实现 | 必须 |
src/lib_json/json_writer.cpp | 序列化实现 | 必须 |
src/lib_json/*.h/*.inl | 内部工具与模板实现 | 必须 |
src/test_*/ 示例目录 | 测试与demo | 可选,建议不要 |
| CMakeLists.txt 等构建文件 | 构建脚本 | 不需要 |
2.3 版本与来源:别去抓master
版权声明和仓库细节这里不展开,只讲一个实用性建议:拿源码时优先取带release标签的发布版本,不要直接抓默认分支的每日快照。原因很现实:默认分支上可能混着正在开发的破坏性改动,某一天某个.cpp改了接口,你的代码里用的旧接口就全部编译不过。而release版本是经过整体验证的,三个.cpp之间版本也一致。
还有一条容易忽视的原则:版本一致性。三个.cpp、内部头、公开头必须来自同一个版本包。混搭是最难排查的问题之一——因为错误信息可能只是某个符号对不上,实际原因是两个文件来自不同时期的代码。我一般会把整个include/json和src/lib_json两个目录原封不动拷贝进工程的third_party/jsoncpp/下,不做任何“精简”,以此保证版本天然一致。
3. 三种落地方案:从手工搬运到 CMake 源码目标
文件认全了,接下来就是把源码“放进”工程。这里有三条常见路线,按团队规模和构建习惯选一条就行。我逐个讲透。
3.1 方案A:手工添加源文件,适合命令行走天下的人
最直接的做法:把json_value.cpp、json_reader.cpp、json_writer.cpp当成你工程自己的源文件,交给构建系统统一编译。以CMake为例,写法长这样:
add_executable(app src/main.cpp third_party/jsoncpp/src/lib_json/json_value.cpp third_party/jsoncpp/src/lib_json/json_reader.cpp third_party/jsoncpp/src/lib_json/json_writer.cpp ) target_include_directories(app PRIVATE third_party/jsoncpp/ )这里有两个关键点。第一,include路径指向third_party/jsoncpp/,而不是third_party/jsoncpp/include,因为源码和公开头文件都约定以json/xx.h形式include。第二,三个.cpp是平级参与编译的,任何构建系统都会把每个.cpp分别编译成目标文件再链接,json_value.cpp里引用Reader符号不会报错,因为link阶段三个目标文件会互相解析。
如果你不用CMake,纯命令行编译也同样简单:
g++ -std=c++11 -Ithird_party/jsoncpp \ -c third_party/jsoncpp/src/lib_json/json_value.cpp g++ -std=c++11 -Ithird_party/jsoncpp \ -c third_party/jsoncpp/src/lib_json/json_reader.cpp g++ -std=c++11 -Ithird_party/jsoncpp \ -c third_party/jsoncpp/src/lib_json/json_writer.cpp g++ -std=c++11 -Ithird_party/jsoncpp \ src/main.cpp json_value.o json_reader.o json_writer.o -o app逻辑说明:这里没有生成任何.a或.so,三个目标文件和你的main.o直接链接成最终可执行文件。后缀.o只是中间产物,它不是库,这就能保证“不经过编译库”这个前提成立。
方案A的优点是透明、直观,IDE工程里能直接看到三个.cpp节点,双击就能跳进源码。缺点是目录结构多了一层,且每次全量编译都要带这三个文件。对个人项目完全够用,团队项目只要约定好third_party目录的同步方式也没问题。
3.2 方案B:amalgamation 单文件合并,最省心的推荐
如果嫌三个.cpp维护起来啰嗦,JsonCpp源码自带一个合并工具:amalgamate.py。运行它会把全部实现合并成一个大文件——常见的产物是dist/jsoncpp.cpp和dist/json.h两个文件,直接把这两个文件扔进工程,其他什么都不用带。命令大致是:
# 在JsonCpp源码根目录执行 python amalgamate.py跑完以后,你的CMake就退化成了两行:
add_executable(app src/main.cpp third_party/jsoncpp-single/jsoncpp.cpp ) target_include_directories(app PRIVATE third_party/jsoncpp-single/ )逻辑说明:合并后的大.cpp已经把内部头文件内容都嵌进去了,include路径只需要指向json.h所在目录。这相当于把“库”降维成了“两个文件”,跨平台拷贝时少了很多牵挂。缺点也有:单文件体积大了,IDE里打开会有点卡,而且你无法在.cpp目录树里快速区分Reader和Writer的代码边界——当然,跳转功能不受影响。
我个人的习惯是:小工具、Demo、教学项目用方案B,生产级多模块工程用方案A或C。B方案的“无脑感”只适用于构建链极简的场景。
3.3 方案C:CMake 的 add_subdirectory 源码目标
如果团队已经在用CMake管理第三方依赖,还有一个介于中间的做法:通过add_subdirectory把JsonCpp的源码目录挂进构建,然后链接它生成的目标。JsonCpp的CMake配置会暴露一个或多个库目标,常见的链接写法:
add_subdirectory(third_party/jsoncpp ${CMAKE_BINARY_DIR}/jsoncpp) add_executable(app src/main.cpp) target_link_libraries(app PRIVATE jsoncpp_static)注意,链接的这个“目标”不是预先编译好的二进制。CMake底层依然是去编译JsonCpp的三个.cpp,只是编译管理交给了构建系统自己。这个过程没有产生.a/.lib的外部交付物,因此它仍然属于“源码方式”,只不过自动化程度更高。
方案C适合的场景:团队里已经统一用CMake,并且不想手工维护每个.cpp的路径。它的坑在于:JsonCpp的CMakeLists可能会引入测试和文档构建逻辑,你需要通过目标名精确链接,而不是直接链接一个叫jsoncpp的宽松目标,否则可能把不必要的测试代码也带进构建。
三种方案选哪个,用这个表就够了:
| 判断维度 | 方案A(手动加.cpp) | 方案B(单文件合并) | 方案C(add_subdirectory) |
|---|---|---|---|
| 配置成本 | 低 | 最低 | 中 |
| 对构建系统的侵入 | 无 | 无 | 依赖CMake |
| 代码阅读体验 | 好 | 一般 | 好 |
| 跨平台拷贝成本 | 中 | 低 | 低 |
| 适合谁 | 个人/小团队 | 快速集成 | 成熟CMake工程 |
4. 编译选项与平台差异:宏不配好,源码编得过去也跑不对
源码加进工程只解决“能不能编”的问题,真正让JsonCpp适应你的平台,还得靠一组预处理宏。很多人把源码拖进来,编译过了,跑起来却行为异常,十有八九就是缩略了这几个宏。
4.1 关键宏速查表
| 宏 | 作用 | 什么情况下需要 |
|---|---|---|
JSONCPP_NO_INT64 | 禁用64位整数存储,largeInt/largeUInt退化为32位 | 32位MCU、老平台 |
JSONCPP_USE_SECURE_MEMORY | 使用安全内存分配,敏感字符串自动清零 | 网关、鉴权、越权漏洞敏感场景 |
JSONCPP_NO_EXCEPTION | 关闭异常,解析失败靠返回码+错误信息处理 | 嵌入式/开了-fno-exceptions的工程 |
JSONCPP_DLL/JSONCPP_STATIC | Windows下控制导入导出符号 | 在Windows把JsonCpp编成DLL时 |
_CRT_SECURE_NO_WARNINGS | 压制MSVC安全函数警告 | 只要你在VS上编就建议加 |
4.2 三套主流编译命令
以GCC为例,最小可用的编译命令:
g++ -std=c++11 -Ithird_party/jsoncpp \ -DJSONCPP_NO_INT64 \ -c third_party/jsoncpp/src/lib_json/json_reader.cppJSONCPP_NO_INT64是针对单片机场景的。若你的MCU没有64位算术指令,开这个宏后所有64位整数都会用32位存储,解析出来的大数会溢出。要明确的是:这是主动取舍,不是Bug。
Clang和GCC基本一致,只是前缀换一下:
clang++ -std=c++11 -Ithird_party/jsoncpp \ -DJSONCPP_NO_EXCEPTION \ -c third_party/jsoncpp/src/lib_json/json_value.cppWindows + MSVC则要写成:
cl /std:c++17 /Ithird_party/jsoncpp ^ /D_CRT_SECURE_NO_WARNINGS ^ third_party\jsoncpp\src\lib_json\json_value.cpp参数说明:/std:c++17是MSVC指定的C++标准;/D_CRT_SECURE_NO_WARNINGS是给C4996警告的后悔药——不开它,VS会直接报strcpy、sscanf不安全导致编译失败。这个问题在JsonCpp的浮点序列化路径里真实存在,不是你代码写得不对。
4.3 异常开关与断言,别依赖try兜底
JsonCpp默认是抛异常的,parse失败会抛出异常。如果你用了JSONCPP_NO_EXCEPTION,解析失败则不会抛,但你必须在调用侧通过返回值判断错误。常见做法是:
Json::CharReaderBuilder builder; std::unique_ptr<Json::CharReader> reader(builder.newCharReader()); std::string errs; Json::Value root; bool ok = reader->parse(dataStart, dataEnd, &root, &errs); if (!ok) { // 错误信息在errs里,而不是try/catch }还有一个很多人不知道的点:JsonCpp内部有断言宏JSON_ASSERT,它只在NDEBUG未定义时生效。Release构建里它是空操作,所以别指望用它抓运行时错误。生产环境真正兜底的是你自己的错误处理和errs信息。
5. 源码直用避坑:五个按下就出问题的集成现场
把JsonCpp源码直接搬进工程,看着简单,实际操作中有几个故障现场出现频率相当高。这一章不聊理论,全是现象、原因、解决。
5.1 链接报“无法解析的外部符号”:漏了实现文件
现象:链接阶段报LNK2019或undefined reference to Json::Reader::parse(...),但编译本身没任何报错。原因:工程里只放了json_value.cpp,或者放了Reader相关的头文件但没有json_reader.cpp参与编译。解决:把三个.cpp全部加入构建目标。检查方法很简单——在工程里搜一下json_reader.cpp在不在源文件列表里。版本不一致也会触发同样的现象,所以同一次拷贝,别混。
5.2 VS2019/VS2022下报C4996错误
现象:编译json_writer.cpp或json_value.cpp时报C4996,提示strcpy、sscanf等函数不安全。原因:JsonCpp内部为了兼容老编译器,保留了C标准库字符串函数的调用,而现代MSVC把这些警告升级为错误。解决:在工程预处理定义里加上_CRT_SECURE_NO_WARNINGS,或者在你的公共头文件顶部:
#ifdef _MSC_VER #define _CRT_SECURE_NO_WARNINGS #endif注意:这个宏必须在包含JsonCpp任何头文件之前生效,否则预处理器已经处理完声明,警告依然存在。
5.3 中文内容乱码或parse失败:UTF-8 BOM在捣乱
现象:解析一个带UTF-8 BOM的JSON文件,第一次parse失败,错误信息指向开头,文件在记事本里打开却一切正常。原因:Reader按无BOM字节流解析,BOM的三个字节EF BB BF会被当成非法字符。解决:读文件后手动剔除,如下:
std::string content = fileContents; // 从文件读出的原始内容 const char* p = content.c_str(); size_t len = content.size(); if (len >= 3 && (unsigned char)p[0] == 0xEF && (unsigned char)p[1] == 0xBB && (unsigned char)p[2] == 0xBF) { p += 3; len -= 3; } std::string errs; Json::Value root; Json::CharReaderBuilder builder; std::unique_ptr<Json::CharReader> reader(builder.newCharReader()); bool ok = reader->parse(p, p + len, &root, &errs);参数说明:这里只用memmove或指针偏移,不产出一个新字符串,避免大文件场景下多一次全量拷贝。我这个习惯是从某跨平台系统的日志解析模块里养成的,那个项目一半的parse报错都来自BOM。
5.4 与另一个JSON库的命名空间冲突
现象:项目里原本有一个JSON库,两个都声明了namespace json或namespace Json,链接期出现大量重定义,或者其中一个库的函数被另一个干扰。原因:C++没有模块隔离,两个全局命名空间同名,必然拌嘴。解决:最稳的是“PImpl隔离”——在你的公共头文件里完全不暴露JsonCpp类型,只在.cpp里使用:
// my_config.h class MyConfig { public: MyConfig(); ~MyConfig(); bool load(const std::string& content); private: struct Impl; std::unique_ptr<Impl> impl_; }; // my_config.cpp #include "my_config.h" #include "json/json.h" struct MyConfig::Impl { Json::Value root; }; bool MyConfig::load(const std::string& content) { impl_->root = Json::Value(); // 解析... return true; }逻辑说明:PImpl让JsonCpp的头文件不再渗入你的公共头文件,客户端代码永远看不到Json::Value,两个库只在一个翻译单元里碰面。代价是多一次指针间接访问,但对配置类对象来说完全可以忽略。
5.5 多线程偶发崩溃:Value被跨线程传递
现象:程序线上偶发段错误,崩溃位置在json_value.cpp的引用计数自增减处。原因:Json::Value的引用计数不是原子的,一个Value被复制后按值传进另一个线程,两个线程同时修改同一份对象,计数就踩踏了。解决:不要按值跨线程传递Value;只传const Json::Value&,或者干脆让每个线程自己解析独立的字符串。解析器(CharReader)本身可以跨线程复用,真正不能共享的是同一个Value实例的写操作。
6. 读多写少的配置场景:三个值钱的小技巧
如果JsonCpp主要拿来读配置,恭喜你,这个场景最容易榨出性能。以下三个技巧,是从一个配置驱动的模拟项目X里沉淀下来的。
第一个技巧:读键永远用const Value&+find,不要用非const的operator[]。JsonCpp的非const版本operator[]在找不到键时会插入一个null Value,这个行为在循环里非常毒。某次我在遍历一个两层结构时,只读了两个键,整个Value的内存涨了三倍——因为每次缺键都往里塞了一个null节点。正确姿势:
const Json::Value& config = root["server"]; std::string host = config.get("host", "127.0.0.1").asString();用get带默认值,或者先find判断再取,都行。切记:对只读对象,const Value&就能在编译期挡住一半的误用。
第二个技巧:复用解析器和Writer,别每次parse都重新构造。CharReaderBuilder和StreamWriterBuilder的构造有成本,它们会初始化特性集。在循环解析批量JSON时,把builder提取到循环外:
Json::CharReaderBuilder builder; std::unique_ptr<Json::CharReader> reader(builder.newCharReader()); for (const auto& line : lines) { Json::Value v; std::string errs; reader->parse(line.data(), line.data() + line.size(), &v, &errs); }newCharReader返回的对象内部没有跨请求状态,可以安全复用。
第三个技巧:数组遍历用for (const auto& item : arr),少用arr[i]下标。非const的下标访问会触发多次边界检查,而const-range遍历在语义上更清晰,也让编译器有更多优化空间。
我在这上面的血泪教训是:新手期给一个配置中心做热加载,用非constoperator[]读了两层字段,测试环境跑半天内存涨了800MB,最后定位发现全是null节点。从那以后养成了一个习惯——凡是只需要读的Json::Value,一律写const Json::Value&,让语言和库共同约束我。希望帮到你。
本文还有配套的精品资源,点击获取