在C++项目里处理接口返回或配置文件中的JSON,最稳妥的做法是引入成熟的解析库而不是自己切分字符串。jsoncpp是目前使用最广泛的 choice 之一,它将JSON文本转换成内存中的Value树,开发者通过路径或迭代器访问字段,类型和嵌套关系都由库来维护。

一、环境准备与集成方式
jsoncpp源码托管在GitHub,核心由jsoncpp_src中的src/lib_json与include/json组成。最省事的做法是把这两个目录直接拖进自己的工程编译,或者在Linux下通过包管理器安装开发包。
以Ubuntu为例,执行apt-get install libjsoncpp-dev之后,头文件位于/usr/include/json,链接时加上-ljsoncpp即可。如果是CMake工程,可以用find_package(jsoncpp)导入目标。下面是一段最简的CMake片段,展示如何把源码子模块编进项目,避免外部依赖版本错乱。
# CMakeLists.txt 片段 add_subdirectory(third_party/jsoncpp) add_executable(app main.cpp) target_link_libraries(app jsoncpp_static)
要注意的是,jsoncpp默认使用C++11及以上标准,老项目需把编译选项调到-std=c++11。若报找不到unique_ptr相关符号,基本就是标准版本过低。
二、从字符串解析JSON对象
旧版教程常写Json::Reader配合parse方法,但新版本中Reader已被标记为废弃,官方推荐用CharReaderBuilder构造读取器。它返回一个CharReader实例,调用parse时传入起始指针、结束指针和输出Value,同时用字符串接收错误信息。
下面示例演示解析一段包含用户信息的JSON,并安全取出字段。我们显式判断类型,防止服务端少了某个字段导致程序崩溃。
#include <json/json.h>
#include <iostream>
#include <string>
int main() {
std::string text = R"({
"name": "张三",
"age": 28,
"tags": ["c++", "backend"]
})";
Json::CharReaderBuilder builder;
Json::Value root;
std::string errs;
const char* begin = text.data();
const char* end = text.data() + text.size();
// 使用Builder生成读取器并解析
if (!Json::parseFromStream(builder, begin, end, &root, &errs)) {
std::cerr << "解析失败: " << errs << std::endl;
return 1;
}
if (root.isMember("name") && root["name"].isString()) {
std::cout << "姓名: " << root["name"].asString() << std::endl;
}
if (root.isMember("age") && root["age"].isInt()) {
std::cout << "年龄: " << root["age"].asInt() << std::endl;
}
return 0;
}
这段代码里isMember先确认键存在,再用isString、isInt确认类型,最后才调用asString等转换函数。实际业务中网络数据不可信,跳过类型判断直接取值是常见崩溃来源。
如果JSON结构较深,可以用链式下标,例如root["profile"]["email"],但每一层都要判空。更优雅的写法是封装一个安全取值模板,对不存在的节点返回默认值。
三、遍历数组与嵌套对象
JSON数组在jsoncpp中对应ValueType::arrayValue,可以用下标循环,也可以用迭代器。下面的例子读取上面的tags数组,并把每个元素打印出来。
if (root.isMember("tags") && root["tags"].isArray()) {
const Json::Value& tags = root["tags"];
for (unsigned int i = 0; i < tags.size(); ++i) {
if (tags[i].isString()) {
std::cout << "标签" << i << ": " << tags[i].asString() << std::endl;
}
}
}
对于不确定结构的嵌套对象,可以用getMemberNames拿到所有键再遍历。这种方式在写通用日志组件或配置导出工具时很实用,不需要提前知道字段名。
需要提醒的是,jsoncpp的Value采用引用计数管理内存,拷贝开销小,但不要把局部Value的引用传出函数作用域。上面代码中的const Json::Value&仅在本帧有效,长期持有应改为值拷贝。
四、从文件读取与写出JSON
配置文件通常落在磁盘,用ifstream配合parseFromStream可直接读流。写出时则用StyledWriter或FastWriter,前者带缩进便于人工查看,后者压缩成单行节省空间。
#include <fstream>
#include <json/json.h>
void save_config(const Json::Value& cfg, const std::string& path) {
std::ofstream ofs(path);
Json::StyledWriter writer;
ofs << writer.write(cfg);
}
Json::Value load_config(const std::string& path) {
std::ifstream ifs(path);
Json::Value root;
Json::CharReaderBuilder builder;
std::string errs;
if (!Json::parseFromStream(builder, ifs, &root, &errs)) {
throw std::runtime_error("配置读取错误: " + errs);
}
return root;
}
StyledWriter在新版中也建议改用StreamWriterBuilder,通过settings设置缩进字符。不过对小工具而言旧接口依旧可用,只是编译器会给出废弃警告。
写文件时注意编码,jsoncpp内部统一用UTF-8,如果源文件是GBK需要先用iconv或自写转换函数处理,否则中文会变成乱码或触发解析异常。
五、常见错误与规避方案
第一类问题是把Value当普通结构体用,比如对不存在的键赋值却忘了先创建对象。jsoncpp允许直接root["a"]["b"] = 1,但若a原本不是对象,会自动转成null再挂子节点,容易掩盖逻辑错误。
第二类是异常安全,解析失败时应释放CharReader实例。用unique_ptr管理读取器生命周期能避免忘记删除带来的内存泄漏。下面示范正确姿势:
Json::CharReaderBuilder builder;
std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
Json::Value root;
std::string errs;
if (!reader->parse(text.data(), text.data() + text.size(), &root, &errs)) {
// 出错时reader离开作用域自动析构
std::cerr << errs << std::endl;
}
第三类是性能,高频解析大文件时反复构造Builder有开销,可把CharReaderBuilder提升为全局或类成员复用。数组特别大时,考虑用Value::resize预分配空间减少内部重排。
掌握以上用法,基本能覆盖C++服务端、客户端里九成的JSON处理场景。把类型判断和错误捕获养成习惯,比事后用调试器查段错误要轻松得多。