cJSON 1.7.19 源码深度分析:数据结构、解析流程与深度注释实践

cJSON 1.7.19 源码深度分析:数据结构、解析流程与深度注释实践
本文基于 cJSON 1.7.19 源码,从核心数据结构、JSON 解析/生成流程、内存管理到深度注释实践,系统梳理这一轻量级 JSON 库的设计与实现,适合 C 语言进阶与嵌入式开发学习。

目录

一、前言

cJSON 是 Dave Gamble 等人开源的超轻量级 C 语言 JSON 解析/生成库,整个库仅由 cJSON.c(约 3200 行)和 cJSON.h(约 306 行)两个文件组成,无外部依赖,MIT 协议,广泛应用于嵌入式、IoT 和各类 C 项目。

本文基于一次「源码分析 + 深度注释」的实践,整理出:

  1. 核心数据结构cJSON 结构体、内存布局、树状链表设计
  2. 核心流程:JSON 解析(字符串→树)、JSON 生成(树→字符串)
  3. 设计亮点:位掩码类型、可插拔内存、嵌套深度保护、数字精度处理
  4. 深度注释:函数级 / 代码块级 / 关键行注释规范与示例
  5. 如何运行与测试:单文件编译、测试用例说明

适合:想啃 cJSON 源码的 C 开发者、做嵌入式 JSON 解析的同学、以及需要「读源码写注释」交作业/做笔记的同学。


二、核心数据结构:cJSON 结构体

2.1 结构体定义

cJSON 用一个统一的节点类型表示所有 JSON 值(对象、数组、字符串、数字、布尔、null 等),核心就是下面这个结构体(cJSON.h 第 103–123 行):

typedefstructcJSON{structcJSON*next;/* 同级下一个兄弟节点 */structcJSON*prev;/* 同级上一个兄弟节点 */structcJSON*child;/* 第一个子节点(仅 Array/Object 使用) */int type;/* 节点类型(位掩码) */char*valuestring;/* 字符串值(String/Raw 类型) */int valueint;/* 整数值(已废弃,建议用 valuedouble) */double valuedouble;/* 数字值(Number 类型) */char*string;/* 键名(仅当节点是对象的子项时有效) */} cJSON;
  • next / prev:同一层级上的兄弟节点,组成双向链表(数组元素之间、对象键值对之间)。
  • child:仅对 Array / Object 有效,指向「第一个子节点」,其余子节点通过 next 串联。
  • type:用位掩码表示类型(见下一小节)。
  • valuestring / valueint / valuedouble:按类型选用;数字统一用 valuedoublevalueint 仅为兼容保留。
  • string:当该节点是某个 Object 的成员时,存键名。

也就是说,整棵 JSON 树 = 多棵「父子 + 兄弟」链表:父子用 child,兄弟用 next/prev

2.2 内存布局(64 位系统示意)

64 位下指针 8 字节、int 4 字节、double 8 字节,考虑对齐后,大致布局如下(方便理解,实际以编译器为准):

在这里插入图片描述
偏移(字节)大小成员
08 字节next
88 字节prev
168 字节child
244 字节type
284 字节(padding)
328 字节valuestring
404 字节valueint
444 字节(padding)
488 字节valuedouble
568 字节string

整体约 64 字节/节点。32 位下指针 4 字节,总大小会小一些(约 40 字节)。

2.3 类型系统:位掩码设计

类型不是用 enum 一个一个值,而是用位掩码,便于和「引用、常量键名」等标记组合:

/* 基本类型(低 8 位) */#definecJSON_Invalid(0)#definecJSON_False(1<<0)/* 1 */#definecJSON_True(1<<1)/* 2 */#definecJSON_NULL(1<<2)/* 4 */#definecJSON_Number(1<<3)/* 8 */#definecJSON_String(1<<4)/* 16 */#definecJSON_Array(1<<5)/* 32 */#definecJSON_Object(1<<6)/* 64 */#definecJSON_Raw(1<<7)/* 128 *//* 附加标记(高位) */#definecJSON_IsReference256/* 引用:Delete 时不释放 child/valuestring */#definecJSON_StringIsConst512/* 键名为常量:Delete 时不释放 string */

用法小结:

  • 取「纯类型」:(item->type) & 0xFF
  • 判断是否引用:(item->type) & cJSON_IsReference
  • 判断键名是否常量:(item->type) & cJSON_StringIsConst

这样既节省字段,又方便扩展(例如以后再加一个 bit 表示「只读」等)。

在这里插入图片描述

2.4 树状链表:一个例子

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 树)

3.1 调用链

入口是 cJSON_Parse(const char *value),内部会:

  1. cJSON_ParseWithOpts(value, NULL, 0)
  2. cJSON_ParseWithLengthOpts(value, strlen(value)+1, NULL, 0)
  3. 初始化 parse_buffer(content, length, offset, depth, hooks)
  4. cJSON_New_Item() 分配根节点
  5. skip_utf8_bom() 跳过 UTF-8 BOM(若有)
  6. buffer_skip_whitespace() 跳过空白
  7. parse_value(item, buffer) ← 真正的递归解析入口
  8. 若要求 require_null_terminated,再检查末尾是否为 \0
  9. 成功返回根节点;失败则 goto failcJSON_Delete(item) 并设置 global_error,返回 NULL

也就是说,所有「一个 JSON 值」的解析都从 parse_value 开始,由首字符决定走哪条分支。

在这里插入图片描述

3.2 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,防止栈溢出。

3.3 parse_array / parse_object 要点

  • parse_array
    • 跳过 [,若紧接着是 ] 则为空数组。
    • 否则 do-while:每次 cJSON_New_Item 一个新节点,用 next/prev 接到当前链表尾部,然后 parse_value(current_item, ...) 解析这一个元素,再根据是否遇到 , 决定是否继续。
    • 最后把 head->prev = current_item(头指尾),item->child = headitem->type = cJSON_Array
    • 任一步失败则 cJSON_Delete(head) 统一释放已建节点。
  • parse_object
    • 结构类似,只是每个「元素」是「键 + 值」:先 parse_string 得到键名(先存在 valuestring),再把它移到 stringvaluestring 置空,然后跳过 :,再 parse_value 得到值。
    • 同样用 next/prev 串成双向链表,head->prev 指尾,item->child = headitem->type = cJSON_Object

解析失败时,会通过 global_error 记录位置,用户可用 cJSON_GetErrorPtr() 获取(注意多线程下不保证可靠)。


四、核心流程二:JSON 生成(cJSON 树 → 字符串)

4.1 调用链

  • 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 开始

在这里插入图片描述

4.2 print_value:按 type 分派

(item->type) & 0xFF 得到基本类型,然后:

  • cJSON_NULL → 写入 "null"
  • cJSON_False → 写入 "false"
  • cJSON_True → 写入 "true"
  • cJSON_Numberprint_number()(处理 NaN/Inf→"null",整数用 %d,否则 %1.15g 或 %1.17g,并统一小数点 locale)
  • cJSON_Stringprint_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。


五、设计亮点小结

  1. 树状链表:child 表示父子,next/prev 表示兄弟,head->prev 指尾,实现 O(1) 尾部追加。
  2. 位掩码类型:一个 int 同时表达「基本类型 + 引用/常量」等标记,省字段且易扩展。
  3. 内存安全:解析失败统一 goto fail + cJSON_Delete,避免泄漏;cJSON_Delete 对兄弟用循环、对子树用递归,并尊重 IsReference/StringIsConst。
  4. 可插拔分配器internal_hooks 封装 malloc/free/realloc,用户可替换;若不用标准库的 malloc/free,则禁用 realloc,避免跨分配器 realloc。
  5. 数字与 locale:NaN/Inf 输出为 “null”;数字先 15 位再 17 位精度;小数点按当前 locale 解析(parse_number 里替换 ‘.’)。
  6. 嵌套深度CJSON_NESTING_LIMIT 限制递归深度,防止恶意或异常深层 JSON 导致栈溢出。

六、深度注释实践(可作规范)

对 cJSON 这类库做「源码分析 + 深度注释」时,可以按三层来做,和本项目里的注释风格一致:

6.1 函数级注释(Doxygen 风格)

说明:作用、参数、返回值、谁负责释放内存、是否线程安全等。例如:

/** * @brief 解析 JSON 字符串,生成 cJSON 树 * @param value 以 '\0' 结尾的 JSON 字符串 * @return 成功:根节点指针;失败:NULL * @note 返回的节点树必须调用 cJSON_Delete() 释放 * @note 失败时可调用 cJSON_GetErrorPtr() 获取出错位置 */CJSON_PUBLIC(cJSON *)cJSON_Parse(constchar*value);

重点:凡是返回「新分配的 cJSON 或 char」,都要写清楚由谁、用什么 API 释放。*

6.2 代码块注释

对「一整段逻辑」用块注释说明:在做什么、为什么这样写、有没有内存/错误处理上的注意点。例如:

/* ===== 递归解析 JSON 数组 ===== * 算法:遇到 '[' 后循环解析每个元素,next/prev 链成链表, * 最后 head->prev = 尾节点。失败则 cJSON_Delete(head) 统一释放。 * 递归深度:受 CJSON_NESTING_LIMIT 限制。 */

6.3 关键行注释

对「容易误解」或「和规范/安全相关」的代码行做简短说明。例如:

input_buffer->offset++;/* 跳过 '[',进入数组内容 */buffer_skip_whitespace(input_buffer);/* 跳过 \r\n\t 等空白,符合 JSON 规范 */if(input_buffer->depth >= CJSON_NESTING_LIMIT)/* 嵌套过深,防止栈溢出 */return false;

这样配合源码阅读,可以快速对应到「数据结构」「解析/生成流程」和「内存与安全」几个维度。


七、如何运行与测试

说明docs/cJSON_annotated.cdocs/cJSON_annotated.h带注释的文档型副本,不能直接替代工程里的 cJSON 使用;实际编译、运行、测试请用仓库根目录下的原始 cJSON.ccJSON.h

7.1 单文件 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 的注释对照看。

7.2 用官方测试套件(CMake)

若要跑 cJSON 仓库自带的单元测试:

cd /path/to/cJSON-1.7.19 mkdir build &&cd build cmake .. cmake --build. ctest 

八、总结

  • cJSON 用「一个 cJSON 结构体 + 树状链表(child + next/prev)」表示整棵 JSON 树,类型用位掩码,便于扩展标记。
  • 解析cJSON_Parseparse_value 递归分派到 null/true/false/string/number/array/object,数组和对象内部用链表 + 深度限制保证安全和清晰。
  • 生成cJSON_Printprint_value 按类型输出,缓冲区由 ensure 动态扩容。
  • 深度注释时可按「函数级(含内存责任)/ 代码块 / 关键行」三层来做,便于复习和写报告。
  • 实际运行和测试请用原始 cJSON.c + cJSON.h,配合仓库中的 test_cjson_demo.c 或官方测试即可。

如果你在做「cJSON 源码分析」课程作业或技术博客,可以直接把本文当作提纲,再结合项目里的 01-核心数据结构分析.md02-核心流程分析.md 和带注释的 cJSON_annotated.c/.h 做细化与引用。


参考资料

  • cJSON 源码:https://github.com/DaveGamble/cJSON
  • JSON 规范:https://www.json.org/
  • 本项目分析文档与注释版源码:见仓库 fuzzing/docs/ 目录(01/02 分析文档、cJSON_annotated.c/.h、test_cjson_demo.c)

Read more

深挖 DeepSeek 隐藏玩法·智能炼金术2.0版本

深挖 DeepSeek 隐藏玩法·智能炼金术2.0版本

前引:屏幕前的你还在AI智能搜索框这样搜索吗?“这道题怎么写”“苹果为什么红”“怎么不被发现翘课” ,。看到此篇文章的小伙伴们!请准备好你的思维魔杖,开启【霍格沃茨模式】,看我如何更新秘密的【知识炼金术】,我们一起来解锁更加刺激的剧情!友情提醒:《《《前方高能》》》 目录 在哪使用DeepSeek 如何对提需求  隐藏玩法总结 几个高阶提示词 职场打工人 自媒体创作 电商实战 程序员开挂 非适用场地 “服务器繁忙”如何解决 (1)硅基流动平台 (2)Chatbox + API集成方案 (3)各大云平台 搭建个人知识库 前置准备 下载安装AnythingLLM 选择DeepSeek作为AI提供商 创作工作区 导入文档 编辑  编辑 小编寄语 ——————————————————————————————————————————— 在哪使用DeepSeek 我们解锁剧情前,肯定要知道在哪用DeepSeek!咯,为了照顾一些萌新朋友,它的下载方式我放在下面了,拿走不谢!  (1)

By Ne0inhk
【AI大模型】DeepSeek + 通义万相高效制作AI视频实战详解

【AI大模型】DeepSeek + 通义万相高效制作AI视频实战详解

目录 一、前言 二、AI视频概述 2.1 什么是AI视频 2.2 AI视频核心特点 2.3 AI视频应用场景 三、通义万相介绍 3.1 通义万相概述 3.1.1 什么是通义万相 3.2 通义万相核心特点 3.3 通义万相技术特点 3.4 通义万相应用场景 四、DeepSeek + 通义万相制作AI视频流程 4.1 DeepSeek + 通义万相制作视频优势 4.1.1 DeepSeek 优势 4.1.2 通义万相视频生成优势 4.2

By Ne0inhk
【DeepSeek微调实践】DeepSeek-R1大模型基于MS-Swift框架部署/推理/微调实践大全

【DeepSeek微调实践】DeepSeek-R1大模型基于MS-Swift框架部署/推理/微调实践大全

系列篇章💥 No.文章01【DeepSeek应用实践】DeepSeek接入Word、WPS方法详解:无需代码,轻松实现智能办公助手功能02【DeepSeek应用实践】通义灵码 + DeepSeek:AI 编程助手的实战指南03【DeepSeek应用实践】Cline集成DeepSeek:开源AI编程助手,终端与Web开发的超强助力04【DeepSeek开发入门】DeepSeek API 开发初体验05【DeepSeek开发入门】DeepSeek API高级开发指南(推理与多轮对话机器人实践)06【DeepSeek开发入门】Function Calling 函数功能应用实战指南07【DeepSeek部署实战】DeepSeek-R1-Distill-Qwen-7B:本地部署与API服务快速上手08【DeepSeek部署实战】DeepSeek-R1-Distill-Qwen-7B:Web聊天机器人部署指南09【DeepSeek部署实战】DeepSeek-R1-Distill-Qwen-7B:基于vLLM 搭建高性能推理服务器10【DeepSeek部署实战】基于Ollama快速部署Dee

By Ne0inhk

用DeepSeek和Cursor从零打造智能代码审查工具:我的AI编程实践

💂 个人网站:【 摸鱼游戏】【神级代码资源网站】【星海网址导航】摸鱼、技术交流群👉 点此查看详情 引言:AI编程革命下的机遇与挑战 GitHub统计显示,使用AI编程工具的开发者平均效率提升55%,但仅有23%的开发者能充分发挥这些工具的潜力。作为一名全栈工程师,我曾对AI编程持怀疑态度,直到一次紧急项目让我彻底改变了看法。客户要求在72小时内交付一个能自动检测代码漏洞、优化性能的智能审查系统,传统开发方式根本不可能完成。正是这次挑战,让我探索出DeepSeek和Cursor这对"黄金组合"的惊人潜力。 一、工具选型:深入比较主流AI编程工具 1.1 为什么最终选择DeepSeek+Cursor? 经过两周的对比测试,我们发现不同工具在代码审查场景的表现差异显著: 工具代码理解深度响应速度定制灵活性多语言支持GitHub Copilot★★★☆★★★★★★☆★★★★Amazon CodeWhisperer★★☆★★★☆★★★★★★☆DeepSeek★★★★☆★★★★★★★☆★★★★☆Cursor★★★☆★★★★☆★★★★★★★★ 关键发现: * Dee

By Ne0inhk