一、JSON 文件解析实战:使用 cJSON 库解析 JSON 格式文件

说实话,JSON 解析这事儿,我当年刚入行时也头疼过一阵子。那时候项目里要对接一个第三方 API,返回的全是 JSON 格式的数据。我心想,不就是个文本格式吗?自己写个解析器不就完了?结果折腾了两天,各种边界情况处理得我头皮发麻。后来同事告诉我:「别傻了,用 cJSON 啊!」

嗯,从那以后,我就再也没自己手写过 JSON 解析器了。

1.1 什么是 cJSON?为什么选它?

cJSON 是一个轻量级的 C 语言 JSON 解析库。它只有一个 .c 文件和一个 .h 文件,加起来不到 2000 行代码。你想想看,这么小的体量,却能完成 JSON 的序列化、反序列化、增删改查全套操作,是不是很划算?

我个人习惯在嵌入式项目里用它。因为资源受限的环境下,你不可能去装什么 Python 或者用 Java 的 Jackson 库。cJSON 纯 C 实现,没有依赖,直接拖进工程就能编译。说白了,它就是为 C 程序员量身定做的 JSON 瑞士军刀。

核心优势:

  • 纯 C 语言实现,零依赖
  • 支持 ANSI C(C89/C90),兼容性极强
  • 解析速度快,内存占用低
  • API 设计简洁,上手快

1.2 安装与引入 cJSON

你不需要什么复杂的安装步骤。直接从 GitHub 上把 cJSON.ccJSON.h 下载下来,放到你的项目目录里,然后在代码里 #include "cJSON.h" 就行了。

我记得有一次在 Linux 下做项目,直接用包管理器安装也很方便:

# Ubuntu/Debian
sudo apt-get install libcjson-dev

# CentOS/RHEL
sudo yum install libcjson-devel

# macOS (Homebrew)
brew install cjson

编译的时候记得链接库:

gcc -o myapp myapp.c -lcjson

1.3 JSON 文件的结构与 cJSON 的数据模型

我们先看一个典型的 JSON 文件长什么样。假设我们有一个学生信息文件 student.json

{
  "name": "张三",
  "age": 20,
  "is_graduated": false,
  "scores": [88, 92, 76],
  "address": {
    "city": "北京",
    "district": "海淀区"
  }
}

cJSON 内部是怎么表示这个结构的呢?它用了一个叫 cJSON 的结构体,每个节点都有类型、值、子节点和兄弟节点指针。说白了,它把 JSON 解析成了一棵多叉树。

小技巧: 我个人习惯把 JSON 的根节点想象成「根目录」,对象就是「文件夹」,数组就是「文件列表」,字符串和数字就是「文件」。这样理解起来特别直观。

cJSON 支持的数据类型:

类型常量 对应 JSON 类型 说明
cJSON_Invalid 无效 未初始化或解析失败
cJSON_False false 布尔假值
cJSON_True true 布尔真值
cJSON_NULL null 空值
cJSON_Number 数字 整数或浮点数
cJSON_String 字符串 双引号括起来的文本
cJSON_Array 数组 有序的值集合
cJSON_Object 对象 无序的键值对集合

1.4 核心 API 速览

你不需要记住所有 API,常用的就那么几个。我列出来给你看:

// 解析 JSON 字符串
cJSON *cJSON_Parse(const char *value);

// 从文件读取并解析
cJSON *cJSON_ParseWithLength(const char *value, size_t buffer_length);

// 获取对象中的指定键
cJSON *cJSON_GetObjectItem(const cJSON *object, const char *string);

// 获取数组元素个数
int cJSON_GetArraySize(const cJSON *array);

// 获取数组中的第 n 个元素
cJSON *cJSON_GetArrayItem(const cJSON *array, int index);

// 判断节点类型
int cJSON_IsInvalid(const cJSON *item);
int cJSON_IsBool(const cJSON *item);
int cJSON_IsNumber(const cJSON *item);
int cJSON_IsString(const cJSON *item);
int cJSON_IsArray(const cJSON *item);
int cJSON_IsObject(const cJSON *item);

// 获取值
int cJSON_GetBoolValue(const cJSON *item);
double cJSON_GetNumberValue(const cJSON *item);
const char *cJSON_GetStringValue(const cJSON *item);

// 释放内存
void cJSON_Delete(cJSON *item);

注意: 我曾经犯过一个低级错误——解析完 JSON 后忘了调用 cJSON_Delete()。结果程序跑着跑着内存就爆了。尤其是嵌入式设备,内存本来就紧张,这种泄漏很致命。所以请记住:谁分配,谁释放

1.5 实战:解析 student.json

光说不练假把式。我们直接写代码来解析上面那个 student.json 文件。

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

// 读取整个文件到字符串
char *read_file(const char *filename) {
    FILE *fp = fopen(filename, "rb");
    if (!fp) {
        printf("无法打开文件: %s\n", filename);
        return NULL;
    }

    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    rewind(fp);

    char *buffer = (char *)malloc(size + 1);
    if (!buffer) {
        fclose(fp);
        return NULL;
    }

    fread(buffer, 1, size, fp);
    buffer[size] = '\0';
    fclose(fp);
    return buffer;
}

int main() {
    // 1. 读取文件
    char *json_str = read_file("student.json");
    if (!json_str) {
        return 1;
    }

    // 2. 解析 JSON
    cJSON *root = cJSON_Parse(json_str);
    free(json_str);  // 文件内容用完了,释放掉

    if (!root) {
        const char *error_ptr = cJSON_GetErrorPtr();
        if (error_ptr) {
            printf("解析错误在: %s\n", error_ptr);
        }
        return 1;
    }

    // 3. 提取数据
    cJSON *name = cJSON_GetObjectItem(root, "name");
    cJSON *age = cJSON_GetObjectItem(root, "age");
    cJSON *is_grad = cJSON_GetObjectItem(root, "is_graduated");
    cJSON *scores = cJSON_GetObjectItem(root, "scores");
    cJSON *address = cJSON_GetObjectItem(root, "address");

    // 4. 打印结果
    if (cJSON_IsString(name) && name->valuestring) {
        printf("姓名: %s\n", name->valuestring);
    }

    if (cJSON_IsNumber(age)) {
        printf("年龄: %d\n", age->valueint);
    }

    if (cJSON_IsBool(is_grad)) {
        printf("是否毕业: %s\n", cJSON_IsTrue(is_grad) ? "是" : "否");
    }

    // 遍历数组
    if (cJSON_IsArray(scores)) {
        int count = cJSON_GetArraySize(scores);
        printf("成绩: ");
        for (int i = 0; i < count; i++) {
            cJSON *item = cJSON_GetArrayItem(scores, i);
            if (cJSON_IsNumber(item)) {
                printf("%.0f ", item->valuedouble);
            }
        }
        printf("\n");
    }

    // 解析嵌套对象
    if (cJSON_IsObject(address)) {
        cJSON *city = cJSON_GetObjectItem(address, "city");
        cJSON *district = cJSON_GetObjectItem(address, "district");
        if (cJSON_IsString(city) && cJSON_IsString(district)) {
            printf("地址: %s %s\n", city->valuestring, district->valuestring);
        }
    }

    // 5. 释放内存
    cJSON_Delete(root);
    return 0;
}

运行结果:

姓名: 张三
年龄: 20
是否毕业: 否
成绩: 88 92 76 
地址: 北京 海淀区

1.6 避坑指南:我踩过的那些坑

做 JSON 解析,有几个地方特别容易出问题。我一个个跟你说:

  1. 空指针检查不能省cJSON_GetObjectItem 返回 NULL 时,千万别直接访问它的成员。我曾经在项目里没做检查,结果线上程序直接段错误,被运维大哥骂了一顿。
  2. 字符串生命周期cJSON_GetStringValue 返回的指针指向 cJSON 内部的内存。如果你 cJSON_Delete 了,这个指针就变成野指针了。所以要么在 delete 之前用完,要么用 strdup 复制一份。
  3. 大文件处理:如果 JSON 文件很大(比如几 MB),一次性读到内存里再解析可能会爆内存。我建议用流式解析或者分块读取。不过 cJSON 本身不支持流式解析,你可以考虑用 cJSON_ParseWithLength 分段处理。
  4. 编码问题:cJSON 默认只支持 UTF-8 编码。如果你遇到 GBK 或者其他编码的 JSON 文件,记得先转码再解析。我当年在某个国产系统上就吃过这个亏。

我的习惯: 每次解析完 JSON,我都会写一个简单的单元测试,把每个字段的类型和值都验证一遍。这样即使上游接口改了格式,我这边也能第一时间发现,而不是等到程序崩溃了才去排查。

1.7 知识体系总览

下面这张图是我自己整理的 cJSON 解析流程,你可以把它当作一个检查清单:

cJSON 解析流程 1. 读取 JSON 文件 2. cJSON_Parse() 3. 检查 root 是否为 NULL 获取错误信息 4. GetObjectItem 提取字段 5. cJSON_Delete() 释放 常见错误 • 忘记释放内存 • 空指针未检查 • 类型判断遗漏 • 字符串生命周期 • 大文件一次性加载 • 编码不匹配 • 嵌套层级过深 • 数组越界访问 • 多线程安全问题 • 未处理 null 值 • 浮点数精度丢失 • 忘记包含头文件 • 链接时未加 -lcjson

1.8 小结

cJSON 这个库,说白了就是「小身材,大能量」。你只要记住几个核心 API,再注意一下内存管理和类型检查,基本就能应付 90% 的 JSON 解析场景了。

我个人建议你把这个实战代码自己敲一遍,然后换一个自己的 JSON 文件试试。比如把你的项目配置文件拿来解析一下。只有亲手写过,才能真正理解那些 API 的用法和坑点。

嗯,这一章就到这里。记住:解析 JSON 不难,难的是把每一步都做对、做稳。


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