简介:这份已编译的 jsoncpp 压缩包面向使用 C++ 与 Visual Studio 处理 JSON 解析、生成和数据交换的开发者,省去从源码编译的环节,适合需要在项目中快速集成轻量级 JSON 能力的初、中级开发场景。包内共 10 个文件,以 8 个 .h 头文件和 2 个 .lib 库文件为主:头文件提供 Json::Value、Reader、Writer 等接口声明,库文件承担链接阶段的实现,压缩包约 1023KB,目录按 include 与 lib 组织,便于直接纳入工程。已有 339 人学习或下载,说明该版本在 VS 环境下具有一定参考与复用价值。拿到后可按头文件声明与库文件实现对应的方式接入现有 C++ 工程,围绕 JSON 解析、序列化和查询进行二次开发,减少环境搭建与编译排查时间,把精力放在业务数据处理与接口联调上。
1. 预编译 jsoncpp 拿到手:版本对不上,后面每一步都在还债
上周又碰到一次典型的翻车:某开发者把一份已编译的 jsoncpp 压缩包解压到工程目录,include 和 lib 一丢,编译一路绿灯,程序跑起来读到配置文件那行直接段错误;换了个机器,同样的代码变成链接期一屏undefined symbol。jsoncpp 这个 C++ 的 JSON 解析库,几乎每个带配置读取、接口返回解析、日志落地的项目都会塞一份进去,装得好是十分钟的事,装不好能查一下午。
预编译包省掉的是 CMake 编译那十几分钟,代价是把“编译器版本、C++11 ABI 开关、目标位数、Debug/Release 运行库”这四道选择题挪到了你身上。这篇按我实际装过的顺序走一遍:先核环境,再放文件,再配查找路径,最后拿一个二十行的最小程序把整套环境钉死。适合手里已经有一份预编译 jsoncpp、准备往 CMake 或 Makefile 项目里接的人。
2. 装前对齐环境:jsoncpp 1.9.x 预编译包的四个匹配项
预编译包真正的风险不是“能不能用”,而是“能不能一直用”。同一份libjsoncpp.so,在打包那台机器上一切正常,换到你机器上可能只是某个符号名多了个__cxx11前缀,链接器就翻脸不认人。所以解压之后别急着cp,先花五分钟把下面四项核一遍:编译器大版本、C++11 ABI 开关、目标位数与架构、Debug/Release 运行库。这四项里任何一项对不上,报错信息都不会直说“是 ABI 的锅”,只会给你一串看不懂的符号名,这就是它最玄学的地方。
2.1 编译器大版本与 C++ 标准先对一遍
jsoncpp 1.9.x 这条线自身是按 C++11 写的,常见预编译包标注的最低门槛在 GCC 4.8、Clang 3.4、MSVC 2015 这一档。你的工程如果还停在-std=c++98,光把#include <json/json.h>加进去就会先炸一批语法错误。我一般的做法是先确认自己这边的编译器档次,再决定这份包能不能用。
# 看编译器主版本,1.9.x 的包最低按 GCC 4.8 这一档估 g++ --version | head -1 # 看默认的 C++ 标准和 ABI 宏,比翻文档快 g++ -dM -E -x c++ /dev/null | grep -E '__cplusplus|_GLIBCXX_USE_CXX11_ABI'第一行给出的是编译器版本,用来判断包里的目标文件是不是你这代编译器产出的;第二条命令打印的是预处理宏,__cplusplus是默认标准(201103表示 C++11),_GLIBCXX_USE_CXX11_ABI就是下一节要重点说的 ABI 开关,值只能是 0 或者 1。如果你的工程在CMakeLists.txt里显式写了CMAKE_CXX_STANDARD 11,那这一项基本没问题,怕的是那种“默认标准凑合用”的老工程,加库之前先显式升一次标准更稳妥。
还有一项容易漏:运行时 libstdc++ 的版本。包是在新机器上编的,用到GLIBCXX_3.4.26这类新符号,搬到老系统上一样跑不起来。提前看一眼:
# 列出这份 so 依赖的最高 GLIBCXX 版本,和运行环境的 libstdc++ 对一下 strings libjsoncpp.so.25 | grep -E '^GLIBCXX_[0-9.]+$' | sort -Vu | tail -3输出里最高那个版本号,就是运行环境 libstdc++ 必须达到的下限。达不到只有两条路:换一份在老系统上编的包,或者在目标机器上重编。别想着打补丁绕过,这类符号缺失在运行时才暴露,比链接期报错难查得多。
2.2 C++11 ABI 开关:__cxx11 那串符号
GCC 5 之后std::string换了实现,为了兼容老代码留了个开关_GLIBCXX_USE_CXX11_ABI。jsoncpp 的公开接口里大量出现std::string(Json::Value::asString()、Json::writeString()都是),所以这个开关必须和预编译包保持一致,错一位都不行。
判断方法很直接,看包里导出符号带不带__cxx11:
# 统计带 __cxx11 的导出符号数量,大于 0 说明包是用新 ABI 编的 nm -D --defined-only libjsoncpp.so.25 | grep -c '__cxx11'结果大于 0,你的工程就要加-D_GLIBCXX_USE_CXX11_ABI=1;等于 0,就加=0。CMake 里对应写成:
# 必须和预编译包的 ABI 一致,否则链接期会出现 std::__cxx11::basic_string 找不到定义 add_compile_definitions(_GLIBCXX_USE_CXX11_ABI=1)这行必须加在add_executable之前,影响的是全局编译选项。我见过有人在某个target_compile_definitions里只给一半的 target 加了这行,结果主程序编过了、单元测试链接失败,症状特别迷惑。整个工程统一加,别挑挑拣拣。
Windows 那边没有这个概念,但有个等价物:动态库要定义导入宏,静态库要定义静态宏,名字在不同版本里改过,最靠谱的办法是打开包里的json/config.h搜一下JSON_DLL和JSONCPP_STATIC_DEFINE,看到哪个就用哪个,别凭记忆写。
2.3 位数、架构与 Debug/Release
这三项经常被当成理所当然,实际出错率不低。用file一条命令就能看出包是什么货色:
file lib/libjsoncpp.so.25 # 期望输出类似:ELF 64-bit LSB shared object, x86-64, version 1 (SYSV)输出里64-bit和x86-64两个词决定了一切。你的进程是 32 位,链接这份 64 位库会直接报file in wrong format,这个还算友好;麻烦的是 ARM 机器上拿了一份 x86-64 的包,报错信息会绕一大圈。Windows 侧对应看dumpbin /headers jsoncpp.lib,关注 machine 那一行是 x64 还是 x86。
Debug/Release 的坑主要在 Windows:MSVC 的 Debug 版用调试版 CRT,Release 版用发布版 CRT,两边的堆管理不通用。你拿一份 Release 编的jsoncpp.lib去链 Debug 工程,编译链接都可能过,运行到Json::Value析构时堆损坏,退出时崩在某个完全无关的地方。判断方法是看包名或者看 lib 目录里有没有d后缀的版本,没有的话就统一用 Release 配置跑,别混着来。
| 匹配项 | 检查命令 | 不一致的表现 |
|---|---|---|
| 编译器大版本 | g++ --version | 链接期符号缺失或语法报错 |
| C++11 ABI | nm -D … | grep -c __cxx11 | std::__cxx11::basic_string未定义 |
| 位数与架构 | file libjsoncpp.so.25 | file in wrong format或直接段错误 |
| Debug/Release | 看包内 lib 命名后缀 | Windows 上堆损坏、退出崩溃 |
| libstdc++ 版本 | strings … | grep GLIBCXX | 运行期version GLIBCXX_x.x.x not found |
2.4 用三条命令把包里的信息读出来
拿到一份来源不明的预编译包,我习惯先把它的“身份证”读一遍,再决定放不放到系统目录:
# 1. 看 soname,这个数字就是 ABI 版本号,别自己猜 readelf -d libjsoncpp.so.25 | grep SONAME # 2. 看依赖了哪些运行库,重点确认没有奇怪的第三方依赖 ldd libjsoncpp.so.25 # 3. 确认关键符号在不在,CharReaderBuilder 是 1.9.x 的解析入口 nm -D --defined-only libjsoncpp.so.25 | c++filt | grep CharReaderBuilder | head -3第一条输出Library soname: [libjsoncpp.so.25]这种格式,25就是 ABI 版本号,后面建软链接要用到它,写错了运行时找不到库。第二条列出的依赖应该只有libstdc++、libm、libgcc_s、libc这几样,多出任何一项都要警惕,说明这个包不是“干净”编译出来的。第三条验证符号,c++filt负责把乱码一样的修饰名还原成可读形式,看到Json::CharReaderBuilder就说明这份包的接口版本对得上 1.9.x。
3. 落地安装:把 include 和 lib 放到位
环境核完就可以动手放了。这一步的目标很简单:让编译器用-I能找到json/json.h,让链接器用-ljsoncpp能摸到库文件,让运行时的动态加载器在ldconfig缓存里能找到那份.so。三件事各自独立,任何一件没做,报错都长得不一样。
3.1 Linux:/usr/local 下的目录布局与软链接
我一般不放系统自带的/usr/lib,那会和发行版包管理器装的那份 jsoncpp 打架。统一放/usr/local,路径清晰,卸载时删目录就干净了。
# 解压到临时目录,先看清楚结构再往系统里搬 tar -xzf jsoncpp-1.9.5-linux-x86_64.tar.gz -C /tmp/jsoncpp-pkg # 建目录,-p 保证父目录存在 sudo install -d /usr/local/include/json /usr/local/lib # 头文件按 json/ 子目录整体搬,因为代码里写的是 #include <json/json.h> sudo cp -a /tmp/jsoncpp-pkg/include/json/. /usr/local/include/json/ # 库文件连软链接一起保留,-a 不要漏 sudo cp -a /tmp/jsoncpp-pkg/lib/libjsoncpp.so* /usr/local/lib/ # 补一个开发用软链接,只在链接期有用 sudo ln -sf libjsoncpp.so.25 /usr/local/lib/libjsoncpp.so # 刷新动态库缓存,否则运行期还是找不到 sudo ldconfiginstall -d等价于mkdir -p加权限设置,比自己mkdir少踩权限坑。cp -a的-a是归档模式,保留符号链接本身而不是跟随链接复制实体文件,这一步如果漏了,.so会变成一个巨大实体文件,后面升级库会很难受。
软链接那条要特别说明:实际文件通常是三层,libjsoncpp.so.25.0.5是实体,libjsoncpp.so.25是 soname 链接,libjsoncpp.so是开发链接。编译链接时找的是最后那个不带版本号的,运行时找的是中间那个带 soname 的。两个链接缺一不可,但只有带 soname 的那个必须跟着ldconfig走。
3.2 Windows + MSVC:三个目录和两个宏
Windows 侧没有ldconfig这套东西,全靠工程配置。预编译包里一般有这几样,先分清用途再往工程里放:
| 文件 | 用途 | 放哪里 |
|---|---|---|
json/json.h、json/json-forwards.h | 头文件 | 任意固定目录,加到 Include 路径 |
jsoncpp.lib | 动态库导入库 | 加到 Library 路径,配.dll用 |
jsoncpp.dll | 运行时库 | 与 exe 同目录,或加进 PATH |
jsoncpp_static.lib | 静态库 | 加到 Library 路径,配静态宏用 |
工程属性里三处要改:C/C++ 的附加包含目录指向头文件根目录(注意是json的上一级,不是json本身),链接器的附加库目录指向 lib 所在目录,附加依赖项写jsoncpp.lib。然后是最容易忘的那一步,宏:
// 链动态库时不要定义 JSONCPP_STATIC_DEFINE,保留导入语义 // 链静态库时必须定义,否则符号被当成 dllimport,链接期报无法解析的外部符号 // 宏的准确名字以包内 json/config.h 为准,不同小版本改过 #define JSONCPP_STATIC_DEFINE #include <json/json.h>这段放在你自己的头文件里,别去改 jsoncpp 自带的头文件,升级包的时候会全部覆盖掉。宏定义写错的表现是__declspec(dllimport) 无法解析的外部符号一串,看到这个词基本就是宏的问题,不用往别处查。
3.3 写一份 jsoncpp.pc,让 pkg-config 接管查找
预编译包如果没带jsoncpp.pc,我一般自己补一份。好处是所有工程的编译命令都变成$(pkg-config --cflags --libs jsoncpp),路径改了只动一个文件。
# 保存为 /usr/local/lib/pkgconfig/jsoncpp.pc prefix=/usr/local exec_prefix=${prefix} libdir=${exec_prefix}/lib includedir=${prefix}/include Name: jsoncpp Description: JSON parsing library for C++ Version: 1.9.5 Libs: -L${libdir} -ljsoncpp Libs.private: -lm Cflags: -I${includedir}写完验证一下:
# 能看到正确的 -I 和 -l 参数就说明文件生效了 pkg-config --cflags --libs jsoncpp pkg-config --modversion jsoncppVersion这一行不是装饰,很多构建脚本靠它做版本门槛判断,写错会导致find_package之类的逻辑走错分支。如果pkg-config找不到文件,先echo $PKG_CONFIG_PATH,没有/usr/local/lib/pkgconfig就补上,或者干脆把.pc文件丢进/usr/lib/pkgconfig。Libs.private里那些只在静态链接时才需要的库放这一行,动态链接不会带进去。
4. 接到构建系统:CMake 与 Makefile 的两套写法
库放好了,接下来是让工程真正找到它。这一步的取舍在于:用find_package省事但依赖包自带 cmake 配置文件,手写 imported target 麻烦一点但完全可控。预编译包通常不带配置文件,所以我默认走第二条路。
4.1 CMake:find_package 与手写 imported target
先试配置文件这条路,一行就能看出有没有:
cmake_minimum_required(VERSION 3.16) project(jsoncpp_demo CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 预编译包带了 jsoncppConfig.cmake 才能走这条,找不到会直接报错停止 find_package(jsoncpp CONFIG REQUIRED) add_executable(demo main.cpp) target_link_libraries(demo PRIVATE JsonCpp::JsonCpp)CONFIG关键字表示只认配置文件模式,不去翻系统模块目录,行为更可预测。目标名JsonCpp::JsonCpp是 1.9.x 导出的标准名字,写错的话 CMake 会提示“目标不存在”。
包不带配置文件时,改成手写:
# 预编译包一般没有 cmake 配置文件,用 IMPORTED 目标把路径钉死 add_library(jsoncpp_prebuilt SHARED IMPORTED) set_target_properties(jsoncpp_prebuilt PROPERTIES IMPORTED_LOCATION "/usr/local/lib/libjsoncpp.so.25" # 实体文件或 soname 链接 INTERFACE_INCLUDE_DIRECTORIES "/usr/local/include" # 头文件根目录 INTERFACE_COMPILE_DEFINITIONS "_GLIBCXX_USE_CXX11_ABI=1" # 和包里的 ABI 对齐 ) add_executable(demo main.cpp) target_link_libraries(demo PRIVATE jsoncpp_prebuilt)IMPORTED_LOCATION建议直接写带 soname 的完整路径,写libjsoncpp.so也行,但那个是软链接,某些构建环境下 CMake 会去解析真实路径,反而绕。INTERFACE_COMPILE_DEFINITIONS那行的作用是把 ABI 定义跟着目标传下去,谁链这个库谁就自动带上,比全局add_compile_definitions更精确。
4.2 Makefile:链接顺序、rpath 与静态库的 -fPIC 问题
不用 CMake 的项目,Makefile 里三个地方要注意:-l的位置、rpath、静态库能不能塞进 so。
CXX := g++ CXXFLAGS := -std=c++11 -O2 -I/usr/local/include -D_GLIBCXX_USE_CXX11_ABI=1 # $$ORIGIN 是 Makefile 里的转义写法,最终传给链接器的是 $ORIGIN LDFLAGS := -L/usr/local/lib -Wl,-rpath,'$$ORIGIN/../lib' LDLIBS := -ljsoncpp demo: main.o $(CXX) $^ -o $@ $(LDFLAGS) $(LDLIBS)LDLIBS必须放在目标文件后面,链接器是从左往右扫的,库放在前面就会出现“符号明明在库里却报未定义”的怪事。-Wl,-rpath,'$ORIGIN/../lib'的含义是把运行时查找路径写进可执行文件,$ORIGIN指 exe 所在目录,这样打包发布时不用配LD_LIBRARY_PATH,也不用动系统缓存,是我最推荐的做法。
用静态库的话把-ljsoncpp换成完整路径/usr/local/lib/libjsoncpp.a,或者写-l:libjsoncpp.a。这里有个前置条件:静态库编译时必须带-fPIC,否则你拿它去生成.so会直接报relocation R_X86_64_32S against ... can not be used when making a shared object; recompile with -fPIC。这个错没法在链接阶段绕过,只能换一份带-fPIC编的静态库,或者改用动态库。
4.3 编译期就该注意的两个信号
编译输出别直接刷过去,有两类警告值得停下来看一眼。一类是废弃接口提示,Json::Reader、Json::Writer在 1.9.x 里已经标了 deprecated,你的老代码能编过但会刷一屏警告,短期内不影响运行,长期看是个迁移信号,新代码统一用CharReaderBuilder和StreamWriterBuilder。
另一类是 ABI 相关的警告,比如某个头文件里std::string的传参出现note: parameter 1 types differ之类,这种十有八九就是 2.2 节那个开关没对齐。我一般会在CXXFLAGS里加-Wall -Wextra,让这类问题在编译期就暴露,而不是留到运行期变成一个查不出来的段错误。
5. 避坑与排查:已编译 jsoncpp 最常翻的五处车
下面这五条按出现频率排,基本覆盖了预编译 jsoncpp 落地时能遇到的大半问题。每条都按“现象、原因、解决”写,方便对着报错直接查。
5.1 链接期一片 undefined symbol
现象:编译目标文件全部成功,链接时刷出十几行undefined reference to 'Json::Value::asString[abi:cxx11]() const'。
原因:多半是-ljsoncpp压根没生效。可能是路径不对(-L指错了目录)、可能库名不对(有的包只有libjsoncpp.so.25没有libjsoncpp.so这个开发链接),也可能是把-ljsoncpp写在了目标文件前面。
解决:先ls -l /usr/local/lib/libjsoncpp.so*确认软链接链完整,缺了就用ln -sf补;再pkg-config --libs jsoncpp看输出的路径对不对;最后检查 Makefile 里LDLIBS的位置。三件事按顺序排一遍,基本能定位。
5.2 编译通过,运行即崩在 Json::Value 相关调用
现象:程序启动正常,读到配置那一行崩掉,gdb 栈里停在std::string的构造或析构,没有任何业务代码。
原因:C++11 ABI 不匹配。头文件按一种 ABI 解释std::string的内存布局,库按另一种 ABI 构造,两边对同一个对象的认知不一样,一传参就踩内存。
解决:nm -D … | grep -c __cxx11确认包里是哪种 ABI,然后在工程里统一加-D_GLIBCXX_USE_CXX11_ABI=0或=1。加完必须全量重编,只重编改了的那几个文件没用,因为 ABI 是编译期决定的,残留的旧目标文件依然是错的。
5.3 静态库塞进 .so 时报 recompile with -fPIC
现象:想把 jsoncpp 静态链进自己的动态库,链接器报relocation R_X86_64_32S against '.rodata' can not be used when making a shared object。
原因:这份.a编译时没开位置无关代码,里面是绝对地址重定位,塞进.so里没法在加载时重定位。
解决:优先换动态库;必须静态的话,只能重新编一份带-fPIC的静态库。检查方法是readelf -r libjsoncpp.a | grep R_X86_64_32,如果这类重定位大量出现,就别硬试了。这个坑没有后悔药,只能换包。
5.4 项目里躺着两份 jsoncpp
现象:链接时出现multiple definition,或者更诡异的情况 —— 明明改了配置,程序行为没变,加日志发现用的是另一个版本的库。
原因:系统包管理器装了一份,/usr/local又装了一份,-L的搜索顺序决定了先找到哪一份。头文件也可能来自第三处。头文件和库版本不一致,是最容易出“行为诡异”的组合。
解决:ldd ./demo | grep jsoncpp看运行时实际加载的是哪个路径;g++ -E -x c++ - <<< '#include <json/json.h>' | grep json.h看实际用的是哪个头文件。两处路径必须来自同一个包。确认之后,把不需要的那份从搜索路径里摘掉,别指望链接器帮你选。
5.5 Windows 上退出时崩溃、堆损坏
现象:MSVC 工程编译链接都过,运行到程序退出时崩在ntdll或者free里,调试版下提示堆损坏。
原因:CRT 版本不一致。Debug 工程链了 Release 版 jsoncpp,两套 CRT 各有各的堆,内存在一边分配、在另一边释放,直接炸。
解决:把工程配置和库的配置对齐,全用 Release 或者全用 Debug 跑。如果包里只有一种,就把自己的工程配置改成一致的那种跑测试,别混着来。这类问题在 Linux 上少见,因为 ABI 层面就卡死了,Windows 上因为能链上所以更难发现。
6. 装完怎么验证:一个二十行的读写程序 + 三条检查命令
安装这件事,光“编译过了”不算完。我判断环境是否真的落地,标准是能跑一个同时覆盖解析和序列化的小程序,再把运行时依赖查一遍。下面这段二十行的代码就够用,解析一段 JSON、改一个字段、再输出回去,把CharReaderBuilder和StreamWriterBuilder两条路径都走一遍。
#include <json/json.h> #include <iostream> #include <sstream> int main() { const std::string raw = R"({"name":"demo","port":8080,"tags":["a","b"]})"; Json::CharReaderBuilder reader_builder; Json::Value root; std::string errs; std::istringstream in(raw); // 1.9.x 的解析入口,老代码里的 Json::Reader 已标记废弃但还能用 if (!Json::parseFromStream(reader_builder, in, &root, &errs)) { std::cerr << "parse failed: " << errs << std::endl; return 1; } root["port"] = 9090; // 改一个已有字段 root["extra"]["enabled"] = true; // 顺便验证嵌套写入 Json::StreamWriterBuilder writer_builder; writer_builder["indentation"] = " "; // 输出带缩进,方便肉眼核对 std::cout << "jsoncpp " << JSONCPP_VERSION_STRING << "\n" << Json::writeString(writer_builder, root) << std::endl; return 0; }JSONCPP_VERSION_STRING这个宏来自头文件,打印出来的版本号就是你实际用的头版本,和运行时库版本必须一致,这是第一个检查点。parseFromStream返回 false 时errs里会有具体位置信息,能直接定位是 JSON 语法问题还是库的问题。writer_builder["indentation"]接受字符串参数,写"\t"就输出制表符,不设的话是紧凑格式。
编译运行三步:
# 用 pkg-config 拼参数,省得手写 -I 和 -L g++ -std=c++11 main.cpp -o demo $(pkg-config --cflags --libs jsoncpp) ./demo # 关键检查:运行时到底加载了哪一份 jsoncpp ldd ./demo | grep jsoncpp预期输出里第一行是版本号(应该是 1.9.x),后面是带缩进的 JSON,port已经从 8080 变成 9090。ldd那一行必须指向你自己装的那份路径,指向系统目录就说明 5.4 节那个坑还在。
三条检查我一般一次性做完,记在部署脚本里:
| 检查项 | 命令 | 通过标准 |
|---|---|---|
| 头库版本一致 | 运行./demo看首行 | 打印版本与包内版本相同 |
| 运行时依赖正确 | ldd ./demo | grep jsoncpp | 路径指向自己安装的目录 |
| 符号确实存在 | nm -D --defined-only … | c++filt | grep CharReaderBuilder | 至少输出三行相关符号 |
这三条里任何一条不过,就先别往下接业务代码,回头查对应章节。尤其是第二条,很多人只验证了编译通过就往项目里搬,结果部署到另一台机器上因为rpath没配、ldconfig没刷,跑起来直接error while loading shared libraries,那时候再回头补,排查成本已经翻倍了。
从那以后我每次往工程里丢预编译 jsoncpp 之前,都强制走一遍nm看 ABI、file看架构、再跑这个二十行小程序,三分钟的事,能挡掉后面几个小时的玄学排查。这套习惯帮我省下的时间,比预编译本身省下来的多得多。希望帮到你。
本文还有配套的精品资源,点击获取