4. 文本序列化:JSON格式简介、cJSON库的安装与使用、JSON序列化与反序列化

好,咱们今天聊聊文本序列化里的重头戏——JSON。说实话,在嵌入式世界里,JSON 已经快成“通用语言”了。我最早接触 JSON 是在一个物联网项目里,设备上报数据给云端,服务器那边直接说“你就给我发 JSON 吧”。那时候我还用拼字符串的方式硬凑,后来才发现,有现成的库不用,纯属给自己找麻烦。

4.1 JSON 格式简介

JSON,全称 JavaScript Object Notation,但别被名字骗了——它跟 JavaScript 没半毛钱关系。说白了,JSON 就是一种轻量级的文本数据交换格式。它长什么样?你肯定见过:

{
    "name": "温度传感器",
    "id": 1001,
    "temperature": 25.6,
    "online": true,
    "tags": ["工业", "无线"]
}

嗯,看着眼熟吧?JSON 的核心结构就两种:

  • 对象(Object):用花括号 {} 包裹,里面是键值对。键必须是字符串,值可以是任意类型。
  • 数组(Array):用方括号 [] 包裹,里面是一组有序的值。

值类型包括:字符串、数字、布尔值、null、对象、数组。就这么简单。我在项目中遇到过有人把 JSON 和 XML 对比,说 XML 太啰嗦。确实,JSON 的语法更紧凑,解析也更快。对于嵌入式设备那点可怜的 RAM 来说,JSON 简直是福音。

小提示:JSON 的键必须用双引号,单引号不行。很多新手在这里翻车,包括我当年。

4.2 cJSON 库的安装与使用

手动解析 JSON?别傻了。C 语言里最流行的 JSON 库就是 cJSON。它轻量、开源、纯 C 实现,整个库就两个文件:cJSON.ccJSON.h。我习惯直接把它扔进项目源码里,编译时一起编进去。

4.2.1 安装方式

有两种方式:

  1. 源码拷贝:从 GitHub 下载,把 cJSON.c 和 cJSON.h 放到你的项目目录,直接 #include 就能用。
  2. 包管理器安装:如果你用 Linux,可以 sudo apt install libcjson-dev。编译时链接 -lcjson

我个人推荐第一种。为什么?嵌入式环境千奇百怪,包管理器不一定有。源码拷贝最可控。我曾经在一个 RTOS 项目里用包管理器装,结果版本不对,折腾了半天。后来老老实实拷贝源码,一分钟搞定。

4.2.2 核心 API 速览

cJSON 的 API 设计得很直观。我列几个最常用的:

函数 作用
cJSON_Parse() 把 JSON 字符串解析成 cJSON 对象树
cJSON_Print() 把 cJSON 对象树格式化成字符串
cJSON_GetObjectItem() 从对象中获取指定键的值
cJSON_CreateObject() 创建一个空的 JSON 对象
cJSON_AddStringToObject() 向对象添加字符串字段
cJSON_Delete() 释放 cJSON 对象树的内存

你想想看,就这几个函数,基本覆盖了 90% 的使用场景。别被吓到,其实很简单。

4.3 JSON 序列化与反序列化

好,重头戏来了。序列化就是把 C 语言的结构体变成 JSON 字符串;反序列化就是反过来,把 JSON 字符串解析回结构体。说白了,就是让数据在“内存结构”和“文本格式”之间来回倒腾。

4.3.1 反序列化:从 JSON 字符串到结构体

假设我们有这样一个结构体:

typedef struct {
    char name[32];
    int id;
    float temperature;
    int online;
} SensorData;

现在收到一段 JSON:{"name":"温度传感器","id":1001,"temperature":25.6,"online":true}。怎么把它填进结构体?看代码:

#include <stdio.h>
#include <string.h>
#include "cJSON.h"

int parse_sensor_data(const char *json_str, SensorData *data) {
    cJSON *root = cJSON_Parse(json_str);
    if (root == NULL) {
        printf("JSON 解析失败\n");
        return -1;
    }

    cJSON *item = cJSON_GetObjectItem(root, "name");
    if (cJSON_IsString(item)) {
        strncpy(data->name, item->valuestring, sizeof(data->name) - 1);
    }

    item = cJSON_GetObjectItem(root, "id");
    if (cJSON_IsNumber(item)) {
        data->id = item->valueint;
    }

    item = cJSON_GetObjectItem(root, "temperature");
    if (cJSON_IsNumber(item)) {
        data->temperature = item->valuedouble;
    }

    item = cJSON_GetObjectItem(root, "online");
    if (cJSON_IsBool(item)) {
        data->online = item->valueint;
    }

    cJSON_Delete(root);
    return 0;
}

注意几个坑:

  • 每次取完值要检查类型,cJSON_IsString()cJSON_IsNumber() 这些判断不能省。我曾经偷懒没检查,结果收到一个空字符串,程序直接崩了。
  • 字符串拷贝要用 strncpy,防止缓冲区溢出。
  • 用完记得 cJSON_Delete(),否则内存泄漏。嵌入式环境内存金贵,别不当回事。

4.3.2 序列化:从结构体到 JSON 字符串

反过来,把结构体变成 JSON 字符串:

char *serialize_sensor_data(const SensorData *data) {
    cJSON *root = cJSON_CreateObject();
    if (root == NULL) return NULL;

    cJSON_AddStringToObject(root, "name", data->name);
    cJSON_AddNumberToObject(root, "id", data->id);
    cJSON_AddNumberToObject(root, "temperature", data->temperature);
    cJSON_AddBoolToObject(root, "online", data->online);

    char *json_str = cJSON_Print(root);
    cJSON_Delete(root);
    return json_str;  // 调用者负责 free()
}

这里有个细节:cJSON_Print() 返回的字符串是 malloc 出来的,调用者用完必须 free()。我见过有人忘了 free,结果内存一点点被吃光。嗯,调试这种问题最头疼了。

核心要点:序列化和反序列化就是一对逆操作。序列化用 cJSON_Create* + cJSON_Print,反序列化用 cJSON_Parse + cJSON_GetObjectItem。记住这个套路,JSON 处理就通了。

4.4 知识体系总览

为了让你看得更清楚,我画了一张图,把本章的知识结构串起来:

JSON 序列化与反序列化知识体系 cJSON 库 反序列化 JSON → 结构体 cJSON_Parse() 解析字符串 cJSON_GetObjectItem() 取值 cJSON_Delete() 释放内存 序列化 结构体 → JSON cJSON_CreateObject() 创建对象 cJSON_Add*ToObject() 添加字段 cJSON_Print() 输出字符串 核心:cJSON_Parse ↔ cJSON_Print 是一对逆操作
避坑指南:我曾经在一个项目里连续调用 cJSON_Parse 多次,但忘了每次调用后释放前一次的结果。结果堆内存被撑爆,系统直接重启。排查了半天才发现是内存泄漏。记住:每个 cJSON_Parse 都要配一个 cJSON_Delete,就像 malloc 和 free 一样,成对出现。

好了,JSON 序列化这块就聊到这儿。cJSON 库虽然小,但五脏俱全。你只要掌握了 Parse 和 Print 这对核心操作,再配合 GetObjectItem 和 Add*ToObject,基本就能应对 90% 的 JSON 处理场景。剩下的,遇到具体需求再查文档也不迟。


公众号:蓝海资料掘金营,微信deep3321