【json】cJSON库解析文件

概要

cJSON 是一个用 ANSI C 编写的轻量级 JSON 解析/生成库,MIT 许可,核心通常只有 cJSON.c 和 cJSON.h 两个文件。它非常适合嵌入式、C 项目配置文件、中小型 JSON 数据的解析和生成。cJSON 会把 JSON 文本解析成一棵内存中的节点树,然后通过 API 遍历和读取。

下面按"解析 JSON 文件"这个使用场景全面介绍。


1. cJSON 特点与局限

特点:

  • 纯 C,ANSI C,可移植性好。
  • 单文件,容易集成到工程。
  • 支持标准 JSON:对象、数组、字符串、数字、布尔、null。
  • 支持解析、生成、修改、打印、压缩。
  • API 简单,命名统一以 cJSON_ 开头。
  • MIT 许可证,商用友好。

局限:

  • 数字内部用 double 存储,大整数可能精度丢失。
  • 不支持 JSON 注释、单引号、尾随逗号等非标准语法。
  • 不支持流式解析,需要一次性把 JSON 读入内存。
  • cJSON_GetErrorPtr() 使用全局错误指针,多线程下不安全。
  • 嵌套深度默认限制约 1000,防止栈溢出。
  • 适合中小型 JSON,超大 JSON 建议用 yyjson、Jansson、json-c 等。

2. 获取与集成

源码集成:

从 GitHub 下载:

text 复制代码
https://github.com/DaveGamble/cJSON

把 cJSON.c、cJSON.h 加入工程,包含:

c 复制代码
#include "cJSON.h"

编译:

bash 复制代码
gcc main.c cJSON.c -o main -lm

系统包安装,如 Ubuntu/Debian:

bash 复制代码
sudo apt install libcjson-dev

包含:

c 复制代码
#include <cjson/cJSON.h>

编译:

bash 复制代码
gcc main.c -lcjson -o main

3. 核心数据结构

cJSON 节点大致定义如下:

c 复制代码
typedef struct cJSON {
    struct cJSON *next;
    struct cJSON *prev;
    struct cJSON *child;

    int type;

    char *valuestring;
    int valueint;        // 旧字段,已不推荐
    double valuedouble;

    char *string;        // 对象成员的 key
} cJSON;

关系:

  • 对象和数组都通过 child 指向第一个子节点。
  • 子节点之间通过 next / prev 组成双向链表。
  • 对象子节点的 string 是键名。
  • 数组子节点的 string 通常为 NULL。
  • type 是位标志,常用类型:
    • cJSON_False
    • cJSON_True
    • cJSON_NULL
    • cJSON_Number
    • cJSON_String
    • cJSON_Array
    • cJSON_Object
    • cJSON_Raw

推荐用类型判断函数:

c 复制代码
cJSON_IsObject(item);
cJSON_IsArray(item);
cJSON_IsString(item);
cJSON_IsNumber(item);
cJSON_IsBool(item);
cJSON_IsTrue(item);
cJSON_IsFalse(item);
cJSON_IsNull(item);

4. 解析 JSON 文件的标准流程

  1. 用 fopen(..., "rb") 打开文件。
  2. 读取全部内容到内存。
  3. 在末尾补 '\0'。
  4. 如果文件有 UTF-8 BOM,跳过前 3 字节。
  5. 用 cJSON_Parse 或 cJSON_ParseWithOpts 解析。
  6. 判断根节点类型。
  7. 用 cJSON_GetObjectItemCaseSensitive、cJSON_GetArrayItem 等读取。
  8. 用 cJSON_Delete 释放 JSON 树。
  9. 用 free 释放自己读取的文件缓冲区。

5. 常用解析 API

c 复制代码
cJSON *cJSON_Parse(const char *value);
cJSON *cJSON_ParseWithLength(const char *value, size_t buffer_length);
cJSON *cJSON_ParseWithOpts(const char *value,
                           const char **return_parse_end,
                           cJSON_bool require_null_terminated);

void cJSON_Delete(cJSON *item);

cJSON *cJSON_GetObjectItem(const cJSON *object, const char *string);
cJSON *cJSON_GetObjectItemCaseSensitive(const cJSON *object, const char *string);

cJSON *cJSON_GetArrayItem(const cJSON *array, int index);
int cJSON_GetArraySize(const cJSON *array);

double cJSON_GetNumberValue(const cJSON *item);
char *cJSON_GetStringValue(const cJSON *item);

const char *cJSON_GetErrorPtr(void);

建议:

  • 对象取键用 cJSON_GetObjectItemCaseSensitive,避免大小写不敏感带来的意外。
  • 解析时用 cJSON_ParseWithOpts(..., 1),要求整个输入是完整 JSON,防止尾随垃圾被忽略。
  • 读取数字用 cJSON_GetNumberValue,不要依赖旧字段 valueint。

6. 完整示例:读取并解析 JSON 文件

假设 config.json:

json 复制代码
{
  "name": "Alice",
  "age": 30,
  "skills": ["C", "Linux", "JSON"],
  "nested": {
    "enabled": true
  }
}

C 代码:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cJSON.h"

static char *read_file(const char *path, long *out_len) {
    FILE *fp = fopen(path, "rb");
    if (!fp) return NULL;

    if (fseek(fp, 0, SEEK_END) != 0) {
        fclose(fp);
        return NULL;
    }

    long len = ftell(fp);
    if (len < 0) {
        fclose(fp);
        return NULL;
    }

    rewind(fp);

    char *buf = (char *)malloc((size_t)len + 1);
    if (!buf) {
        fclose(fp);
        return NULL;
    }

    size_t n = fread(buf, 1, (size_t)len, fp);
    fclose(fp);

    buf[n] = '\0';
    if (out_len) *out_len = (long)n;
    return buf;
}

int main(int argc, char **argv) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s file.json\n", argv[0]);
        return 1;
    }

    long len = 0;
    char *text = read_file(argv[1], &len);
    if (!text) {
        perror("read_file");
        return 1;
    }

    char *json = text;

    // 跳过 UTF-8 BOM
    if (len >= 3 &&
        (unsigned char)json[0] == 0xEF &&
        (unsigned char)json[1] == 0xBB &&
        (unsigned char)json[2] == 0xBF) {
        json += 3;
    }

    const char *end = NULL;
    cJSON *root = cJSON_ParseWithOpts(json, &end, 1);
    if (!root) {
        const char *err = cJSON_GetErrorPtr();
        long offset = end ? (long)(end - json) : -1;
        fprintf(stderr, "JSON parse error at offset %ld near: %.40s\n",
                offset, err ? err : "(unknown)");
        free(text);
        return 1;
    }

    // 读取字符串
    cJSON *name = cJSON_GetObjectItemCaseSensitive(root, "name");
    if (cJSON_IsString(name)) {
        printf("name = %s\n", name->valuestring);
    }

    // 读取数字
    cJSON *age = cJSON_GetObjectItemCaseSensitive(root, "age");
    if (cJSON_IsNumber(age)) {
        printf("age = %g\n", cJSON_GetNumberValue(age));
    }

    // 读取数组
    cJSON *skills = cJSON_GetObjectItemCaseSensitive(root, "skills");
    if (cJSON_IsArray(skills)) {
        cJSON *item = NULL;
        cJSON_ArrayForEach(item, skills) {
            if (cJSON_IsString(item)) {
                printf("skill: %s\n", item->valuestring);
            }
        }
    }

    // 读取嵌套对象
    cJSON *nested = cJSON_GetObjectItemCaseSensitive(root, "nested");
    if (cJSON_IsObject(nested)) {
        cJSON *enabled = cJSON_GetObjectItemCaseSensitive(nested, "enabled");
        if (cJSON_IsBool(enabled)) {
            printf("nested.enabled = %s\n",
                   cJSON_IsTrue(enabled) ? "true" : "false");
        }
    }

    cJSON_Delete(root);
    free(text);
    return 0;
}

编译:

bash 复制代码
gcc main.c cJSON.c -o json_demo -lm
./json_demo config.json

7. 生成、修改、打印 JSON

创建:

c 复制代码
cJSON *root = cJSON_CreateObject();

cJSON_AddStringToObject(root, "name", "Bob");
cJSON_AddNumberToObject(root, "age", 25);
cJSON_AddBoolToObject(root, "enabled", 1);
cJSON_AddNullToObject(root, "extra");

cJSON *arr = cJSON_AddArrayToObject(root, "skills");
cJSON_AddItemToArray(arr, cJSON_CreateString("C"));
cJSON_AddItemToArray(arr, cJSON_CreateString("JSON"));

char *out = cJSON_Print(root);          // 格式化输出
// char *out = cJSON_PrintUnformatted(root); // 紧凑输出

printf("%s\n", out);

free(out);          // 注意:cJSON_Print 返回的字符串用 free 释放
cJSON_Delete(root); // 释放 JSON 树

修改:

c 复制代码
cJSON_ReplaceItemInObjectCaseSensitive(
    root, "age", cJSON_CreateNumber(26)
);

cJSON_DeleteItemFromObjectCaseSensitive(root, "extra");

注意:

  • cJSON_AddItemToObject / cJSON_AddItemToArray 后,节点所有权转移给父节点。
  • cJSON_DetachItemFromObject 分离出的节点需要自己 cJSON_Delete。
  • cJSON_Delete 会递归释放整棵树。

8. 内存管理与错误处理

内存释放规则:

  • cJSON_Parse 返回的树:用 cJSON_Delete(root)。
  • cJSON_Print 返回的字符串:用 free(),不是 cJSON_Delete。
  • 自己 malloc 的文件缓冲区:用 free()。
  • 加入父节点的节点:不要单独释放,最终释放根节点即可。
  • 使用 cJSON_InitHooks 自定义分配器时,释放要使用对应 hooks 的 free。

错误处理:

c 复制代码
cJSON *root = cJSON_Parse(text);
if (!root) {
    const char *err = cJSON_GetErrorPtr();
    fprintf(stderr, "parse error near: %s\n", err ? err : "unknown");
}

更推荐:

c 复制代码
const char *end = NULL;
cJSON *root = cJSON_ParseWithOpts(text, &end, 1);
if (!root) {
    fprintf(stderr, "parse error at offset %ld\n", (long)(end - text));
}

注意:

  • cJSON_GetErrorPtr() 返回全局指针,多线程不安全。
  • 解析失败后应尽快使用错误指针,因为下一次解析可能覆盖。
  • 解析成功后,输入缓冲区可以释放;解析失败时,错误指针可能指向输入缓冲区,打印错误前不要释放输入。

9. 常见坑与注意事项

  1. 文件编码:JSON 标准要求 UTF-8。中文乱码通常是文件不是 UTF-8。
  2. BOM :UTF-8 BOM 会让 cJSON 解析失败,需要手动跳过 EF BB BF。
  3. 数字精度 :cJSON 用 double,超过 2^53 的整数可能不精确。
  4. 大小写 :cJSON_GetObjectItem 默认大小写不敏感,建议用 cJSON_GetObjectItemCaseSensitive。
  5. 类型检查 :取到节点后先用 cJSON_IsString、cJSON_IsNumber 等判断,再访问字段。
  6. 不要访问空指针 :cJSON_GetObjectItemCaseSensitive 找不到会返回 NULL。
  7. 不支持注释 ://、/* */、尾随逗号都不是标准 JSON,cJSON 解析器不接受。
  8. 线程安全:cJSON 本身大部分操作可并发,但错误指针和 hooks 是全局的。
  9. 大文件:cJSON 一次性加载,内存占用约为文件大小加节点开销。
  10. 释放方式 :树用 cJSON_Delete,打印字符串用 free,不要混用。

10. 总结

用 cJSON 解析 JSON 文件的核心就是:

text 复制代码
读文件 -> 补 '\0' -> 跳过 BOM -> cJSON_ParseWithOpts
-> 判断类型 -> GetObjectItem/GetArrayItem 遍历
-> 读取 valuestring / cJSON_GetNumberValue
-> cJSON_Delete 释放树 -> free 释放文件缓冲区

cJSON 简单、轻量、易集成,非常适合 C 语言项目解析配置文件和小型 JSON 数据。如果项目对性能、超大文件、流式解析或大整数精度要求很高,可以考虑 yyjson、Jansson、json-c 等替代库。

cJSON_ParseWithOpts使用

cJSON_ParseWithOpts 是 cJSON 中解析 JSON 的核心函数之一,它比 cJSON_Parse 提供了更精细的控制:可以要求整个输入必须是一个完整的 JSON 值(后面不能有多余的非空白字符),也可以返回解析结束的位置。下面详细介绍它的用法。


1. 函数原型

c 复制代码
#include "cJSON.h"

CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(
    const char *value,
    const char **return_parse_end,
    cJSON_bool require_null_terminated
);

相关类型:

c 复制代码
typedef int cJSON_bool;
#define cJSON_true 1
#define cJSON_false 0

2. 参数详解

2.1 const char *value

要解析的 JSON 字符串。

  • 必须是有效的 C 字符串,以 '\0' 结尾。
  • 不能为 NULL,否则行为未定义(通常返回 NULL 并可能崩溃)。
  • 如果 JSON 数据来自文件,需要先读入内存并补 '\0'。
  • 如果文件有 UTF-8 BOM(EF BB BF),需要手动跳过,否则解析会失败。

2.2 const char **return_parse_end

可选参数,用于返回解析结束的位置。

  • 如果传入 NULL,则不关心结束位置。
  • 如果传入一个 const char ** 变量的地址,函数成功时会把该指针设置为解析结束后的下一个字符位置。
  • 具体指向哪里取决于 require_null_terminated:
    • 当 require_null_terminated = 1 时,成功解析后函数会跳过尾随空白,然后检查是否到达 '\0'。因此 *return_parse_end 会指向字符串末尾的 '\0'。
    • 当 require_null_terminated = 0 时,函数解析完一个完整的 JSON 值就停止,不跳过后续字符。因此 *return_parse_end 指向 JSON 值之后的下一个字符,可能是空白、逗号、其他文本或 '\0'。
  • 解析失败时,*return_parse_end 也会被设置,通常指向出错位置附近,可用于计算错误偏移。

2.3 cJSON_bool require_null_terminated

是否要求整个输入必须"干净地"以 '\0' 结尾。

  • 1(cJSON_true):严格模式。解析完一个 JSON 值后,函数会跳过空白字符(空格、\t、\n、\r),然后要求下一个字符必须是 '\0'。如果还有任何非空白字符,解析失败并返回 NULL。
  • 0(cJSON_false):宽松模式。函数只解析出第一个完整的 JSON 值,然后立即返回,不关心后面是否还有内容。

使用建议:

  • 解析整个文件或网络包时,用 1,确保没有多余垃圾数据。
  • 从一段混合文本中提取第一个 JSON 对象时,用 0。
  • 连续解析多个 JSON 值时,用 0,并通过 return_parse_end 推进指针。

3. 返回值与错误处理

  • 成功 :返回一个 cJSON *,指向解析出的 JSON 树根节点。使用完毕后必须用 cJSON_Delete(root) 释放。
  • 失败 :返回 NULL。

失败时可通过以下方式获取错误信息:

c 复制代码
const char *err = cJSON_GetErrorPtr();
  • cJSON_GetErrorPtr() 返回一个指向错误位置附近的全局指针。
  • 该指针是全局的,多线程下不安全,应尽快使用。
  • 如果使用了 return_parse_end,可以计算偏移量:
c 复制代码
const char *end = NULL;
cJSON *root = cJSON_ParseWithOpts(text, &end, 1);
if (!root) {
    long offset = end ? (long)(end - text) : -1;
    fprintf(stderr, "parse error at offset %ld near: %.40s\n",
            offset, end ? end : "(unknown)");
}

注意:解析失败时,text 缓冲区不要立即释放,因为 cJSON_GetErrorPtr() 或 end 可能指向它。


4. 与 cJSON_Parse 的关系

cJSON_Parse 是最简单的封装:

c 复制代码
cJSON *cJSON_Parse(const char *value) {
    return cJSON_ParseWithOpts(value, 0, 0);
}

也就是说:

  • 不返回结束位置。
  • 不要求整个字符串是完整 JSON,解析到第一个完整值就停止。
  • 如果 JSON 后面有垃圾数据,cJSON_Parse 不会报错。

因此,在需要严格校验时,应直接使用 cJSON_ParseWithOpts。


5. 典型使用场景与示例

5.1 严格解析整个 JSON 文件

要求文件内容必须是一个完整的 JSON 值,后面只能有空白。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include "cJSON.h"

int main(void) {
    FILE *fp = fopen("config.json", "rb");
    if (!fp) { perror("fopen"); return 1; }

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

    char *text = malloc(len + 1);
    fread(text, 1, len, fp);
    fclose(fp);
    text[len] = '\0';

    // 跳过 UTF-8 BOM
    char *json = text;
    if (len >= 3 &&
        (unsigned char)json[0] == 0xEF &&
        (unsigned char)json[1] == 0xBB &&
        (unsigned char)json[2] == 0xBF) {
        json += 3;
    }

    const char *end = NULL;
    cJSON *root = cJSON_ParseWithOpts(json, &end, 1);
    if (!root) {
        const char *err = cJSON_GetErrorPtr();
        long offset = end ? (long)(end - json) : -1;
        fprintf(stderr, "JSON parse error at offset %ld near: %.40s\n",
                offset, err ? err : "(unknown)");
        free(text);
        return 1;
    }

    // 使用 root ...

    cJSON_Delete(root);
    free(text);
    return 0;
}

5.2 宽松解析,获取结束位置

假设有一段文本,其中嵌入了一个 JSON 对象,后面还有其他内容:

c 复制代码
const char *text = "{\"name\":\"Alice\",\"age\":30} 后面的内容";
const char *end = NULL;
cJSON *root = cJSON_ParseWithOpts(text, &end, 0);
if (root) {
    printf("解析成功,JSON 结束于偏移 %ld\n", (long)(end - text));
    printf("剩余内容: %s\n", end);
    cJSON_Delete(root);
}

输出:

text 复制代码
解析成功,JSON 结束于偏移 25
剩余内容:  后面的内容

注意:require_null_terminated = 0 时,end 指向 JSON 值后的第一个字符,这里是空格。

5.3 连续解析多个 JSON 值

如果一段文本中有多个连续的 JSON 值(例如 NDJSON,每行一个 JSON),可以用 return_parse_end 循环解析:

c 复制代码
const char *p = "{\"a\":1}\n{\"b\":2}\n{\"c\":3}";
const char *end = NULL;

while (*p) {
    // 跳过空白
    while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
    if (*p == '\0') break;

    cJSON *item = cJSON_ParseWithOpts(p, &end, 0);
    if (!item) {
        fprintf(stderr, "解析失败,位置: %s\n", cJSON_GetErrorPtr());
        break;
    }

    // 处理 item ...
    char *out = cJSON_PrintUnformatted(item);
    printf("解析到: %s\n", out);
    free(out);
    cJSON_Delete(item);

    if (end == p) break; // 防止死循环
    p = end;
}

5.4 校验解析后没有多余字符

严格模式已经自动完成这个校验。如果你想自己控制,可以用宽松模式加手动检查:

c 复制代码
const char *end = NULL;
cJSON *root = cJSON_ParseWithOpts(text, &end, 0);
if (root) {
    // 跳过尾随空白
    while (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r') end++;
    if (*end != '\0') {
        fprintf(stderr, "JSON 后面有多余内容: %s\n", end);
        cJSON_Delete(root);
        root = NULL;
    }
}

6. 注意事项与常见坑

  1. value 必须以 '\0' 结尾

    如果从文件读取时只读了 len 字节,必须 text[len] = '\0'。如果数据中包含 '\0',应使用 cJSON_ParseWithLength 系列。

  2. UTF-8 BOM 必须手动跳过

    cJSON 不自动处理 BOM。跳过 3 字节 EF BB BF 后再解析。

  3. require_null_terminated = 1 时,尾随空白是允许的

    空格、\t、\n、\r 会被跳过。其他任何字符都会导致失败。

  4. return_parse_end 在失败时也有效

    可以用来定位错误偏移。但不要依赖它一定指向精确的错误字符,它只是解析停止的位置。

  5. 错误指针是全局的

    cJSON_GetErrorPtr() 多线程不安全。如果多线程解析,建议只用 return_parse_end 计算偏移,或加锁。

  6. 内存释放

    解析成功返回的树用 cJSON_Delete 释放。return_parse_end 只是指向原输入缓冲区的指针,不需要释放。

  7. 不要混用释放函数

    • cJSON_ParseWithOpts 返回的树 → cJSON_Delete
    • cJSON_Print 返回的字符串 → free
    • 自己 malloc 的文件缓冲区 → free
  8. 嵌套深度限制

    cJSON 默认限制嵌套深度约 1000 层,超过会解析失败,防止栈溢出。

  9. 数字精度

    内部用 double 存储,大整数可能丢失精度。需要精确大整数时考虑其他库。

  10. 性能

    一次性加载整个 JSON 到内存,适合中小型数据。超大 JSON 建议用流式解析器。


7. 总结

cJSON_ParseWithOpts 是 cJSON 解析功能的核心,掌握三个参数即可灵活应对不同场景:

参数 作用 常用取值
value 要解析的 JSON 字符串 以 '\0' 结尾的缓冲区
return_parse_end 返回解析结束位置 NULL 或 const char **
require_null_terminated 是否要求整个输入是完整 JSON 1 严格,0 宽松
  • 解析整个文件、配置、网络包 → cJSON_ParseWithOpts(text, &end, 1)
  • 从混合文本提取第一个 JSON → cJSON_ParseWithOpts(text, &end, 0)
  • 连续解析多个 JSON → 宽松模式 + return_parse_end 推进指针

正确使用它可以避免"JSON 后面有垃圾数据却解析成功"的隐蔽问题,也能方便地定位解析错误。

相关推荐
Zip-List1 小时前
工作小记-pytorch的算子优化: 转gpu核函数 python调c
c语言·c++·pytorch·python·cuda
C++ 老炮儿的技术栈2 小时前
char 造成的数据溢出
linux·服务器·c语言·c++·算法·c
Zwawa15 小时前
先把轮子转稳,才不会烧心:编码器速度环 PID 怎么接回 PWM
c语言·单片机·嵌入式
工具派19 小时前
JSON转Excel后18位订单号不对了?数字精度与类型边界实测
json·excel
Escalating_xu19 小时前
【C 语言】编译和链接:从源文件、目标文件到可执行程序完整解析
c语言·开发语言
C++ 老炮儿的技术栈19 小时前
std::atomic 是什么
linux·c语言·网络·c++·visual studio
Logic10120 小时前
C语言/数据结构位运算题解:异或XOR找出数轴移动中只出现一次的指令——只出现一次的数字
c语言·数据结构·数组·位运算·时间复杂度·算法题·异或性质
weixin_4629019721 小时前
CubeIDE串口重定向 printf
c语言
阿皮 API21 小时前
Shopee虾皮商品详情API接口调用实战:Python获取东南亚商品数据与JSON解析
开发语言·python·json