ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

JsonCpp 源码直用指南:免编译集成、CMake 配置与五个避坑现场

JsonCpp 源码直用指南:免编译集成、CMake 配置与五个避坑现场

简介:无需编译库的JsonCpp源码包,面向在Visual Studio项目中直接集成JSON解析功能的C++开发者,尤其适合中小型项目快速引入JSON能力,解决原生使用JsonCpp时配置麻烦的问题。压缩包共38个文件,主体为14个.h头文件与5个.cpp源文件,涵盖Json::Reader、Json::Value、Json::Writer等核心类实现,以及少量内部工具头文件;另有Visual Studio解决方案/工程文件及少量编译中间文件,便于对照工程理解源码组织。包体仅1.11MB,轻量易分发。已有961人学习下载。除核心源码外,内含一个Windows示例工程,清晰展示实现文件与头文件的组合方式;包内目录结构明确,可直接借鉴其源码组织方式,省去预编译库和链接配置流程,快速获得JSON解析、生成和读写能力,适合需要快速集成或深度阅读源码的二次开发场景。

1. JsonCpp 源码直用:不编译库也能跑,还更适合上手改

很多人一听到 jsoncpp 库下载,第一反应是去翻编译好的 .a 或 .lib,或者让 vcpkg 帮装。但真到嵌入式环境、老编译器或者客户现场那台不联网的机器上,直接运用源码、不编译库往往才是活下来的方案。JsonCpp 本体就是一组头文件加三个实现 cpp,几乎不依赖外部库,嵌进主工程一起编译,省去包管理器同步的麻烦,也方便出问题时直接读源码定位。下面按我的实际操作顺序,把老项目切换过程中的文件取舍、两种引入写法、编译参数设置,以及五个翻车现场逐一说清,适合不想被依赖链绑架的 C++ 开发者。

2. JsonCpp 源码结构:先搞清 include 与 src 的最小文件集合

JsonCpp 的源码包在 GitHub 上是很标准的目录布局,顶层有 include、src、test、doc 几个目录。真正要拿进工程的是 include/json 下的头文件和 src/lib_json 下的实现文件。这里有个常被忽略的点:src 目录底下除了 lib_json,还有一堆辅助工具和测试代码,如果图省事把整个 src 拷进去,编译时会多出一堆用不到的目标文件,还可能因为测试代码依赖 Catch2 之类的第三方库而报配置错误。所以第一步不是写构建脚本,而是先把文件清单理清楚。

我用过几种源码引入方式,最后稳定下来的是这个原则:只带 include 和 src/lib_json 两个目录,其余全部不带。这样 JsonCpp 自身的头文件包含关系能完整保留,编译器也不会误打误撞用上测试代码。下面把文件集合讲细,讲完再讨论版本差异。

2.1 必须拿走的文件:json.h 与三个实现 cpp

以 JsonCpp 1.9.x 为例,include/json 目录下这几个头文件是核心:

文件作用
json.h统一入口,聚合包含下面几个头文件
value.hJson::Value 类的定义,含迭代器和内部存储
reader.hJson::Reader 与 CharReader 解析接口
writer.hStreamWriter、Writer 等序列化接口
features.hFeatures 配置,控制解析的宽松程度
allocator.h内存分配器接口,不常用,但不能缺

对应 src/lib_json 下有三个实现文件:json_reader.cpp、json_value.cpp、json_writer.cpp。这三个文件加起来不到两万行,编译速度可以接受。早期版本(0.6.x)是单个 json.cpp 加单个 json.h,文件更少,但 0.x 时代的 API 和现在的差异不小,比如没有 StreamWriterBuilder,序列化只能调 toStyledString(),缩进不可调。如果项目不卡 C++ 标准,我会选 1.9.x。

有个容易漏的细节:src/lib_json 里有 json_valueiterator.inl 这个文件,扩展名不是 .cpp,但它被 value.h 以 inline include 的方式包含,不能漏拷。漏了它,编译到 Value 的迭代器相关代码时会报找不到文件的错,而且错误信息指向的路径是 json 目录里的相对引用,第一次碰到会绕很久。

实际集成时,我一般用一条命令完成拷贝:

mkdir -p third_party/jsoncpp cp -r jsoncpp/include third_party/jsoncpp/include cp -r jsoncpp/src/lib_json third_party/jsoncpp/src_lib_json

这里特意把 lib_json 重命名成 src_lib_json,是为了避免工程里的构建系统误扫描 src 目录下其他子目录。如果你用的是 IDE,直接在工程里添加文件时更容易看清哪些文件被包含,建议逐个添加而不是拖整个目录进来。

另外还有一个初始化问题:JsonCpp 的源码里带一个 dist 目录或贡献脚本,某些版本还要生成 version.h 头文件。如果下载的是官方 release 包,这个文件已经在 include/json 里;如果是从 git clone 的源码,需要先运行 cmake 或 python amalgamate 脚本生成。我建议直接下载 release 包,省去这步生成步骤。

2.2 版本差异:老版本与 1.9.x 的文件组织不一样

JsonCpp 迭代了十几年,源码组织有两个明显的分水岭。0.x 时代就是一对 json.h / json.cpp,想在一个老工程里快速塞一个 JSON 解析器,找个 0.6.x 直接编就是,对 C++98 也友好。1.7.x 开始把 value、reader、writer 分拆,但有些实现还互相混着。到了 1.9.x 才稳定成现在的三件套。

这个版本差异,在决定“直接运用源码、不编译库”时得先想清楚:如果工程编译器还守在 C++98 模式,要么锁死 0.6.x,要么把工程标准提到 C++11,否则后面会遇到一堆 auto、override 的语法报错。我个人建议能升就升,因为 1.9.x 不仅修了老版本一堆解析边界问题,序列化接口也更灵活,将来业务需要高性能场景时,升级库比改业务代码成本低得多。

源码包里有个 amalgamate.py 脚本,能把所有头文件和实现合并成单个 jsoncpp.cpp 加 json/json.h,也就是常说的合并版。运行方式很简单:

python amalgamate.py --output=../jsoncpp-amalgamated

脚本会在指定目录生成 json/json.h 和 jsoncpp.cpp 两个文件。合并版对没有源码目录管理习惯的工程很友好,两个文件往项目里一放,include 路径指一下就能编。我做过几次合并版集成,编起来比多文件版还快,因为省了头文件扫描。代价是后续想改某个内部实现,得在一大坨文件里找位置。我的习惯是:临时演示或小工具用合并版,长期维护的项目保持多文件原始结构,两者切换成本并不高。

2.3 检查清单:拷完文件后必做的三项确认

文件拷完,别急着写构建脚本,先做三件事确认状态。第一,include/json 下不能有非头文件混入,比如 .txt 或说明文件,有就清掉。第二,src 相关目录只留 .cpp 和 .inl,其他子目录的测试文件全部移除。第三,用一条命令确认 json.h 里 include 的头文件都在 include/json 目录下存在:

grep -o '#include[^<"]*"[^"]*"' jsoncpp/include/json/json.h

把 grep 出的文件名和实际目录列表比对一遍,避免少拷某个头文件导致编译中期报错。这三项确认做完,源码准备工作才算结束。我见过不少同事跳过这一步直接改 CMake,结果编译到一半报找不到 features.h,回来一查是上次清理时误删了。

3. 直接引入的两种工程组织方式:CMake 子目录与手工列源文件

源码拷进来之后,下一步就是把 JsonCpp 接进构建系统。这里有两类主流做法,一种适合用 CMake 组织的新工程,一种适合还在用 Makefile 或 IDE 的老工程。两条路的本质一样:让 JsonCpp 的实现 cpp 参与主工程编译,而不是先产出一个静态库再链接。说白了,就是把 JsonCpp 当成你自己工程里的几个普通源文件对待。

顺手说一下为什么不先编静态库:静态库本身没毛病,但多一层产物就多一层平台差异。在 Windows 上编的 .lib 换到 Linux 就不能用,交叉编译环境更是得重新编一次;如果源码直接进工程,编译选项跟着主工程走,编译器换掉也只是重新编一次的事,不会有二进制不兼容的坑。

3.1 方式一:CMake 工程用 add_subdirectory 接管源码

如果你的工程已经用 CMake 管理,最省事的做法是把 JsonCpp 放到 third_party/jsoncpp 目录下,然后在主 CMakeLists.txt 里 add_subdirectory。

# 关闭 JsonCpp 自带的测试和安装逻辑,只保留静态编译 set(JSONCPP_WITH_TESTS OFF CACHE BOOL "" FORCE) set(JSONCPP_WITH_CMAKE_PACKAGE OFF CACHE BOOL "" FORCE) set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) add_subdirectory(third_party/jsoncpp) add_executable(my_app main.cpp) target_link_libraries(my_app PRIVATE jsoncpp_static) target_include_directories(my_app PRIVATE third_party/jsoncpp/include)

三个 set 的用途分别说一下。JSONCPP_WITH_TESTS 不关掉,JsonCpp 的 CMake 会把 test 目录下的测试目标也加进来,多出一堆依赖,构建时间翻倍;JSONCPP_WITH_CMAKE_PACKAGE 控制的是安装包相关逻辑,源码直用时不需要;BUILD_SHARED_LIBS 置为 OFF 是强制走静态路径,配合后面的 jsoncpp_static 目标,避免生成 dll 后运行期还要带一个库文件。

target_link_libraries 里链接的 jsoncpp_static 是 JsonCpp 提供的编译目标,另一个叫 jsoncpp_object,只编译不归档。普通应用链接前者即可。另外注意,add_subdirectory 放在 set 后面才生效,CMakeLists 是顺序执行的,顺序写反了等于没设置,这个细节我犯过一次,编译到一半测试目标冒出来才知道。

3.2 方式二:Makefile / IDE 工程手工把 cpp 列进源文件

没有 CMake 的老工程,老板一般也不让大动构建系统,那就直接在构建的源文件列表里加三个 cpp。以 Makefile 为例:

CXX := g++ CXXFLAGS := -std=c++11 -I third_party/jsoncpp/include APP_SOURCES := main.cpp \ third_party/jsoncpp/src/lib_json/json_reader.cpp \ third_party/jsoncpp/src/lib_json/json_value.cpp \ third_party/jsoncpp/src/lib_json/json_writer.cpp all: my_app my_app: $(APP_SOURCES:.cpp=.o) $(CXX) $(CXXFLAGS) -o $@ $^ %.o: %.cpp $(CXX) $(CXXFLAGS) -c $< -o $@

这里最容易出错的是头文件路径。JsonCpp 的实现 cpp 内部通过 #include "json/value.h" 的方式引用头文件,所以 -I 必须指向 include 目录,让编译器能找到 json/ 这个子目录。如果只把 include/json 目录加进 -I,编译器会找 json/json/value.h,自然就报找不到文件。

Windows 上 Visual Studio 工程同理:把三个 cpp 加进 Source Files,把 third_party/jsoncpp/include 加进 Additional Include Directories,然后别忘了加第 4 章要讲的静态库宏。这套做法我实测过 gcc、clang、MSVC 三个编译器都能过,差异只在宏和 C++ 标准设置上。

3.3 快速验证:用最小 main 函数确认集成成功

源码引入最忌讳一步到位直接改业务代码。我在集成时永远先写一个临时 main.cpp,单独编译验证 JsonCpp 能工作,再往业务层接。这个临时文件的写法基本是固定套路:

#include <json/json.h> #include <iostream> int main() { Json::Value root; root["name"] = "jsoncpp-source-test"; root["count"] = 1; root["tags"].append("source"); root["tags"].append("direct"); Json::StreamWriterBuilder builder; builder["indentation"] = " "; std::string output = Json::writeString(builder, root); std::cout << output << std::endl; return 0; }

这段代码覆盖三个基础操作:给 root 赋键值、append 构造数组、用 StreamWriterBuilder 序列化。writeString 是 1.9.x 推荐的序列化入口,老版本常用 root.toStyledString(),后者缩进写死没法配置,新工程建议直接走 StreamWriterBuilder。这个临时 main 能编译能运行,说明头文件路径、三个 cpp 的编译和链接全部正常,这时候再往业务代码里接,排查范围就小很多。

3.4 两种引入方式怎么选:加一层适配更稳

add_subdirectory 适合中大型工程,JsonCpp 的 CMake 会把编译选项、宏定义一并管好,你只需要链接目标;手工列 cpp 适合轻量工程,构建脚本完全可控,没有 CMake 的隐含行为。但无论选哪种,都建议单独建一个 JSON 适配层,把 JsonCpp 的 include 集中在一个封装类里,不要散落在业务代码各处。这样将来想换 RapidJSON 或自研解析器,只需改封装层,代价最小。我在老项目里就是这么做的,业务代码里只有一个 JsonUtil 类见过 JsonCpp 的头文件。

4. 避坑清单:源码直用最容易翻车的五个现场

源码引入 JsonCpp 的坑,多集中在链接、宏、编码和 Value 的语义细节上。这五个我全都实际踩过,按现象、原因、解决的顺序写,供你对照排查。

4.1 链接报重复定义或符号冲突

现象:编译一切正常,链接阶段报 multiple definition of `Json::Value::Value(Json::Value const&)',MSVC 下对应 LNK2005。出现这类问题,工程状态基本都满足一个共性:既在链接一个预编译的 JsonCpp 库,又把源码 cpp 加进了编译。

原因:vcpkg 或 apt 装过的 JsonCpp 静态库还挂在链接参数里,源码加进来后,同一份符号被编译了两遍,链接器无法区分。这种情况在旧工程里非常常见,因为没人记得当年是谁装了那个包。

解决:检查链接项里所有 JsonCpp 相关条目,二选一。做源码直用就把包管理器里的 JsonCpp 从依赖清单移除,并删掉 build 缓存重新生成。CMakeCache 里残留的链接参数不会自动消失,不删缓存的话,旧库路径还会继续被带出来,清完缓存重新 configure 一遍,问题基本就消失了。

4.2 Windows 下链接报 LNK2019:外部符号无法解析

现象:Windows 上用 MSVC 编译源码,编译期没有报错,链接时却报 LNK2019,指向 Json::Value 的构造函数或某个成员函数无法解析。

原因:JsonCpp 头文件里有条件编译逻辑,定义 JSONCPP_DLL 时走 __declspec(dllimport/dllexport) 分支。源码直用、没有定义 JSONCPP_USE_STATIC_LIBRARY 时,某些版本会默认走 DLL import 分支,链接器以为这个类是从外部 DLL 导入的,自然找不到导出符号。

解决:在预处理定义里加上 JSONCPP_USE_STATIC_LIBRARY。Visual Studio 的入口是:项目属性 → C/C++ → 预处理器 → 预处理器定义,添加这一条。注意不要同时加 JSONCPP_DLL,两个宏互斥,同时定义会进 dllexport 分支,一样报错。我最初把两个宏都写上去,结果编译过了但链接报错更离谱,后来才明白它们是开关不是共存项。

4.3 老工程编译 JsonCpp 1.9.x 报几百行语法错误

现象:一个 C++98 标准的老工程,编译 json_writer.cpp 时报错提到 auto、override、右值引用相关语法,错误数量动辄几百行,从第一个 include 开始一路报过去。

原因:JsonCpp 1.9.x 要求 C++11 以上,工程还在旧标准,编译器用 C++98 语法去解析 C++11 代码,自然全军覆没。

解决:两条路,把工程标准提到 C++11,或者锁死 JsonCpp 0.6.x。我一般顶格处理——工程标准升到 C++11 再引入,因为老版本 API 差异大,将来换新还得再改业务代码。升标准这件事本身要评估工程里其他第三方库是否兼容,但如果你的工程已经用了 any 或 shared_ptr,标准大概率已经是 C++11,那直接加 -std=c++11 就行。

4.4 读不存在的 key,JSON 里悄悄多出空字段

现象:代码里只是想做一次读取判断,比如 root["user"]["age"],序列化后输出里却多出 "user": {"age": null}。这个字段没人写过,但它就是出现在输出里。

原因:Json::Value 的非 const operator[],在 key 不存在时会默认插入一个 null 值再返回引用。这个语义坑防不胜防,尤其在你以为 operator[] 和 std::map 的 operator[] 行为一样时,std::map 也会插入,但 JsonCpp 的插入发生在嵌套查找的中间层,影响范围更大。

解决:读取前用 root.isMember("user") 判断 key 是否存在,或者用 root.get("user", Json::nullValue) 拿默认值。get 版本不会插入新 key。在循环里读配置字段时尤其要小心,你以为只是取值,实际上在持续改写原对象,导致每次序列化输出都在膨胀。

4.5 解析带 BOM 的文件失败或中文乱码

现象:Windows 记事本保存的 JSON 文件,用 Reader 解析时报错,或者第一个 key 解析异常;另存为 ANSI 编码时中文直接乱码。这类问题在从 Windows 机器上拿配置文件时出现频率最高。

原因:带 BOM 的文件前三个字节是 EF BB BF,JsonCpp 不会自动跳过;ANSI 编码的中文是 GBK 序列,JsonCpp 只按 UTF-8 解码。

解决:读取文件时用 binary 模式,读完后手动跳过 BOM,再交给 Reader。

std::ifstream ifs("config.json", std::ios::binary); std::vector<char> buf((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>()); if (buf.size() >= 3 && (unsigned char)buf[0] == 0xEF && (unsigned char)buf[1] == 0xBB && (unsigned char)buf[2] == 0xBF) { buf.erase(buf.begin(), buf.begin() + 3); } std::string jsonText(buf.begin(), buf.end()); Json::CharReaderBuilder rbuilder; Json::Value root; std::string errs; std::istringstream iss(jsonText); if (!Json::parseFromStream(rbuilder, iss, &root, &errs)) { std::cerr << "parse failed: " << errs << std::endl; }

注意这里用 CharReaderBuilder 加 parseFromStream,这是 1.9.x 推荐的解析入口,比旧版 Json::Reader 更可控。至于 GBK 文件,先转码成 UTF-8 再解析,别指望 JsonCpp 帮你处理编码,它只认 UTF-8。

5. 编译宏与内存模型:源码模式下必须看懂的参数

直接把源码编进项目,本来就是为了能看清和调整 JsonCpp 的行为。这一章把影响编译行为的宏和影响运行行为的内存模型拆开讲,你会发现很多“玄学报错”背后其实都有明确的开关。

5.1 三个关键宏:JSONCPP_USE_STATIC_LIBRARY / JSONCPP_DLL / JSONCPP_NO_MAP

第一个宏是 JSONCPP_USE_STATIC_LIBRARY,见第 4 章,源码直用时必须在预处理里定义,Windows 下尤其重要。跨平台构建里我建议直接写在公共编译选项里,避免每个平台单独维护,否则换台机器又得查一遍。第二个是 JSONCPP_DLL,只在你要把 JsonCpp 编成 DLL 时使用,源码直用的场景严禁同时定义。第三个是 JSONCPP_NO_MAP,定义后 Json::Value 内部用 vector 替代 map 来存对象成员,适合成员数量少、以读取为主的 JSON 结构,能减少内存碎片;反之,成员多、频繁增删的场景用 map 更稳。是否启用,找一份线上真实配置做基准测试再定,别拍脑袋。

还有一个容易被忽略的点:JSONCPP 开头的宏定义可能会被预编译头夹带传递。如果工程里预编译头包含过旧版本 JsonCpp 的配置文件,编译时不会报错但行为会不一致。遇到诡异问题,先全工程搜一遍 JSONCPP 开头的大写宏,看有没有定义冲突。

5.2 Json::Value 的内存模型:哪些数据在栈上,哪些在堆上

Json::Value 的内部存储不是简单的一个 union,而是按类型分派:整数、布尔、unsigned 这类标量直接存在 Value 对象内部,不分配堆内存;字符串、数组、对象则通过指针指向堆上的真实数据。这个设计带来的直接后果是“复制 Value”是浅拷贝,配合写时复制,看起来像深拷贝,实际只在调用非 const 方法修改时才真正复制底层数据。

Json::Value a; a["key"] = "value"; Json::Value b = a; // 浅拷贝,共享底层数据 b["key"] = "other"; // 非常量 operator[] 触发写时复制 std::cout << a["key"].asString() << std::endl; // 输出 value

这里 b 的赋值操作会触发 detach,所以 a 不受影响。这个特性对单线程使用是透明的,但有两个陷阱:第一,把 Value 放进 STL 容器后,不要长期保存指向容器元素的引用,容器扩容会触发底层数据搬移,引用可能失效;第二,不要把同一个 Value 交给多个线程同时读写,写时复制在单线程下没问题,多线程下没有加锁,数据竞争会带来偶发崩溃。

5.3 Reader 与 Writer 的参数配置

JsonCpp 的解析和序列化行为不是写死的,主要通过 Features 和 CharReaderBuilder / StreamWriterBuilder 配置。下面这张表是 Features 里最常用的几个开关:

参数默认值作用
allowCommentsfalse是否允许 JSON 里带 C/C++ 风格注释
strictRootfalse根节点必须是对象或数组时置 true
allowDroppedNullPlaceholdersfalse解析时是否忽略部分缺失的空值占位
allowNumericKeysfalse是否允许数字作为对象 key

严格模式下建议把 strictRoot 打开,能拦截文件被截断后解析出非法根类型的情况。allowComments 在解析手工维护的配置文件时值得打开,很多运维同学习惯在 JSON 里写注释,实际部署时再关掉即可。

序列化端,StreamWriterBuilder 的常用参数包括 indentation、emitUTF8 等。writer 在 1.9.x 被标记为 deprecated,新代码统一走 StreamWriterBuilder。另外,大 JSON 文件的解析是递归下降实现,嵌套深度过深时会耗尽线程栈。面对深度嵌套的结构,常见做法是先做一次快速检查:统计左花括号的最大连续深度,超过 500 层就换用迭代式解析方案。JsonCpp 没有提供独立的迭代解析 API,这个检查是规避爆栈最实用的手段。

6. 集成自检:用回归测试确认你的 JsonCpp 没问题

资源落地最关键的是验证方式。我每次把 JsonCpp 源码接进新工程,都会跑一段固定的回归测试,不是打印一个 hello 就完事,而是拿一份线上真实 JSON 数据走完整链路:解析、遍历、改写、序列化、再解析。

#include <json/json.h> #include <iostream> #include <fstream> #include <sstream> int main(int argc, char** argv) { if (argc < 2) { std::cerr << "usage: self_check <json-file>" << std::endl; return 1; } std::ifstream ifs(argv[1], std::ios::binary); Json::CharReaderBuilder rbuilder; Json::Value root; std::string errs; if (!Json::parseFromStream(rbuilder, ifs, &root, &errs)) { std::cerr << "parse failed: " << errs << std::endl; return 2; } int nodeCount = 0; for (Json::Value::const_iterator it = root.begin(); it != root.end(); ++it) { ++nodeCount; } Json::StreamWriterBuilder wbuilder; std::string output = Json::writeString(wbuilder, root); std::istringstream iss(output); Json::Value again; std::string errs2; if (!Json::parseFromStream(rbuilder, iss, &again, &errs2)) { std::cerr << "reserialize parse failed: " << errs2 << std::endl; return 3; } std::cout << "self_check ok, root keys=" << nodeCount << std::endl; return 0; }

这个程序接收一个 JSON 文件参数,第一步用 CharReaderBuilder 解析,失败直接返回错误码;第二步遍历根节点统计数量,确认数据结构完整读入;第三步把整个 Value 序列化成字符串,再对这个字符串重新解析,只要二次解析成功,说明序列化没有产生损坏的输出。assert 可以加在关键节点上。我一般在 CI 里配这条命令跑一份固定的生产配置样本,任何一次升级或者宏调整导致行为漂移,第一时间就能看到。

还有一个调试技巧值得记住:开着一份 value.cpp 的源码,遇到不符合预期的解析结果就用 gdb 打断点到 Json::Value::resolveReference,能看到内部存储到底走了哪条分支。源码直用的最大红利就是这里有完整符号,不用面对黑匣子。从那以后我每次集成 JsonCpp 源码,都强制走一遍“编译 → 解析真实样本 → 改写 → 再序列化”这条链,链接宏、编码、内存模型相关的坑基本都在这一轮暴露完,希望帮到你。

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

返回列表