简介:面向C语言开发者的JSON数据解析与生成库合集,收录了JSON-C和JSON两套实现。JSON-C具备完善的接口,能够将JSON文本解析为动态数据结构,支持字段迭代访问、再序列化,并带有错误处理与格式控制能力;另一个JSON库则保持精简风格,同样覆盖编码解码基础操作。两套库均适用于嵌入式、服务端以及接口数据传输等对性能敏感的场景。压缩包内共有20个文件,以16个头文件为主,用于声明数据类型、函数接口与宏定义;另附两个静态库文件、一个动态库文件和一个libtool存档文件,方便按项目需要选择静态或动态链接方式。整个压缩包只有109KB,十分轻量便携,目前已有484人学习下载。借助整理好的头文件和编译产物,开发者无需从源码自行编译,直接集成即可在C工程中调用JSON相关接口,高效完成数据交换;还可以对照比较两套库在性能、接口风格和内部机制上的差异,为后续选型或二次开发提供依据。
1. json 库下载与 cjson 的选择:为什么大多数 C 项目只需这份单文件源码
做 C 语言项目遇到 JSON 解析,第一反应是找个 json 库下载下来直接编。cjson(cJSON)就是这类需求里最常被搜的库:单个 .c 文件加一个 .h 头文件,没有任何第三方依赖,内存分配可以接管,在嵌入式环境、插件系统、配置解析场景里都够用。它不是功能最全的,但它是「最先把事情跑通」的那个;大家问的 cjson 库下载,本质上是想找一份能立刻编进工程的源码包。
下面从选型、源码包结构、编译到踩坑和封装习惯,按我实际拆项目的顺序讲完整。适合正在做 C/C++ 工程的开发者,也适合第一次用 C 解析 JSON 的新手。新手照着能跑通,熟手可以重点看第 4 章的边界和第 5 章的封装习惯。
2. cjson 源码包拆解:文件清单、版本边界与三种集成方式
下载资源时先别急着 make,源码包里的文件构成决定了后面怎么编、怎么链接、单元测试放哪。cjson 的源码包一般都很「素」:核心就两个文件,剩下的 CMake 和 tests 是辅助。先讲清楚包里有什么、每种文件什么时候用得到,再决定用拷贝还是库的方式接进工程。
2.1 为什么是 cjson:三类 C JSON 库的取舍
C 语言领域常见的 json 库有三个流派,选型错误后面全是坑。为了说得直观,我把关键差异列成一张对比表:
| 对比项 | cjson | json-c | jansson |
|---|---|---|---|
| 核心代码规模 | 两个文件,几千行 | 上万行,多模块 | 中等,约一个模块 |
| 第三方依赖 | 无 | 需要 glib 系列支持 | 无,但构建较重 |
| API 风格 | 直接操作树节点 | 对象抽象 + 序列化配置 | 引用计数,类 Python |
| 嵌入式适配 | 很好,内存钩子可换 | 一般,体量大 | 一般,依赖较多 |
| 典型场景 | 配置解析、小数据交换 | 大型通用服务 | 需要严格生命周期管理的服务 |
我当时的场景是某个采集程序里要解析服务端下发的一小段配置,总量不超过几十 KB。用一个 json 库下载回来,第一衡量标准就是「能少一个依赖就少一个」。cjson 的核心实现可以直接把 .c/.h 拖进工程,对构建系统零要求。反过来,如果你的程序要处理流式 JSON,或者要做 Schema 级别的结构校验,cjson 不合适——那是另外两个库的领域。选型就一句话:资源受限或追求可移植性,cjson;追求完整功能且不介意依赖,再考虑 json-c 或 jansson。
2.2 下载包里的真实内容:文件清单与版本边界
源码包解开之后,你大概率会看到下面这些文件,不是每个都要编进工程:
| 文件 | 作用 | 什么时候必须带 |
|---|---|---|
| cJSON.c / cJSON.h | 核心实现与声明 | 总是需要 |
| cJSON_Utils.c / cJSON_Utils.h | JSON Pointer 等高级操作 | 只用基础 API 时不编 |
| CMakeLists.txt | CMake 构建入口 | 用 CMake 管理构建时 |
| tests/ 目录 | 官方回归用例 | 验证库行为、排查解析差异时 |
| LICENSE | 许可证文本 | 商用注意保留 |
版本这块有个血泪经验:release 页里各 tag 之间,1.7 系列往后 API 基本稳定,老项目里流传的 cJSON_Version()、cJSON_IsString 这类 helper 在 1.7 系列都齐了。别直接拿 master 分支当生产依赖,我见过有人图新拉 master,结果某次 commit 改了内部格式化函数行为,线上浮点打印全部变样。用 release tag 或者固定某次 commit,出问题才有后悔药。
2.3 三种集成方式:源码拷贝、静态库与动态库
方式 A,也是八成场景我会选的方式:直接把 cJSON.c 和 cJSON.h 拷进自己的 src 目录。编译参数共享、没有额外构建步骤,缺点是代码库里混入第三方文件,商用时要留意 LICENSE 保留。方式 B 适合用 CMake 管理的大型工程,用 add_subdirectory 引入:
# 在你的 CMakeLists.txt 中引入 cjson 源码包 option(BUILD_SHARED_LIBS "Whether to build shared or static lib" OFF) set(ENABLE_CJSON_TEST OFF CACHE BOOL "") add_subdirectory(third_party/cjson) target_link_libraries(your_app PRIVATE cjson)参数说明:BUILD_SHARED_LIBS 关闭后得到静态库;ENABLE_CJSON_TEST 必须关掉,否则 tests 会依赖 unity 测试框架,拖慢构建。add_subdirectory 引入的 target 名是 cjson,链接即可。如果你的工程只用基础 API,cJSON_Utils 不会被编进来。
方式 C 是独立编译成 .so 或 .a 再链接,适合多个进程或插件共享同一份 cJSON。无论哪种方式,编译器都要支持 C99,老工具链要提前确认标准选项,不然编译直接报错。嵌入式工程里还可以用 cJSON_InitHooks 把内存分配换成自己的内存池,这块不用动源码,适合对 malloc 行为有严格要求的板子。
3. 从下载到跑通:Makefile 组织与四条核心 API 的最小范例
把源码包拖进工程只是第一步。下面给一份能在五分钟内编译运行的最小工程,把 Parse、GetObjectItem、Print、Delete 四个动作串起来。这四个函数的配对关系,就是以后所有 cjson 代码的地基。
3.1 下载后先做的三项检查
拿到源码包先做三件事。第一,确认目录里有 cJSON.c 和 cJSON.h,而不是只有打包脚本或子模块指针,遇到过下载到的 zip 里差头文件的情况。第二,确认编译器标准至少 C99,gcc 需要 -std=c99,部分老旧交叉工具链要单独处理。第三,如果工程用 CMake,先跑一遍 tests 里的用例,验证这份资源和你的编译环境兼容。我一般会先跑官方测试用例再装进自己的工程,省得后面分不清是库的问题还是自己的问题。
3.2 最小工程结构:Makefile 怎么写
一个最小验证工程只需要两个文件:main.c 和下载回来的 cJSON.c。Makefile 这样组织:
CC := gcc CFLAGS := -std=c99 -Wall -I./cjson TARGET := parse_config SRCS := main.c ./cjson/cJSON.c $(TARGET): $(SRCS) $(CC) $(CFLAGS) -o $@ $^ clean: rm -f $(TARGET)逻辑说明:-I./cjson 是头文件搜索路径,指向放 cJSON.h 的目录;SRCS 里只列 cJSON.c,不列 cJSON_Utils.c,用不到就别编,减少无谓的符号冲突。别把 tests 下的 test.c 加进来,它们依赖 unity 框架,编译会失败。cJSON 内部不依赖 libm,所以不需要 -lm,这点和很多人的习惯不同。
3.3 最小解析实例:把配置 JSON 读出来
下面这段代码覆盖了解析、取值、格式化输出、释放整条链路,也是后面所有封装的骨架:
#include <stdio.h> #include <stdlib.h> #include "cJSON.h" int main(void) { const char *raw = "{\"host\":\"192.168.1.10\",\"port\":8080,\"debug\":true}"; cJSON *root = cJSON_Parse(raw); if (root == NULL) { const char *err = cJSON_GetErrorPtr(); fprintf(stderr, "parse failed before: %s\n", err ? err : "unknown"); return 1; } cJSON *host = cJSON_GetObjectItem(root, "host"); cJSON *port = cJSON_GetObjectItem(root, "port"); if (cJSON_IsString(host) && host->valuestring != NULL) printf("host=%s\n", host->valuestring); if (cJSON_IsNumber(port)) printf("port=%d\n", port->valueint); char *out = cJSON_Print(root); if (out) { printf("%s\n", out); free(out); /* Print 返回的内存必须手动释放 */ } cJSON_Delete(root); /* 释放整棵树,子节点级联释放 */ return 0; }逻辑说明:cJSON_Parse 接受 const char*,解析成功返回树根节点,失败返回 NULL 且不产出任何可释放对象。cJSON_IsString 和 cJSON_IsNumber 是类型守卫 helper,不检查直接读 valuestring 是常见翻车点。cJSON_Print 返回带缩进格式化的字符串,内部 malloc,用完必须 free。最后 cJSON_Delete 递归释放所有子节点,这条链路缺一环就是内存泄漏。
用 Makefile 编出来后:
$ make $ ./parse_config host=192.168.1.10 port=8080 { "host": "192.168.1.10", "port": 8080, "debug": true }到这里,你已经把 cjson 库下载、编译、运行验证完整走通了一遍。接下来才是真正花时间的地方——能编译但行为不对、偶尔崩、内存涨,这些问题基本都集中在第 4 章。
4. cjson 库高频排查:五个翻车现场与对应解法
这部分是实操里最常见的排查场景,按「现象 → 原因 → 解决」写。前面代码能跑通只是开始,真正花时间的都是这些边界和配对问题。
4.1 内存问题:Parse 后没 Delete、Print 后没 free、AddItem 后重复 Delete
现象:valgrind 报 definitely lost;或者在向对象 AddItem 之后又单独 Delete 子 item,程序 double free 直接崩。
原因:cJSON 的对象所有权是「树归属」。cJSON_AddItemToObject 会把子节点所有权交给父节点,之后只能通过删除父节点来释放整棵树;cJSON_Parse 返回的树必须由 cJSON_Delete 释放;cJSON_Print 和 cJSON_PrintUnformatted 返回的是 malloc 出来的字符串,得单独 free。三条配对少了哪条,内存账都对不上。
解决:约定「谁 Parse 谁 Delete,谁 Print 谁 free」。C 没有 RAII,我写函数时会把清理路径提前列好:函数开头声明 cJSON *root = NULL,不管中间哪个分支 return,都先走到统一出口做 cJSON_Delete。如果你改了内存钩子,配套释放也要走 cJSON_free 或统一 Delete 父节点,别混用系统 free。
注意:向 cJSON 对象里 AddItem 之后,子节点的所有权归父节点,不要再单独 Delete 子节点;需要摘除时用 cJSON_DetachItemFromObject。
4.2 深层嵌套崩溃:默认 1000 层上限不是护身符
现象:解析某个外部数据源,cJSON_Parse 返回 NULL,或者直接段错误。
原因:cJSON 默认的 CJSON_NESTING_LIMIT 是 1000,超过这个嵌套深度会主动失败;但每个嵌套的对象在递归解析时都吃栈,嵌入式小栈环境下 1000 层也可能提前爆栈。这个宏保护的是解析深度,不是栈安全。
解决:如果数据源本身异常,直接拒收并记录错误位置;如果确需支持超深结构,用编译参数 -DCJSON_NESTING_LIMIT=4096 提高上限,同时把该解析任务放到栈更大的线程里跑。别在中断上下文里调 Parse,别在栈只有 2KB 的任务里解析嵌套 JSON,这是用空间换稳定的典型场景。
4.3 解析失败定位:GetErrorPtr 只告诉你「在哪」不告诉「为什么」
现象:cJSON_Parse 返回 NULL,日志里只有一个 false,你根本不知道 JSON 哪里写错了。
原因:cJSON_Parse 失败时不提供具体错误码,只留一个内部指针标记解析中断位置。对第三方数据源来说,这几乎是唯一线索。
解决:用 cJSON_GetErrorPtr() 拿到出错位置并打印上下文,出错点往往就在返回值所指字符附近——多了个逗号、值少了引号、字符串未闭合,一眼就能看出来。处理外部 JSON 前先判 root == NULL,再打 error ptr:
cJSON *root = cJSON_Parse(raw); if (!root) { const char *pos = cJSON_GetErrorPtr(); fprintf(stderr, "parse failed near: %.60s\n", pos ? pos : "(null)"); return 1; }参数说明:%.60s 限制只打印出错点附近 60 个字符,防止把整段超大 JSON 灌进日志。另外注意,cJSON_Parse 要求整段输入都是有效 JSON,不允许多个并列的 JSON 对象挤在一起,那是 NDJSON 的领域,需要逐行 Parse。
4.4 大小写与 Key 匹配:为什么拿不到你以为的字段
现象:数据里字段名是 "Host",代码里写 "host" 也能取到值;可当需要区分两个仅大小写不同的字段时,取回来的是同一个。
原因:cJSON_GetObjectItem 默认是大小写不敏感的,这是兼容老版本行为的遗留设计。对需要严格匹配的场景,这就是埋雷,尤其是用户可自定义的配置字段。
解决:用 cJSON_GetObjectItemCaseSensitive 做严格匹配。解析用户配置时,我全程用 CaseSensitive 版本,从源头杜绝歧义:
cJSON *a = cJSON_GetObjectItem(root, "Host"); /* 大小写不敏感 */ cJSON *b = cJSON_GetObjectItemCaseSensitive(root, "Host"); /* 严格匹配 */逻辑说明:两个调用结果可能不同,取决于数据里字段名到底是 "Host" 还是 "host"。配置文件只要出现两个字段名仅大小写不同的情况,普通 GetObjectItem 必翻车。新写代码直接默认 CaseSensitive。
4.5 平台差异:浮点打印、文件编码与老编译器
现象:同一份 JSON,在嵌入式板子上解析结果和 PC 上不同;或者序列化出来的浮点数比 printf 输出长出一截。
原因:cJSON 内部用自己的数字格式化逻辑,浮点输出和 printf %g 的结果可能不一致;部分老工具链对 vsnprintf 的支持不完整,也可能导致字符串处理异常。这不是库的 bug,是平台差异。
解决:浮点字段做回读断言时允许 1 个 ulp 误差,别做精确字符串比对;跨平台测试固定在同一 commit 上跑,避免库版本漂移;老编译器先查 C99 支持度,别急着怪库。处理外部文本时,如果数据源是 UTF-8 带 BOM 的文件,记得先跳过 BOM 再 Parse,不然首字符会解析失败。
5. 进阶用法:把 cjson 封装成自己的配置文件读写层
项目里最常用的其实是读配置文件和回写配置。把 Parse、Delete、Print、free 的配对逻辑收敛到一个函数里,整个工程其他地方就不用再记这些约定。下面这个 load_json_file 是我在每个项目里都会复制一份的习惯实现:
static cJSON *load_json_file(const char *path) { FILE *fp = fopen(path, "rb"); if (!fp) return NULL; /* 文件不存在直接返回 */ fseek(fp, 0, SEEK_END); long sz = ftell(fp); fseek(fp, 0, SEEK_SET); if (sz <= 0) { fclose(fp); return NULL; } char *buf = (char *)malloc((size_t)sz + 1); if (!buf) { fclose(fp); return NULL; } size_t rd = fread(buf, 1, (size_t)sz, fp); fclose(fp); if (rd != (size_t)sz) { free(buf); return NULL; } buf[rd] = '\0'; /* 必须补结束符 */ cJSON *root = cJSON_Parse(buf); free(buf); /* 原始 buffer 使命结束 */ return root; }参数说明:fopen 用 "rb" 二进制模式,避免 Windows 下换行符被转换;ftell 取文件大小后要用 fseek 回到开头;读出来的 buffer 补 '\0' 是 cJSON_Parse 正常工作的前提,cJSON 不会自动截断输入。调用方拿到 root 后做业务逻辑,最后统一 cJSON_Delete(root),析构只此一处。
配套的验证技巧也很简单:写一个回读测试,用 load_json_file 读入一份 json,再用 cJSON_PrintUnformatted 输出,跟原文件做语义比对(忽略 key 顺序)。这个测试能快速确认读写层没破坏数据,也能在升级库版本后第一时间发现格式化行为变化。
从那以后,我每次往工程里塞第三方库,都强制走一遍「选型 → 读源码包结构 → 最小工程跑通 → 把配对释放写进封装」这条流程。cjson 库下载这东西,真正值钱的不是那两三个文件,而是你用它的边界和坑位;把这些提前钉死,后面十年都不用再回头查一次。希望帮到你。
本文还有配套的精品资源,点击获取