
前言C 标准库没有JSON 解析能力。这一点必须先说清楚很多人翻遍string、sstream、iomanip都找不到相关接口因为它们根本不存在。想在 Linux 上用 C 处理 JSON就必须引入第三方库。第二个需要澄清的事实是Linux 上常用的 JSON 库不止一个而且它们的 API 风格差别很大选错了会觉得JSON 怎么这么难用。三大主流选择是nlohmann/json单头文件接口风格高度模仿 STL 容器读写都方便是目前最省心的选择。RapidJSON以解析速度和内存可控为设计目标API 更底层取值前必须逐层校验类型。JsonCpp老牌库很多发行版仓库里都有但推荐的入口已经从Json::Reader换成了CharReaderBuilder。第三个误解关于读取文件。ifs str只会读到第一个空白字符为止而 JSON 文件里到处都是空白。读整份 JSON 必须用流迭代器或者整块读取。本文以 C17 为基准在 Linux 上给出三套可编译的完整示例并说明各自的坑。代码在 GCC 13 / Clang 17 上均可编译。一、环境准备三种库在 Debian / Ubuntu 系上的常见包名如下其他发行版请以你自己的仓库为准库包名引入方式是否要链接nlohmann/jsonnlohmann-json3-dev#include nlohmann/json.hpp否纯头文件RapidJSONrapidjson-dev#include rapidjson/document.h否纯头文件JsonCpplibjsoncpp-dev#include json/json.h是需要-ljsoncpp安装与编译。前两个是纯头文件库编译时不需要额外参数JsonCpp 必须链接用pkg-config取编译与链接参数最省事它会同时给出头文件搜索路径和库名sudo apt update sudo apt install -y nlohmann-json3-dev rapidjson-dev libjsoncpp-dev g -stdc17 -Wall -Wextra read_json.cpp -o read_json g -stdc17 -Wall -Wextra $(pkg-config --cflags --libs jsoncpp) demo.cpp -o demo用 CMake 时nlohmann 提供官方的 CMake 包配置cmake_minimum_required(VERSION 3.16) project(json_demo CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(nlohmann_json 3.6 REQUIRED) add_executable(app main.cpp) target_link_libraries(app PRIVATE nlohmann_json::nlohmann_json)二、读文件先把这个写对不管用哪个库第一步都是把整个文件读成一个std::string。这一步写错后面的解析必然失败。// C17, g -stdc17 read.cpp -o read #include fstream #include iostream #include iterator #include string bool read_whole_file(const std::string path, std::string out) { // 用 binary 打开Linux 上文本/二进制模式没有区别但换到别的平台就有 std::ifstream ifs(path, std::ios::binary); if (!ifs) { return false; // 打开失败一定要检查 } out.assign(std::istreambuf_iteratorchar(ifs), std::istreambuf_iteratorchar()); return true; } int main() { std::string text; if (!read_whole_file(config.json, text)) { std::cerr 无法打开 config.json\n; return 1; } std::cout 读到 text.size() 字节\n; return 0; }std::istreambuf_iterator来自iteratorstd::string::assign的这对迭代器重载会把区间内容整体拷进去一次读完不受空白字符影响。三、nlohmann/json最接近 STL 的用法假设config.json内容如下{ name: demo, port: 8080, debug: false, tags: [cpp, json] }完整的读写程序// C17, g -stdc17 -Wall -Wextra demo_nlohmann.cpp -o demo_nlohmann #include nlohmann/json.hpp #include fstream #include iostream #include string using json nlohmann::json; // 常用的类型别名 int main() { std::ifstream ifs(config.json); if (!ifs) { std::cerr 无法打开 config.json\n; return 1; } json cfg; try { cfg json::parse(ifs); // 直接从输入流解析 } catch (const json::parse_error e) { std::cerr 解析失败: e.what() \n; return 1; } // value()键不存在时返回给的默认值不会抛异常 const std::string name cfg.value(name, std::string(unnamed)); const int port cfg.value(port, 0); const bool debug cfg.value(debug, false); std::cout name port (debug ? on : off) \n; // contains() 需要 nlohmann 3.6 及以上 if (cfg.contains(tags) cfg[tags].is_array()) { for (const auto tag : cfg[tags]) { std::cout tag.getstd::string() \n; } } // 构造新对象并序列化 json out; out[name] name; out[port] port; std::cout out.dump(2) \n; // 参数是缩进空格数 return 0; }几个必须记住的接口语义json::parse(输入, 回调, 是否抛异常, 是否允许注释)。第四个参数ignore_comments默认是false也就是标准 JSON 里不允许注释带注释的文件会解析失败。value(键, 默认值)不抛异常at(键)在键不存在时抛json::out_of_rangeget类型()在类型不符时抛json::type_error。contains(键)从 3.6 版本开始提供更早的版本用count(键)或find(键) ! end()。对非 const的 json 对象使用operator[]访问一个不存在的键会插入一个 null 值。这正是很多人读一下配置文件被改了的原因。四、RapidJSON速度优先校验要靠自己RapidJSON 的 API 更接近手动解析一棵 DOM 树每个取值动作前都要自己确认类型// C17, g -stdc17 -Wall -Wextra demo_rapidjson.cpp -o demo_rapidjson #include rapidjson/document.h #include rapidjson/error/en.h #include rapidjson/stringbuffer.h #include rapidjson/writer.h #include cstdio #include cstddef #include string int main() { const std::string text R({name:demo,port:8080,tags:[cpp,json]}); rapidjson::Document doc; doc.Parse(text.c_str()); if (doc.HasParseError()) { std::printf(解析失败: %s (偏移 %zu)\n, rapidjson::GetParseError_En(doc.GetParseError()), static_caststd::size_t(doc.GetErrorOffset())); return 1; } if (!doc.IsObject() || !doc.HasMember(name) || !doc[name].IsString()) { std::printf(结构不符合预期\n); return 1; } // GetString() 返回的是指向 doc 内部缓冲的指针必须立刻拷成 std::string const std::string name doc[name].GetString(); const int port (doc.HasMember(port) doc[port].IsInt()) ? doc[port].GetInt() : 0; std::printf(%s %d\n, name.c_str(), port); if (doc.HasMember(tags) doc[tags].IsArray()) { for (rapidjson::SizeType i 0; i doc[tags].Size(); i) { if (doc[tags][i].IsString()) { std::printf(%s\n, doc[tags][i].GetString()); } } } // 序列化把 DOM 写进 StringBuffer rapidjson::StringBuffer buffer; rapidjson::Writerrapidjson::StringBuffer writer(buffer); doc.Accept(writer); std::printf(%s\n, buffer.GetString()); return 0; }RapidJSON 的检查顺序是有讲究的HasMember判断键在不在IsXxx判断类型对不对两者都通过才能调对应的GetXxx。跳过任一步都可能触发库内部的断言在 release 构建下断言被禁用时行为就不再受保证。五、JsonCpp老牌库的推荐入口JsonCpp 的Json::Reader已经不再推荐使用官方推荐的是CharReaderBuilder配合newCharReader()// C17, g -stdc17 -Wall -Wextra $(pkg-config --cflags --libs jsoncpp) demo_jsoncpp.cpp -o demo_jsoncpp #include json/json.h #include iostream #include memory #include string int main() { const std::string text R({name:demo,port:8080}); Json::Value root; Json::CharReaderBuilder builder; const std::unique_ptrJson::CharReader reader(builder.newCharReader()); std::string errs; if (!reader-parse(text.data(), text.data() text.size(), root, errs)) { std::cerr 解析失败: errs \n; return 1; } // get(键, 默认值)值不存在时返回默认值比直接 operator[] 安全 const std::string name root.get(name, Json::Value(unnamed)).asString(); const int port root.get(port, Json::Value(0)).asInt(); std::cout name port \n; // 序列化 Json::StreamWriterBuilder wbuilder; wbuilder[indentation] ; const std::string out Json::writeString(wbuilder, root); std::cout out \n; return 0; }JsonCpp 的asInt()/asString()在类型不可转换时会抛Json::LogicError在禁用异常的构建配置下会直接中止所以取值的稳妥顺序是先用isInt()/isString()判断或者用get(键, 默认值)把键不存在的情况挡掉。六、三者的取舍维度nlohmann/jsonRapidJSONJsonCpp集成方式单个头文件多个头文件需要链接库API 风格像 STL 容器value()/at()/getT()DOM 大量IsXxx/GetXxxJson::ValueasXxx()类型安全类型不符抛type_error类型不符需自己判断否则可能断言失败类型不符抛LogicError错误处理异常或返回 discarded 值HasParseError()GetErrorOffset()parse()返回bool 错误串学习成本最低最高中等设计取向易用性优先解析速度与内存可控优先兼容老项目这里不给出性能数字——不同库在不同数据形状上的表现差别很大拿别人的基准当结论没有意义。真要选型用你自己真实的 JSON 样本跑一遍才是最靠谱的。常见坑点场景❌ 错误写法✅ 正确写法说明读整个文件ifs text;用std::istreambuf_iterator整块读operator遇到空白就停只会读到第一个词忽略打开失败直接std::ifstream ifs(path);就用先if (!ifs) { ... }文件不存在时后面拿到的是空串报错信息指向解析器方向全错nlohmann 读到 null对非 const json 写cfg[key]读值cfg.value(key, 默认值)或cfg.at(key)非 const 的operator[]遇到不存在的键会插入 null忘捕获异常auto j json::parse(s);不做保护try/catch (const json::parse_error)格式错误会抛异常未捕获直接终止RapidJSON 悬垂指针const char* p doc[name].GetString();之后 doc 销毁才用p立刻std::string name doc[name].GetString();指针指向 Document 内部缓冲Document 一销毁就无效RapidJSON 跳过类型校验直接doc[port].GetInt()先HasMemberIsInt再取值键不存在或类型不符会触发库内断言JsonCpp 旧入口Json::Reader reader; reader.parse(...)Json::CharReaderBuildernewCharReader()Json::Reader已不再是推荐用法JsonCpp 直接 asXxxroot[port].asInt()root.get(port, Json::Value(0)).asInt()键不存在时operator[]会构造 nullasInt()对它抛LogicError总结环节关键做法选库新项目优先 nlohmann/json对解析开销极度敏感时评估 RapidJSON维护老代码用 JsonCpp装库nlohmann-json3-dev/rapidjson-dev/libjsoncpp-dev前两个头文件即可JsonCpp 要链接读文件std::ios::binary打开检查打开结果用流迭代器整块读解析一律处理失败路径nlohmann 捕获parse_errorRapidJSON 查HasParseErrorJsonCpp 检查返回的bool取值先确认键存在和类型正确再取值优先用不抛异常或带默认值的接口生命周期RapidJSON 的GetString()是指向 Document 内部的指针必须及时拷贝走在 Linux 上用 C 解析 JSON难点从来不在JSON 语法而在库的边界条件文件打不开怎么办、键不在怎么办、类型不对怎么办、解析出来的指针能活多久。把这四个问题在代码里各有明确答案剩下的就是查文档而已。