简介无需编译库的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 直接编就是对 C98 也友好。1.7.x 开始把 value、reader、writer 分拆但有些实现还互相混着。到了 1.9.x 才稳定成现在的三件套。这个版本差异在决定“直接运用源码、不编译库”时得先想清楚如果工程编译器还守在 C98 模式要么锁死 0.6.x要么把工程标准提到 C11否则后面会遇到一堆 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 : -stdc11 -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 报几百行语法错误现象一个 C98 标准的老工程编译 json_writer.cpp 时报错提到 auto、override、右值引用相关语法错误数量动辄几百行从第一个 include 开始一路报过去。原因JsonCpp 1.9.x 要求 C11 以上工程还在旧标准编译器用 C98 语法去解析 C11 代码自然全军覆没。解决两条路把工程标准提到 C11或者锁死 JsonCpp 0.6.x。我一般顶格处理——工程标准升到 C11 再引入因为老版本 API 差异大将来换新还得再改业务代码。升标准这件事本身要评估工程里其他第三方库是否兼容但如果你的工程已经用了 any 或 shared_ptr标准大概率已经是 C11那直接加 -stdc11 就行。4.4 读不存在的 keyJSON 里悄悄多出空字段现象代码里只是想做一次读取判断比如 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 BFJsonCpp 不会自动跳过ANSI 编码的中文是 GBK 序列JsonCpp 只按 UTF-8 解码。解决读取文件时用 binary 模式读完后手动跳过 BOM再交给 Reader。std::ifstream ifs(config.json, std::ios::binary); std::vectorchar buf((std::istreambuf_iteratorchar(ifs)), std::istreambuf_iteratorchar()); 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根节点必须是对象或数组时置 trueallowDroppedNullPlaceholdersfalse解析时是否忽略部分缺失的空值占位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 源码都强制走一遍“编译 → 解析真实样本 → 改写 → 再序列化”这条链链接宏、编码、内存模型相关的坑基本都在这一轮暴露完希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?