跳到主要内容cJSON 1.7.19 源码深度剖析:数据结构、解析流程与注释规范 | 极客日志C算法
cJSON 1.7.19 源码深度剖析:数据结构、解析流程与注释规范
cJSON 1.7.19 作为轻量级 C 语言 JSON 库,核心在于统一节点结构体与树状链表设计。文章深入剖析其内存布局、位掩码类型系统及双向链表实现,详解从字符串到树的解析流程及反向生成逻辑。重点涵盖递归深度保护、可插拔内存管理及数字精度处理等安全机制。同时提供函数级、代码块级及关键行级的注释规范示例,帮助开发者理解源码并应用于嵌入式或底层开发场景。
CodeArtist2 浏览 cJSON 是 Dave Gamble 等人开源的超轻量级 C 语言 JSON 解析/生成库,整个库仅由 cJSON.c(约 3200 行)和 cJSON.h(约 306 行)两个文件组成,无外部依赖,MIT 协议,广泛应用于嵌入式、IoT 和各类 C 项目。
核心数据结构:cJSON 结构体
cJSON 用一个统一的节点类型表示所有 JSON 值(对象、数组、字符串、数字、布尔、null 等),核心就是下面这个结构体(cJSON.h 第 103–123 行):
typedef struct cJSON {
struct cJSON* next;
struct cJSON* prev;
struct cJSON* child;
int type;
char* valuestring;
int valueint;
double valuedouble;
char* string;
} cJSON;
- next / prev:同一层级上的兄弟节点,组成双向链表(数组元素之间、对象键值对之间)。
- child:仅对 Array / Object 有效,指向「第一个子节点」,其余子节点通过
next 串联。
- type:用位掩码表示类型(见下一小节)。
- valuestring / valueint / valuedouble:按类型选用;数字统一用
valuedouble,valueint 仅为兼容保留。
- string:当该节点是某个 Object 的成员时,存键名。
也就是说,整棵 JSON 树 = 多棵「父子 + 兄弟」链表:父子用 child,兄弟用 next/prev。
内存布局(64 位系统示意)
64 位下指针 8 字节、int 4 字节、double 8 字节,考虑对齐后,大致布局如下(方便理解,实际以编译器为准):
| 偏移 (字节) | 大小 | 成员 |
|---|
| 0 | 8 字节 | next |
| 8 | 8 字节 | prev |
| 16 | 8 字节 | child |
| 24 | 4 字节 | type |
| 28 | 4 字节 | (padding) |
| 32 | 8 字节 | valuestring |
| 40 | 4 字节 | valueint |
| 44 | 4 字节 | (padding) |
| 48 | 8 字节 | valuedouble |
| 56 | 8 字节 | string |
整体约 64 字节/节点。32 位下指针 4 字节,总大小会小一些(约 40 字节)。
类型系统:位掩码设计
类型不是用 enum 一个一个值,而是用位掩码,便于和「引用、常量键名」等标记组合:
#define cJSON_Invalid (0)
#define cJSON_False (1<<0)
#define cJSON_True (1<<1)
#define cJSON_NULL (1<<2)
#define cJSON_Number (1<<3)
#define cJSON_String (1<<4)
#define cJSON_Array (1<<5)
#define cJSON_Object (1<<6)
#define cJSON_Raw (1<<7)
#define cJSON_IsReference 256
#define cJSON_StringIsConst 512
- 取「纯类型」:
(item->type) & 0xFF
- 判断是否引用:
(item->type) & cJSON_IsReference
- 判断键名是否常量:
(item->type) & cJSON_StringIsConst
这样既节省字段,又方便扩展(例如以后再加一个 bit 表示「只读」等)。
树状链表:一个例子
JSON:{"name": "Alice", "age": 25, "scores": [90, 95, 88]} 解析后,在内存里大致是:
- 根节点(Object):
child → 第一个键值对 "name": "Alice"。
- 该键值对节点:
string = "name", valuestring = "Alice",next → 下一个键值对 "age": 25,再 next → "scores": [90,95,88]。
"scores" 的值是一个 Array 节点,其 child → 第一个元素 90,再 next → 95,再 next → 88。
也就是说:对象/数组的一层 = 一条双向链表(next/prev),向下的一层 = child。另外,实现里还有一个小技巧:链表头的 prev 指向最后一个节点,这样在尾部追加时是 O(1)。
核心流程一:JSON 解析(字符串 → cJSON 树)
调用链
入口是 cJSON_Parse(const char *value),内部会:
cJSON_ParseWithOpts(value, NULL, 0)
- →
cJSON_ParseWithLengthOpts(value, strlen(value)+1, NULL, 0)
- 初始化
parse_buffer(content, length, offset, depth, hooks)
cJSON_New_Item() 分配根节点
skip_utf8_bom() 跳过 UTF-8 BOM(若有)
buffer_skip_whitespace() 跳过空白
parse_value(item, buffer) ← 真正的递归解析入口
- 若要求
require_null_terminated,再检查末尾是否为 \0
- 成功返回根节点;失败则
goto fail,cJSON_Delete(item) 并设置 global_error,返回 NULL
也就是说,所有「一个 JSON 值」的解析都从 parse_value 开始,由首字符决定走哪条分支。
parse_value:按首字符分派
'n' → 尝试匹配 "null",成功则 item->type = cJSON_NULL,offset += 4
'f' → 尝试匹配 "false",成功则 item->type = cJSON_False,offset += 5
't' → 尝试匹配 "true",成功则 item->type = cJSON_True,offset += 4
"'" → 调用 parse_string(),解析字符串(含转义和 \uXXXX)
'-' 或 '0'~'9' → 调用 parse_number(),内部用 strtod,并做 int 溢出饱和
'[' → 调用 parse_array(),内部循环里对每个元素再调 parse_value(递归)
'{' → 调用 parse_object(),内部循环里先 parse_string 拿键名,再 parse_value 拿值(递归)
- 其他 → 返回 false,解析失败
数组/对象在进入前会检查 depth >= CJSON_NESTING_LIMIT(默认 1000),超限直接返回 false,防止栈溢出。
parse_array / parse_object 要点
- parse_array:
- 跳过
[,若紧接着是 ] 则为空数组。
- 否则 do-while:每次
cJSON_New_Item 一个新节点,用 next/prev 接到当前链表尾部,然后 parse_value(current_item, ...) 解析这一个元素,再根据是否遇到 , 决定是否继续。
- 最后把
head->prev = current_item(头指尾),item->child = head,item->type = cJSON_Array。
- 任一步失败则
cJSON_Delete(head) 统一释放已建节点。
- parse_object:
- 结构类似,只是每个「元素」是「键 + 值」:先
parse_string 得到键名(先存在 valuestring),再把它移到 string,valuestring 置空,然后跳过 :,再 parse_value 得到值。
- 同样用
next/prev 串成双向链表,head->prev 指尾,item->child = head,item->type = cJSON_Object。
解析失败时,会通过 global_error 记录位置,用户可用 cJSON_GetErrorPtr() 获取(注意多线程下不保证可靠)。
核心流程二:JSON 生成(cJSON 树 → 字符串)
调用链
cJSON_Print(item) → 内部 print(item, true, &global_hooks),格式化(带缩进、换行)
cJSON_PrintUnformatted(item) → print(item, false, &global_hooks),紧凑一行
print 里:分配一块 printbuffer(默认 256 字节),调用 **print_value(item, buffer)**,再根据实际长度 realloc 或重新 malloc 拷贝,返回字符串指针。调用方需用 cJSON_free 释放。
也就是说,所有「把一个 cJSON 值变成字符串」的逻辑都从 print_value 开始。
print_value:按 type 分派
(item->type) & 0xFF 得到基本类型,然后:
- cJSON_NULL → 写入
"null"
- cJSON_False → 写入
"false"
- cJSON_True → 写入
"true"
- cJSON_Number →
print_number()(处理 NaN/Inf→"null",整数用 %d,否则 %1.15g 或 %1.17g,并统一小数点 locale)
- cJSON_String →
print_string() → print_string_ptr(),加引号并转义
- cJSON_Raw → 直接 memcpy
valuestring
- cJSON_Array → 写
[,对每个 child 调用 print_value,中间加 ,,最后写 ]
- cJSON_Object → 写
{,对每个 child 先打键名(print_string_ptr),再 :,再 print_value 打值,最后 }
输出缓冲区用 ensure() 管理:空间不够时按「2 倍」扩容(或到 INT_MAX),内部用 realloc 或 malloc+memcpy+free,取决于是否设置了自定义 hooks 的 realloc。
设计亮点小结
- 树状链表:child 表示父子,next/prev 表示兄弟,head->prev 指尾,实现 O(1) 尾部追加。
- 位掩码类型:一个 int 同时表达「基本类型 + 引用/常量」等标记,省字段且易扩展。
- 内存安全:解析失败统一
goto fail + cJSON_Delete,避免泄漏;cJSON_Delete 对兄弟用循环、对子树用递归,并尊重 IsReference/StringIsConst。
- 可插拔分配器:
internal_hooks 封装 malloc/free/realloc,用户可替换;若不用标准库的 malloc/free,则禁用 realloc,避免跨分配器 realloc。
- 数字与 locale:NaN/Inf 输出为 "null";数字先 15 位再 17 位精度;小数点按当前 locale 解析(parse_number 里替换 '.')。
- 嵌套深度:
CJSON_NESTING_LIMIT 限制递归深度,防止恶意或异常深层 JSON 导致栈溢出。
深度注释实践(可作规范)
对 cJSON 这类库做「源码分析 + 深度注释」时,可以按三层来做,和本项目里的注释风格一致:
函数级注释(Doxygen 风格)
说明:作用、参数、返回值、谁负责释放内存、是否线程安全等。例如:
CJSON_PUBLIC(cJSON *)cJSON_Parse(const char* value);
重点:凡是返回「新分配的 cJSON 或 char」,都要写清楚由谁、用什么 API 释放。
代码块注释
对「一整段逻辑」用块注释说明:在做什么、为什么这样写、有没有内存/错误处理上的注意点。例如:
关键行注释
对「容易误解」或「和规范/安全相关」的代码行做简短说明。例如:
input_buffer->offset++;
buffer_skip_whitespace(input_buffer);
if(input_buffer->depth >= CJSON_NESTING_LIMIT)
return false;
这样配合源码阅读,可以快速对应到「数据结构」「解析/生成流程」和「内存与安全」几个维度。
如何运行与测试
说明:docs/cJSON_annotated.c 和 docs/cJSON_annotated.h 是带注释的文档型副本,不能直接替代工程里的 cJSON 使用;实际编译、运行、测试请用仓库根目录下的原始 cJSON.c 和 cJSON.h。
单文件 demo 编译(推荐)
在 fuzzing/docs/ 下已提供一个单文件测试程序 test_cjson_demo.c,用「原始 cJSON」编译即可:
Linux / macOS / MinGW(gcc):
cd /path/to/cJSON-1.7.19/fuzzing/docs
gcc -o test_cjson_demo test_cjson_demo.c ../../cJSON.c -I../../ -lm
./test_cjson_demo
Windows(MSVC Developer Command Prompt):
cd d:\path\to\cJSON-1.7.19\fuzzing\docs
cl test_cjson_demo.c ../../cJSON.c /I../../ /Fe:test_cjson_demo.exe
test_cjson_demo.exe
该 demo 覆盖:解析、打印、手动建树、类型判断、链表遍历、错误处理、Minify、Duplicate、版本信息等,可以和 cJSON_annotated.c/.h 的注释对照看。
用官方测试套件(CMake)
cd /path/to/cJSON-1.7.19
mkdir build && cd build
cmake ..
cmake --build .
ctest
总结
- cJSON 用「一个 cJSON 结构体 + 树状链表(child + next/prev)」表示整棵 JSON 树,类型用位掩码,便于扩展标记。
- 解析:
cJSON_Parse → parse_value 递归分派到 null/true/false/string/number/array/object,数组和对象内部用链表 + 深度限制保证安全和清晰。
- 生成:
cJSON_Print → print_value 按类型输出,缓冲区由 ensure 动态扩容。
- 深度注释时可按「函数级(含内存责任)/ 代码块 / 关键行」三层来做,便于复习和写报告。
- 实际运行和测试请用原始 cJSON.c + cJSON.h,配合仓库中的
test_cjson_demo.c 或官方测试即可。
如果你在做「cJSON 源码分析」课程作业或技术博客,可以直接把本文当作提纲,再结合项目里的 01-核心数据结构分析.md、02-核心流程分析.md 和带注释的 cJSON_annotated.c/.h 做细化与引用。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online