news 2026/10/12 7:14:35

C/C++ JSON实战:字符串转JSON、数组解析与序列化避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C/C++ JSON实战:字符串转JSON、数组解析与序列化避坑指南

1. 从一段字符串说起:为什么C/C++里处理JSON总让人头疼

如果你写过C或者C++,大概率遇到过这样的场景:设备上报了一串数据,格式是JSON,你需要把它解析出来,取出某个字段做判断;或者反过来,你采集到的几个传感器数值,需要拼成JSON发给上层服务。听起来很简单对吧?但真动手的时候,很多人第一反应是——C语言里连个字典都没有,字符串操作又那么原始,这活儿到底怎么干?

我刚开始接触这类需求的时候,也走过不少弯路。最早的做法是直接用strstr去查找字段名,然后手动截取冒号后面的内容,再自己去掉引号。这种方法对付一两个固定字段还行,一旦JSON嵌套一层,或者数组里再套对象,代码就彻底失控了。后来也试过自己写一个简易的解析器,状态机画了一整页纸,结果边界情况层出不穷:转义字符没处理、数字和字符串分不清、空数组和空对象搞混。折腾了好几天,最后发现——与其自己造轮子,不如找一个成熟但轻量的库,把精力放在业务逻辑上。

这篇内容就是把我这些年在这个环节上积累的经验整理出来。核心围绕四件事:字符串转JSON、JSON转字符串、数组解析、数组添加。我会讲清楚每一步为什么这么做、选哪个库、怎么避坑,以及那些文档里不会写的实操细节。无论你是刚接触C/C++的新手,还是已经用过一些JSON库但总觉得不够顺手的老手,应该都能从中找到有用的东西。

提示:本文所有代码示例基于C++11及以上标准,C语言部分会单独说明。涉及的库均为开源且广泛使用的方案,不涉及任何特定平台绑定。

2. 选库这件事:为什么我最终选择了这几个方案

2.1 常见JSON库的横向对比

在C/C++生态里,处理JSON的库不算少,但真正适合嵌入到项目里的其实就那么几个。我整理了一个对比表格,方便你根据自己的场景做选择:

库名称语言是否需编译内存管理数组支持适用场景
cJSONC源码集成手动释放完善嵌入式、纯C项目
nlohmann/jsonC++单头文件RAII自动完善现代C++项目、快速开发
RapidJSONC++单头文件可选完善高性能场景、大数据量
jsoncppC++需编译RAII自动完善传统C++项目

这张表里,cJSON和nlohmann/json是我用得最多的两个。cJSON的优势在于纯C、体积极小、源码就两个文件,直接拖进工程就能用,特别适合资源受限的环境。nlohmann/json的优势在于语法极其自然,像写Python一样操作JSON,代码可读性极高,适合业务逻辑复杂的场景。

RapidJSON性能最好,但API相对繁琐,学习曲线陡一些。jsoncpp比较老牌,但近年来用得越来越少,新项目不太推荐。

2.2 选型背后的逻辑:不是越强越好

很多人选库的时候喜欢看性能跑分,觉得解析速度越快越好。但实际项目中,解析性能往往不是瓶颈。一个典型的JSON报文可能就几百字节到几KB,解析耗时在微秒级别,相比网络传输和业务处理,完全可以忽略。

真正影响开发效率的是API的易用性和内存安全性。我见过太多项目因为手动管理cJSON的内存,在某个分支忘记cJSON_Delete,导致内存泄漏,跑了几天才暴露出来。也见过用nlohmann/json因为异常处理没做好,一个格式错误的报文直接让程序崩溃。

所以我的建议是:

  • 如果是纯C环境或者资源极度受限(比如单片机),选cJSON,但一定要封装一层内存管理。
  • 如果是C++11及以上,优先选nlohmann/json,开发效率高,内存自动管理,不容易出错。
  • 如果是性能敏感的大数据量场景(比如每秒解析几万条),考虑RapidJSON。

注意:无论选哪个库,都不要在核心业务代码里直接裸用它的API。封装一层自己的接口,后续换库或者加日志、加校验都会方便很多。

2.3 集成方式:单头文件 vs 源码编译

nlohmann/json和RapidJSON都是单头文件库,直接把.hpp拖进项目,#include就能用,不需要改CMake或者Makefile。这一点对快速原型开发非常友好。

cJSON需要把.c和.h两个文件加入编译,如果是嵌入式环境,可能还需要配置malloc和free的钩子函数。我一般会在cJSON之上再包一层,比如定义一个JsonWrapper结构体,把创建、解析、释放都封装成函数,业务代码只调封装后的接口。

这里有个小技巧:如果你用的是cJSON,可以在项目里定义一个宏,统一替换cJSON_malloc和cJSON_free,方便后续接入自己的内存池或者做内存统计。

3. 字符串转JSON:解析过程中的那些坑

3.1 基本解析流程与代码骨架

先看一个最基础的例子,用nlohmann/json把一个字符串解析成JSON对象:

#include <nlohmann/json.hpp> #include <string> #include <iostream> using json = nlohmann::json; int main() { std::string raw = R"({"name":"sensor01","value":36.5,"status":"ok"})"; try { json j = json::parse(raw); std::string name = j["name"]; double value = j["value"]; std::string status = j["status"]; std::cout << name << " " << value << " " << status << std::endl; } catch (json::parse_error& e) { std::cerr << "解析失败: " << e.what() << std::endl; } return 0; }

这段代码看起来很简单,但里面有几个关键点值得展开说。

第一,json::parse会抛出异常,必须用try-catch包住。很多人图省事不写异常处理,结果线上遇到一个格式错误的报文,整个服务就挂了。我一般会在catch里记录原始字符串,方便排查问题。

第二,用j["name"]取值的时候,如果字段不存在,nlohmann/json会返回一个null值,不会抛异常。但如果你直接把它赋值给std::string,就会得到一个空字符串。这个行为有时候会掩盖问题,建议用j.contains("name")先判断,或者用j.value("name", "default")指定默认值。

第三,对于数字类型,j["value"]返回的是json对象,赋值给double时会自动转换。但如果原始JSON里这个字段是字符串类型的"36.5",转换就会抛异常。所以解析之前最好对字段类型心里有数。

3.2 嵌套对象和数组的解析策略

实际报文往往不是扁平的,而是嵌套的。比如:

{ "device": { "id": "A001", "sensors": [ {"type": "temp", "value": 36.5}, {"type": "humidity", "value": 62.0} ] } }

解析这种结构,用nlohmann/json可以这样写:

json j = json::parse(raw); std::string deviceId = j["device"]["id"]; for (auto& sensor : j["device"]["sensors"]) { std::string type = sensor["type"]; double value = sensor["value"]; // 处理每个传感器 }

这里有个容易踩的坑:如果sensors字段不存在,j["device"]["sensors"]会返回null,对它做范围for循环会直接抛异常。所以稳妥的做法是先判断:

if (j.contains("device") && j["device"].contains("sensors") && j["device"]["sensors"].is_array()) { for (auto& sensor : j["device"]["sensors"]) { // ... } }

用cJSON的话,解析嵌套结构需要一层层调用cJSON_GetObjectItem,代码会啰嗦一些:

cJSON* root = cJSON_Parse(raw); if (root == NULL) { // 解析失败 return; } cJSON* device = cJSON_GetObjectItem(root, "device"); if (device == NULL) { cJSON_Delete(root); return; } cJSON* sensors = cJSON_GetObjectItem(device, "sensors"); if (sensors == NULL || !cJSON_IsArray(sensors)) { cJSON_Delete(root); return; } int count = cJSON_GetArraySize(sensors); for (int i = 0; i < count; i++) { cJSON* sensor = cJSON_GetArrayItem(sensors, i); cJSON* type = cJSON_GetObjectItem(sensor, "type"); cJSON* value = cJSON_GetObjectItem(sensor, "value"); // 处理 } cJSON_Delete(root);

可以看到,cJSON的每一步都要判空,而且最后必须记得cJSON_Delete。我一般会写一个辅助宏来简化判空逻辑,但即便如此,代码量还是比nlohmann/json多不少。

3.3 解析时的编码与转义问题

JSON标准要求字符串使用UTF-8编码,但实际项目中,你可能会遇到GBK编码的中文字符串,或者包含转义字符的内容。比如:

{"message": "温度过高\n请检查"}

这里的\n是转义字符,解析后应该变成真正的换行符。nlohmann/json和cJSON都会自动处理标准转义,但如果你自己手动解析字符串,就很容易漏掉。

另一个常见问题是BOM头。有些系统生成的JSON文件会在开头带一个UTF-8 BOM(EF BB BF),直接解析会失败。处理方法是解析前先检查并跳过BOM:

if (raw.size() >= 3 && (unsigned char)raw[0] == 0xEF && (unsigned char)raw[1] == 0xBB && (unsigned char)raw[2] == 0xBF) { raw = raw.substr(3); }

还有一个坑是数字精度。JSON里的数字如果很大,比如超过double的精度范围,解析后可能会丢失精度。如果业务涉及大整数(比如订单号),建议在JSON里用字符串存储,解析后再按需转换。

提示:解析之前先做一次格式校验,可以用在线的JSON校验工具确认报文是否合法。很多解析失败的问题,根源其实是发送方生成的JSON本身就不合法。

4. JSON转字符串:序列化时的格式控制与性能考量

4.1 基本序列化与紧凑格式

把JSON对象转回字符串,用nlohmann/json只需要一行:

json j; j["name"] = "sensor01"; j["value"] = 36.5; j["status"] = "ok"; std::string output = j.dump(); // 输出: {"name":"sensor01","status":"ok","value":36.5}

注意,nlohmann/json默认会按key的字母顺序排序,所以输出顺序可能和你插入的顺序不一样。如果对顺序有要求,需要用ordered_json或者自己控制。

dump()默认生成紧凑格式,没有空格和换行。如果希望输出可读性更好的格式,可以传参数:

std::string pretty = j.dump(4); // 缩进4个空格

cJSON的序列化:

cJSON* root = cJSON_CreateObject(); cJSON_AddStringToObject(root, "name", "sensor01"); cJSON_AddNumberToObject(root, "value", 36.5); cJSON_AddStringToObject(root, "status", "ok"); char* output = cJSON_PrintUnformatted(root); // 使用output free(output); cJSON_Delete(root);

cJSON_Print会生成带缩进的格式,cJSON_PrintUnformatted生成紧凑格式。注意返回的字符串是用malloc分配的,必须手动free,否则内存泄漏。

4.2 浮点数精度与特殊字符处理

浮点数序列化是个容易出问题的地方。比如36.5这个值,dump()后可能变成36.5,但也可能变成36.50000000000001,取决于底层浮点表示。如果对精度敏感,建议在序列化前先做四舍五入,或者用字符串存储。

特殊字符的处理也需要注意。比如字符串里包含双引号、反斜杠、控制字符,序列化时必须正确转义。nlohmann/json和cJSON都会自动处理,但如果你自己拼接字符串,就很容易出错。

我见过一个案例:某系统把用户输入直接拼进JSON字符串,用户输入了带双引号的内容,导致生成的JSON格式错误,接收方解析失败。正确的做法是始终用库的序列化接口,不要手动拼接。

4.3 大对象序列化的性能优化

当JSON对象很大(比如包含几千个数组元素)时,序列化可能成为性能瓶颈。几个优化思路:

第一,避免不必要的拷贝。nlohmann/json的dump()会返回一个新的std::string,如果对象很大,这个拷贝开销不小。可以考虑用dump的输出直接写入目标缓冲区。

第二,使用dump的ensure_ascii参数。默认情况下,非ASCII字符会被转义成\uXXXX形式,这会增加输出长度。如果接收方支持UTF-8,可以设置ensure_ascii=false:

std::string output = j.dump(-1, ' ', false, json::error_handler_t::replace);

第三个参数就是ensure_ascii,设为false可以保留原始UTF-8字符。

第三,对于RapidJSON,可以用Writer直接写入预分配的缓冲区,避免动态内存分配。这在嵌入式环境里特别有用。

注意:序列化后的字符串如果用于网络传输,记得确认接收方的编码支持。有些老系统只认ASCII,这时候就必须保持ensure_ascii=true。

5. 数组解析:从JSON数组到C/C++数据结构

5.1 遍历数组的几种方式

JSON数组解析成C/C++数组或容器,最直接的方式是遍历。用nlohmann/json:

json j = json::parse(R"([1, 2, 3, 4, 5])"); // 方式一:范围for for (auto& element : j) { int val = element; // 处理val } // 方式二:下标访问 for (size_t i = 0; i < j.size(); i++) { int val = j[i]; // 处理val } // 方式三:转成vector std::vector<int> vec = j.get<std::vector<int>>();

方式三最简洁,但要求数组里所有元素类型一致,否则会抛异常。如果数组里混合了不同类型,就只能用前两种方式,逐个判断类型。

用cJSON遍历数组:

cJSON* array = cJSON_Parse(R"([1, 2, 3, 4, 5])"); int size = cJSON_GetArraySize(array); for (int i = 0; i < size; i++) { cJSON* item = cJSON_GetArrayItem(array, i); if (cJSON_IsNumber(item)) { int val = item->valueint; // 处理val } } cJSON_Delete(array);

5.2 数组元素类型不一致时的处理

实际报文里,数组元素类型不一致的情况很常见。比如:

[ {"type": "temp", "value": 36.5}, {"type": "humidity", "value": 62}, {"type": "pressure", "value": 1013.25} ]

这里每个元素都是对象,但value字段有的是浮点数,有的是整数。用nlohmann/json处理时,可以统一按double读取:

for (auto& item : j) { std::string type = item["type"]; double value = item["value"].get<double>(); // 处理 }

get<double>()会把整数也转成浮点数,不会丢精度(在double范围内)。

如果数组里既有对象又有字符串,就需要先判断类型:

for (auto& item : j) { if (item.is_object()) { // 按对象处理 } else if (item.is_string()) { // 按字符串处理 } else if (item.is_number()) { // 按数字处理 } }

5.3 嵌套数组的递归解析

嵌套数组,比如[[1,2],[3,4],[5,6]],需要递归或者双层循环处理:

json j = json::parse(R"([[1,2],[3,4],[5,6]])"); for (auto& row : j) { for (auto& col : row) { int val = col; // 处理val } }

如果嵌套层数不确定,可以写一个递归函数:

void processArray(const json& j) { if (j.is_array()) { for (auto& item : j) { processArray(item); } } else if (j.is_number()) { // 处理叶子节点 } }

这种递归方式在处理树形结构数据时特别有用,比如组织架构、文件目录等。

提示:递归解析时要注意栈深度。如果嵌套层数可能很深(比如超过1000层),建议改用迭代方式加显式栈,避免栈溢出。

6. 数组添加:动态构建JSON数组的实操细节

6.1 从空数组开始逐步添加元素

构建JSON数组,最常用的方式是从空数组开始,逐个添加元素。用nlohmann/json:

json arr = json::array(); arr.push_back(1); arr.push_back("hello"); arr.push_back({{"key", "value"}}); std::string output = arr.dump(); // 输出: [1,"hello",{"key":"value"}]

json::array()创建一个空数组,push_back添加元素。注意,nlohmann/json的数组可以混合类型,这一点比C++的std::vector灵活。

用cJSON:

cJSON* arr = cJSON_CreateArray(); cJSON_AddItemToArray(arr, cJSON_CreateNumber(1)); cJSON_AddItemToArray(arr, cJSON_CreateString("hello")); cJSON* obj = cJSON_CreateObject(); cJSON_AddStringToObject(obj, "key", "value"); cJSON_AddItemToArray(arr, obj); char* output = cJSON_PrintUnformatted(arr); free(output); cJSON_Delete(arr);

cJSON的cJSON_AddItemToArray会把元素的所有权转移给数组,所以最后只需要cJSON_Delete(arr),不需要单独释放每个元素。

6.2 批量添加与性能注意事项

如果需要添加大量元素,逐个push_back可能会有性能问题,因为数组可能需要多次扩容。nlohmann/json的数组底层是std::vector,扩容策略和vector类似。如果预先知道元素数量,可以先reserve:

json arr = json::array(); arr.get_ref<json::array_t&>().reserve(1000); for (int i = 0; i < 1000; i++) { arr.push_back(i); }

不过说实话,对于大多数场景,这点性能差异可以忽略。除非你要添加几十万个元素,否则没必要过度优化。

cJSON的数组是链表结构,添加元素是O(1)操作,不需要预分配。但链表的内存开销比连续数组大,遍历时缓存局部性也差一些。

6.3 在已有数组中插入和删除元素

除了在末尾添加,有时候需要在中间插入或删除元素。nlohmann/json支持insert和erase:

json arr = {1, 2, 3, 4, 5}; // 在索引2处插入99 arr.insert(arr.begin() + 2, 99); // arr变成 [1,2,99,3,4,5] // 删除索引0处的元素 arr.erase(arr.begin()); // arr变成 [2,99,3,4,5]

cJSON没有直接的插入接口,需要先创建新数组,按顺序添加元素,再替换原数组。或者用cJSON_InsertItemInArray(部分版本支持)。

这里有个坑:nlohmann/json的insert和erase会改变数组大小,如果之前保存了迭代器或引用,可能会失效。操作后需要重新获取。

注意:在遍历数组的过程中删除元素,容易导致迭代器失效或者跳过元素。如果确实需要边遍历边删除,建议先记录要删除的索引,遍历结束后统一删除。

7. 实战中的经验与避坑清单

7.1 内存管理:最容易出问题的地方

cJSON的内存管理是手动模式,每个cJSON_Create*和cJSON_Parse返回的对象都需要cJSON_Delete释放。我总结了几条规则:

  • cJSON_Parse返回的根对象,最后必须cJSON_Delete。
  • cJSON_Print返回的字符串,必须free。
  • cJSON_AddItemToArray和cJSON_AddItemToObject会转移所有权,不需要单独释放被添加的元素。
  • 如果解析失败,cJSON_Parse返回NULL,不需要释放。

nlohmann/json是RAII模式,对象析构时自动释放内存,基本不用担心泄漏。但有一个例外:如果用了json::parse的allow_exceptions=false版本,解析失败会返回一个discarded值,后续操作需要判断。

7.2 错误处理:不要假设输入永远合法

线上环境里,你永远不知道会收到什么样的报文。可能缺字段、可能类型不对、可能整个JSON就是坏的。我的做法是:

  • 解析时用try-catch包住,记录原始报文和错误信息。
  • 取字段前先判断存在性和类型。
  • 对于关键字段,设置默认值或走降级逻辑。
  • 记录解析失败的次数和频率,方便监控告警。

7.3 性能与可读性的平衡

有些团队为了追求极致性能,把所有JSON操作都写成手动字符串拼接,结果代码难以维护,bug频出。我的观点是:除非性能测试证明JSON处理是瓶颈,否则优先保证代码可读性和安全性。

用成熟的库,代码量少,出错概率低,后续维护成本也低。真到了性能瓶颈,再针对性优化也不迟。

7.4 跨平台兼容性注意事项

如果你的代码需要在多个平台编译,注意以下几点:

  • 浮点数序列化的结果可能因平台而异,如果对格式有严格要求,建议统一用字符串存储。
  • 字节序问题:JSON本身是文本格式,不涉及字节序,但如果JSON里嵌入了二进制数据的Base64编码,解码时要注意。
  • 编译器差异:不同编译器对C++标准的支持程度不同,nlohmann/json需要C++11,RapidJSON对C++版本要求更低。

我在实际项目中遇到过一个问题:在某个平台上double序列化后多了一位小数,导致接收方校验失败。后来改成用std::ostringstream手动控制精度,才解决。

8. 一个完整的示例:从解析到修改再到序列化

最后,用一个完整的例子把前面讲的内容串起来。假设我们收到一个设备状态报文,需要解析出来,给每个传感器数组添加一个时间戳字段,再序列化发回去。

#include <nlohmann/json.hpp> #include <iostream> #include <string> using json = nlohmann::json; int main() { std::string raw = R"({ "device": "A001", "sensors": [ {"type": "temp", "value": 36.5}, {"type": "humidity", "value": 62.0} ] })"; try { json j = json::parse(raw); // 解析 std::string device = j.value("device", "unknown"); std::cout << "设备: " << device << std::endl; // 数组解析与添加 if (j.contains("sensors") && j["sensors"].is_array()) { for (auto& sensor : j["sensors"]) { std::string type = sensor.value("type", ""); double value = sensor.value("value", 0.0); std::cout << "传感器: " << type << " = " << value << std::endl; // 添加时间戳字段 sensor["timestamp"] = 1699999999; } } // 序列化 std::string output = j.dump(2); std::cout << "输出:\n" << output << std::endl; } catch (json::parse_error& e) { std::cerr << "解析错误: " << e.what() << std::endl; } catch (json::type_error& e) { std::cerr << "类型错误: " << e.what() << std::endl; } return 0; }

这个例子里,j.value("device", "unknown")在字段不存在时返回默认值,比直接用j["device"]更安全。sensor["timestamp"] = 1699999999直接在遍历过程中修改了数组元素,因为用的是引用,修改会反映到原数组。

用cJSON实现同样的逻辑会复杂一些,需要手动创建时间戳对象并添加到每个传感器对象中,最后别忘了释放内存。

我在实际使用中发现,nlohmann/json的value方法配合默认值,能省掉大量判空代码,代码可读性提升明显。而cJSON虽然啰嗦,但在纯C环境和资源受限场景下依然是首选。选哪个,取决于你的项目约束和团队习惯。

最后再分享一个小技巧:如果JSON报文里字段名经常变,可以考虑用json::object的迭代器遍历所有key,做通用处理,而不是硬编码字段名。这样面对协议变更时,代码改动量会小很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 7:10:10

SQL Server数据库质疑与损坏修复:DBCC CHECKDB实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/12 7:09:05

pstack命令实战:Linux线程死锁与假死排查指南

你有没有遇到过这种情况&#xff1a;线上服务进程明明还活着&#xff0c;端口也通&#xff0c;请求却全部卡住不返回。CPU 不高&#xff0c;日志没有异常&#xff0c;内存也不见涨&#xff0c;就像一个人睁着眼睛但已经没有反应了。我第一次处理这种“假死”问题时&#xff0c;…

作者头像 李华
网站建设 2026/10/12 7:07:37

Claude Code Mod定制全攻略:从配置到自定义命令与钩子脚本

如果你已经装了Claude Code并且觉得它的默认行为不够顺手&#xff0c;大概率会产生一个念头&#xff1a;能不能把它改了&#xff1f;能&#xff0c;而且可改的空间比你想的大不少。这篇文章讲的是Claude Code Mod——从零把官方版本装好&#xff0c;再用配置、指令文件、自定义…

作者头像 李华
网站建设 2026/10/12 7:06:54

Linux安全加固实操:SSH防护、防火墙配置与日志审计

干了这么多年 Linux 运维&#xff0c;我见过太多“裸奔”的服务器&#xff1a;SSH 跑在默认 22 端口、root 密码简单得离谱、防火墙该开的不该开的全开着、日志几个月没人翻。说实话&#xff0c;每次看到这种状况我都后背发凉&#xff0c;因为中招往往不是运气问题&#xff0c;…

作者头像 李华
网站建设 2026/10/12 7:06:45

AI助力数学建模论文复现:9大技巧与10款软件实战指南

读数学建模论文&#xff0c;最痛苦的不是看不懂模型&#xff0c;而是明明把公式推明白了、算法也理解了&#xff0c;换到自己手上想复现一版&#xff0c;结果一跑全是问题。数据对不上、约束漏项、符号体系混乱&#xff0c;一个细节能卡一整天。我这几年代数模、带学生、帮人审…

作者头像 李华
网站建设 2026/10/12 7:05:21

WinForm DataGridView真分页控件:SQL截断+内存缓存+UI状态隔离

简介&#xff1a;这是一份面向WinForm初学者与中级开发者的DataGridView分页控件封装方案&#xff0c;专为解决Windows桌面应用中数据量较大时表格展示卡顿、滚动不友好等常见问题而设计。控件已封装为独立可复用组件&#xff0c;集成简单、调用直观&#xff0c;附带完整源码便…

作者头像 李华