4155 字
21 分钟
JSON & cJSON

1.JSON 语法规则详解#

JSON (JavaScript Object Notation) 是一种轻量级的数据交换格式,其语法是 JavaScript 对象的子集,但独立于语言。

1. 基础结构#

JSON 是一个由以下两种结构组成的纯字符串:

  • 对象 (Object): 由花括号 {} 包裹,内部是键值对集合。
  • 数组 (Array): 由方括号 [] 包裹,内部是值的有序列表。
// 这是一个 JSON 对象
{
"name": "Alice",
"age": 30,
"isStudent": false,
"hobbies": ["reading", "cycling"], // 值可以是数组
"address": { // 值也可以是另一个对象
"street": "123 Main St",
"city": "New York"
}
}
// 这是一个 JSON 数组
["apple", "banana", "cherry"]

注释: JSON 文件通常以对象或数组作为根元素。


2. 数据类型#

JSON 值必须是以下六种数据类型之一:

类型示例注释
字符串 (String)"Hello World"必须使用双引号包裹。
数字 (Number)42, 3.14159, -7, 0e0可以是整数或浮点数,不支持 NaN 和 Infinity。
布尔值 (Boolean)true, false必须是小写,没有引号。
空值 (Null)null必须是小写,表示空值。
对象 (Object){"key": "value"}无序的键值对集合。
数组 (Array)[1, 2, 3]有序的值列表。

注释: JSON 没有 undefinedDate 或函数等 JavaScript 特有的类型。


3. 对象 (Object) 的具体规则#

  • 花括号包裹: 对象必须由花括号 {} 包裹。
  • 键值对结构: 内容由逗号分隔的键值对组成。
  • 键 (Key):
    • 必须是字符串
    • 必须用双引号 "" 包裹
    • 在同一对象中必须是唯一的
  • 值 (Value): 可以是任何有效的 JSON 数据类型。
  • 分隔符: 键和值之间用冒号 : 分隔。
  • 尾逗号问题: 最后一个键值对后面不能有逗号
// 正确示例
{
"name": "Bob", // 键"name"是带双引号的字符串
"age": 25 // 注意:最后一个属性后无逗号
}
// 错误示例
{
name: "Bob", // 错误:键缺少双引号
"age": 25, // 错误:最后一个属性后面多了逗号(在某些解析器中会报错)
}

4. 数组 (Array) 的具体规则#

  • 方括号包裹: 数组必须由方括号 [] 包裹。
  • 值的有序列表: 内容是由逗号分隔的值列表。
  • 值的类型: 数组中的值可以是任何有效的 JSON 数据类型,且类型可以混用。
  • 尾逗号问题: 最后一个值后面不能有逗号
// 正确示例
["a", "b", "c"] // 简单字符串数组
// 复杂数组示例
[
"string",
42,
null,
true,
{"key": "value"},
[1, 2, 3]
]
// 错误示例
["a", "b", "c",] // 错误:最后一个元素后面多了逗号

5. 字符串 (String) 的具体规则#

  • 双引号包裹: 必须使用双引号 "" 包裹,单引号无效。
  • 字符转义: 某些特殊字符必须使用反斜杠 \ 进行转义。
转义序列含义
\"双引号 (避免与字符串结尾引号冲突)
\\反斜杠本身
\/正斜杠 (可选,但通常不强制要求)
\b退格符 (backspace)
\f换页符 (formfeed)
\n换行符 (newline)
\r回车符 (carriage return)
\t制表符 (tab)
\uXXXXUnicode 字符 (如 \u4e2d 表示汉字“中”)
// 正确示例
"It's a beautiful day." // 字符串内的单引号不需要转义
"He said: \"Hello!\""
"Path: C:\\Windows\\System32" // 路径中的反斜杠需要转义
"Multi\nLine" // 包含换行符
// 错误示例
'This is not valid JSON' // 错误:使用了单引号

6. 数字 (Number) 的具体规则#

  • 格式: 可以是整数、浮点数、负数或使用科学计数法表示。
  • 表示法:
    • 不能以 0 开头(除了 0 本身和小数),即不能有前导零。
    • 至少有一位数字。
    • 可以有负号 - 表示负数。
  • 不支持 NaN, Infinity, -Infinity
// 正确示例
42
3.14159
-10
0.5
1.0e10 // 科学计数法:1.0 * 10^10
2e-5 // 科学计数法:2 * 10^-5
// 错误示例 (在标准JSON中无效)
010 // 错误:前导零
0. // 错误:小数点后必须有数字
+.5 // 错误:没有正号表示法
NaN
Infinity

7. 空白字符#

  • 允许的空白字符: 可以在任何令牌(如大括号、冒号、逗号)之间插入以下空白字符,用于格式化,增强可读性。
    • 空格 (space )
    • 制表符 (tab \t)
    • 换行符 (newline \n)
    • 回车符 (carriage return \r)
  • 不允许的位置: 不能在键、字符串值或数字中间插入空白(字符串值内的转义字符除外)。
// 以下都是有效的等价JSON
{"name":"Alice","age":30}
{ "name": "Alice", "age": 30 } // 使用空格美化
{
"name": "Alice", // 使用换行和缩进美化
"age": 30
}

8. 注释#

  • 官方标准: 根据 RFC 7159 标准,JSON 不支持注释
  • 实践变通: 虽然标准不支持,但某些解析器允许使用一些“黑客”技巧来添加伪注释,但这会破坏兼容性,强烈不推荐
    • 不推荐的做法: 使用多余的键来充当注释。
    {
    "//": "这是一个伪注释,但不应该这样做",
    "data": "actual value"
    }
    注释: 为了最大兼容性,请永远不要在需要交换或存储的 JSON 文件中使用任何形式的注释。配置文件的注释需求应由其他格式(如 JSON5, YAML, TOML)来满足。

9.总结#

规则类别要点必须遵守
结构{}[] 构成
引号所有字符串必须使用双引号
逗号对象或数组中,最后一项后不能有逗号
数据类型仅限 string, number, boolean, null, object, array
键名对象中的键名必须是唯一的字符串
注释不支持

遵循这些规则才能生成标准、无歧义且可被所有 JSON 解析器正确读取的 JSON 数据。在编写后,可以使用在线的 JSON 验证工具(如 JSONLint)来检查语法是否正确。

2.cJSON库的使用#

cJSON 是一个用 C 语言编写的轻量级 JSON 解析库。它非常适合用于资源受限的环境,如嵌入式系统、物联网设备,或者当你需要在 C 项目中处理 JSON 数据而又不想引入大型依赖时。

1.移植#

  1. 获取源码:直接从官方 GitHub 仓库 (https://github.com/DaveGamble/cJSON) 下载 cJSON.ccJSON.h 文件。
  2. 添加到项目:把这两个文件复制到你的项目目录中,并在你的源文件中包含头文件 #include "cJSON.h"
  3. 编译链接:在编译时,确保将 cJSON.c 一起编译。

2.核心数据结构#

cJSON 库的核心是 cJSON 结构体,它用于表示所有的 JSON 数据类型:

typedef struct cJSON {
struct cJSON *next; // 链表后指针,用于兄弟节点
struct cJSON *prev; // 链表前指针,用于兄弟节点
struct cJSON *child; // 子节点指针,用于对象或数组的子元素
int type; // 节点类型(如对象、数组、字符串、数字等)
char *valuestring; // 如果类型是字符串,这里存储字符串值
double valuedouble; // 存储数字值(整数和浮点数都使用此字段)
char *string; // 对象的键名(key)
} cJSON;

这种双向链表和树形结构混合的设计,使得遍历和操作 JSON 数据变得高效。

JSON 数据类型通过 type 字段来区分,常用的类型宏定义有:

  • cJSON_Object:对象
  • cJSON_Array:数组
  • cJSON_String:字符串
  • cJSON_Number:数字
  • cJSON_True:布尔值1
  • cJSON_False:布尔值0
  • cJSON_NULL:空

3.主要 API 功能#

1. JSON 解析#

你可以将 JSON 字符串解析成 cJSON 对象树,然后方便地访问其中的数据。

// 基础解析函数
const char *json_string = "{\"name\":\"Alice\", \"age\":25, \"city\":\"Beijing\"}";
cJSON *root = cJSON_Parse(json_string);
if (root == NULL) {
// 处理解析错误
const char *error_ptr = cJSON_GetErrorPtr();
if (error_ptr != NULL) {
fprintf(stderr, "Error before: %s\n", error_ptr);
}
}
// 更高级的解析选项,允许获取解析结束位置等
const char *end_ptr = NULL;
cJSON *root = cJSON_ParseWithOpts(json_string, &end_ptr, 1);

2. 数据访问与查询#

解析成功后,你可以像这样访问数据:

// 获取对象成员
cJSON *name = cJSON_GetObjectItemCaseSensitive(root, "name");
if (cJSON_IsString(name) && (name->valuestring != NULL)) {
printf("Name: %s\n", name->valuestring);
}
cJSON *age = cJSON_GetObjectItemCaseSensitive(root, "age");
if (cJSON_IsNumber(age)) {
printf("Age: %d\n", age->valueint); // 对于整数
// 或者使用 age->valuedouble 获取浮点值
}
// 处理数组
cJSON *array = cJSON_GetObjectItemCaseSensitive(root, "hobbies");
if (cJSON_IsArray(array)) {
int array_size = cJSON_GetArraySize(array);
for (int i = 0; i < array_size; i++) {
cJSON *item = cJSON_GetArrayItem(array, i);
if (cJSON_IsString(item) && (item->valuestring != NULL)) {
printf("Hobby %d: %s\n", i, item->valuestring);
}
}
}

3. JSON 生成与序列化#

你可以使用 cJSON 提供的 API 构建复杂的 JSON 结构,并将其转换为字符串。

// 创建一个对象
cJSON *root = cJSON_CreateObject();
// 添加各种类型的键值对
cJSON_AddStringToObject(root, "name", "Bob");
cJSON_AddNumberToObject(root, "age", 30);
cJSON_AddBoolToObject(root, "is_student", false); // 或 cJSON_AddTrueToObject / cJSON_AddFalseToObject
// 创建并添加一个嵌套对象
cJSON *address = cJSON_CreateObject();
cJSON_AddStringToObject(address, "city", "Shanghai");
cJSON_AddStringToObject(address, "street", "Nanjing Road");
cJSON_AddItemToObject(root, "address", address);
// 创建并添加一个数组
cJSON *hobbies = cJSON_CreateArray();
cJSON_AddItemToArray(hobbies, cJSON_CreateString("reading"));
cJSON_AddItemToArray(hobbies, cJSON_CreateString("swimming"));
cJSON_AddItemToObject(root, "hobbies", hobbies);
// 将 cJSON 对象转换为字符串
char *json_str = cJSON_Print(root); // 格式化的输出,带缩进和换行
// 或者使用 cJSON_PrintUnformatted(root); 获取紧凑格式的字符串
printf("%s\n", json_str);
// 记得释放字符串!
free(json_str);

4. 内存管理#

cJSON 需要你手动管理内存,这一点非常重要:

// 解析 JSON 后,最终需要删除根节点来释放整个树
cJSON_Delete(root);
// 由 cJSON_Print 或 cJSON_PrintUnformatted 返回的字符串需要使用 free() 释放
free(json_str);
// 你可以自定义内存分配器(例如在嵌入式系统中)
cJSON_Hooks hooks;
hooks.malloc_fn = my_custom_malloc;
hooks.free_fn = my_custom_free;
cJSON_InitHooks(&hooks);

好的,cJSON 库虽然轻量,但提供了非常丰富的函数集来处理 JSON 数据的创建、解析、访问和修改。下面我将按照功能类别详细列举 cJSON 中常用的函数,并对每个函数进行说明。

4.cJSON API 函数详解#

1. 解析函数 (Parse)#

这些函数用于将 JSON 格式的字符串解析成 cJSON 对象树。

函数参数返回值说明
cJSON_Parseconst char *valuecJSON*核心解析函数。将给定的 JSON 字符串解析成一个 cJSON 对象树。解析失败返回 NULL
cJSON_ParseWithOptsconst char *value, const char **return_parse_end, cJSON_bool require_null_terminatedcJSON*带选项的解析函数。功能更强大,可以获取解析结束的位置,并选择是否要求字符串以空字符结尾。
cJSON_GetErrorPtrvoidconst char*cJSON_Parse 失败时,调用此函数可以获取一个指向解析错误发生位置的指针。

示例:

const char *json_string = "{\"name\":\"Alice\", \"age\":30}";
cJSON *root = cJSON_Parse(json_string);
if (root == NULL) {
const char *error_ptr = cJSON_GetErrorPtr();
printf("Error before: %s\n", error_ptr);
}
// ... 使用 root ...
cJSON_Delete(root); // 必须记得删除!

2. 创建函数#

这些函数用于在内存中从头开始创建 cJSON 对象。

2.1 基础创建函数(Create)#

函数返回值说明
cJSON_CreateObjectcJSON*创建一个空的 JSON 对象 ({})。
cJSON_CreateArraycJSON*创建一个空的 JSON 数组 ([])。
cJSON_CreateStringcJSON*创建一个 字符串 类型的 cJSON 项。需要提供 const char *string 参数。
cJSON_CreateNumbercJSON*创建一个 数字 类型的 cJSON 项。需要提供 double num 参数。
cJSON_CreateBoolcJSON*创建一个 布尔值 类型的 cJSON 项。需要提供 cJSON_bool boolean 参数(通常用 1 表示 true, 0 表示 false)。
cJSON_CreateTruecJSON*直接创建一个表示 true 的 cJSON 项。
cJSON_CreateFalsecJSON*直接创建一个表示 false 的 cJSON 项。
cJSON_CreateNullcJSON*创建一个表示 null 的 cJSON 项。
cJSON_CreateRawcJSON*创建一个原始的 JSON 字符串(不会被再次转义)。需要提供 const char *raw 参数。

2.2 便捷添加函数 (Add)#

这些函数将创建和添加步骤合二为一,非常常用。

函数说明
cJSON_AddStringToObject向对象中添加一个字符串键值对。参数:(cJSON *object, const char *name, const char *string)
cJSON_AddNumberToObject向对象中添加一个数字键值对。参数:(cJSON *object, const char *name, double number)
cJSON_AddBoolToObject向对象中添加一个布尔值键值对。参数:(cJSON *object, const char *name, cJSON_bool boolean)
cJSON_AddNullToObject向对象中添加一个 Null 键值对。参数:(cJSON *object, const char *name)
cJSON_AddItemToObject向对象中添加一个已创建的 cJSON 项(最通用的添加函数)。参数:(cJSON *object, const char *name, cJSON *item)
cJSON_AddItemToObjectCS与上一个类似,但假设键名 (name) 是常量字符串或长期存在的,库不会复制它。使用需谨慎

2.3 数组创建与添加函数(Add)#

函数说明
cJSON_AddItemToArray向数组末尾添加一个 cJSON 项。参数:(cJSON *array, cJSON *item)
cJSON_CreateIntArray用一个整数 int 数组快速创建一个 cJSON 数组。参数:(const int *numbers, int count)
cJSON_CreateFloatArray用一个浮点数 float 数组快速创建。参数:(const float *numbers, int count)
cJSON_CreateDoubleArray用一个双精度浮点数 double 数组快速创建。参数:(const double *numbers, int count)
cJSON_CreateStringArray用一个字符串 const char* 数组快速创建。参数:(const char *const *strings, int count)

创建示例:

cJSON *root = cJSON_CreateObject(); // 创建根对象
cJSON_AddStringToObject(root, "name", "Bob"); // 便捷添加字符串
cJSON_AddNumberToObject(root, "score", 95.5); // 便捷添加数字
cJSON *hobbies = cJSON_CreateArray(); // 手动创建数组
cJSON_AddItemToArray(hobbies, cJSON_CreateString("reading")); // 手动添加元素
cJSON_AddItemToArray(hobbies, cJSON_CreateString("gaming"));
cJSON_AddItemToObject(root, "hobbies", hobbies); // 将数组添加到对象
// ... 使用和删除 root ...

3. 访问与查询函数 (Get)#

这些函数用于从已解析或创建的 cJSON 树中获取数据。

函数返回值说明
cJSON_GetObjectItemcJSON*根据键名从对象中获取对应的 cJSON 项。不区分大小写
cJSON_GetObjectItemCaseSensitivecJSON*根据键名从对象中获取对应的 cJSON 项。区分大小写推荐使用
cJSON_GetArrayItemcJSON*根据索引从数组中获取对应的 cJSON 项。参数:(const cJSON *array, int index)
cJSON_GetArraySizeint获取数组的大小(即元素个数)。参数:(const cJSON *array)
cJSON_HasObjectItemcJSON_bool检查对象中是否包含指定的键。参数:(const cJSON *object, const char *string)

访问示例:

cJSON *name = cJSON_GetObjectItemCaseSensitive(root, "name");
if (cJSON_IsString(name)) { // 先检查类型!
printf("Name: %s\n", name->valuestring);
}
cJSON *scores = cJSON_GetObjectItemCaseSensitive(root, "scores");
if (cJSON_IsArray(scores)) {
int array_size = cJSON_GetArraySize(scores);
for (int i = 0; i < array_size; i++) {
cJSON *score_item = cJSON_GetArrayItem(scores, i);
if (cJSON_IsNumber(score_item)) {
printf("Score %d: %f\n", i, score_item->valuedouble);
}
}
}

4. 类型检查函数 (IS)#

这些宏用于安全地检查 cJSON 项的类型,在访问其值之前必须使用

说明
cJSON_IsInvalid检查项是否为无效的(通常来自错误的解析)。
cJSON_IsFalse检查项是否为 false
cJSON_IsTrue检查项是否为 true
cJSON_IsBool检查项是否为布尔值(True 或 False)。
cJSON_IsNull检查项是否为 null
cJSON_IsNumber检查项是否为数字。
cJSON_IsString检查项是否为字符串。
cJSON_IsArray检查项是否为数组。
cJSON_IsObject检查项是否为对象。
cJSON_IsRaw检查项是否为原始(Raw)类型。

5. 序列化/打印函数 (Print)#

这些函数用于将 cJSON 对象树转换回 JSON 字符串。

函数返回值说明
cJSON_Printchar*将 cJSON 项格式化为可读的、带缩进和换行的 JSON 字符串。返回的字符串必须用 cJSON_free() 释放
cJSON_PrintUnformattedchar*将 cJSON 项格式化为紧凑的、没有额外空格的 JSON 字符串。性能稍好,体积更小。同样需要释放
cJSON_PrintBufferedchar*使用预分配的缓冲区进行打印,适用于对性能要求极高的场景。参数:(const cJSON *item, int prebuffer, cJSON_bool fmt)
cJSON_PrintPreallocatedcJSON_bool将 cJSON 项打印到用户提供的、预先分配好的缓冲区中。完全避免动态内存分配。参数:(cJSON *item, char *buffer, const int length, const cJSON_bool format)

打印示例:

cJSON *root = ...; // 某个 cJSON 对象
char *json_str = cJSON_Print(root); // 生成格式化的字符串
printf("%s\n", json_str);
cJSON_free(json_str); // 必须用 cJSON_free 释放!

6. 修改与删除函数 (Delete/Insert/Replace)#

这些函数用于修改现有的 cJSON 树或删除其中的项。

函数说明
cJSON_Delete最重要的函数。递归删除一个 cJSON 项及其所有子项,释放所有相关内存。参数:(cJSON *item)
cJSON_DeleteItemFromObject从对象中删除指定键名的项并释放它。参数:(cJSON *object, const char *string)
cJSON_DeleteItemFromArray从数组中删除指定索引的项并释放它。参数:(cJSON *array, int which)
cJSON_DeleteItemFromObjectCaseSensitive区分大小写地从对象中删除项。
cJSON_InsertItemInArray在数组的指定索引处插入一个项。参数:(cJSON *array, int which, cJSON *newitem)
cJSON_ReplaceItemInObject替换对象中指定键名的项。会删除旧的项。参数:(cJSON *object, const char *string, cJSON *newitem)
cJSON_ReplaceItemInArray替换数组中指定索引的项。会删除旧的项。参数:(cJSON *array, int which, cJSON *newitem)
cJSON_ReplaceItemInObjectCaseSensitive区分大小写地替换对象中的项。
cJSON_SetValuestring设置一个字符串类型的 cJSON 项的值。参数:(cJSON *object, const char *valuestring)

7. 工具函数#

函数说明
cJSON_Duplicate深度复制一个 cJSON 项及其所有子项。参数:(const cJSON *item, cJSON_bool recurse)recursetrue 表示递归复制所有子节点。返回的副本也需要用 cJSON_Delete 释放
cJSON_Compare比较两个 cJSON 项是否在逻辑上相等(包括值和顺序)。参数:(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive)
cJSON_InitHooks允许用户自定义内存分配和释放函数(钩子),常用于嵌入式系统。参数:(cJSON_Hooks* hooks)

8. 内存管理总结#

操作创建/分配函数对应的释放/删除函数
创建 cJSON 结构体cJSON_Parse, cJSON_CreateObject, cJSON_CreateArraycJSON_Delete
生成 JSON 字符串cJSON_Print, cJSON_PrintUnformattedcJSON_free (或自定义的 free 函数)

掌握这些函数后,你就能高效、安全地在你的 C 项目中使用 cJSON 库来处理所有 JSON 数据交换任务了。核心原则永远是:检查返回值、检查类型、配对释放内存

JSON & cJSON
https://fuwari.vercel.app/posts/jsoncjson/json--cjson/
作者
南星
发布于
2025-09-13
许可协议
CC BY-NC-SA 4.0