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 没有 undefined、Date 或函数等 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) |
\uXXXX | Unicode 字符 (如 \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。
// 正确示例423.14159-100.51.0e10 // 科学计数法:1.0 * 10^102e-5 // 科学计数法:2 * 10^-5
// 错误示例 (在标准JSON中无效)010 // 错误:前导零0. // 错误:小数点后必须有数字+.5 // 错误:没有正号表示法NaNInfinity7. 空白字符
- 允许的空白字符: 可以在任何令牌(如大括号、冒号、逗号)之间插入以下空白字符,用于格式化,增强可读性。
- 空格 (space
) - 制表符 (tab
\t) - 换行符 (newline
\n) - 回车符 (carriage return
\r)
- 空格 (space
- 不允许的位置: 不能在键、字符串值或数字中间插入空白(字符串值内的转义字符除外)。
// 以下都是有效的等价JSON{"name":"Alice","age":30}{ "name": "Alice", "age": 30 } // 使用空格美化{ "name": "Alice", // 使用换行和缩进美化 "age": 30}8. 注释
- 官方标准: 根据 RFC 7159 标准,JSON 不支持注释。
- 实践变通: 虽然标准不支持,但某些解析器允许使用一些“黑客”技巧来添加伪注释,但这会破坏兼容性,强烈不推荐。
- 不推荐的做法: 使用多余的键来充当注释。
注释: 为了最大兼容性,请永远不要在需要交换或存储的 JSON 文件中使用任何形式的注释。配置文件的注释需求应由其他格式(如 JSON5, YAML, TOML)来满足。{"//": "这是一个伪注释,但不应该这样做","data": "actual value"}
9.总结
| 规则类别 | 要点 | 必须遵守 |
|---|---|---|
| 结构 | 由 {} 或 [] 构成 | ✅ |
| 引号 | 所有键和字符串必须使用双引号 | ✅ |
| 逗号 | 对象或数组中,最后一项后不能有逗号 | ✅ |
| 数据类型 | 仅限 string, number, boolean, null, object, array | ✅ |
| 键名 | 对象中的键名必须是唯一的字符串 | ✅ |
| 注释 | 不支持 | ✅ |
遵循这些规则才能生成标准、无歧义且可被所有 JSON 解析器正确读取的 JSON 数据。在编写后,可以使用在线的 JSON 验证工具(如 JSONLint)来检查语法是否正确。
2.cJSON库的使用
cJSON 是一个用 C 语言编写的轻量级 JSON 解析库。它非常适合用于资源受限的环境,如嵌入式系统、物联网设备,或者当你需要在 C 项目中处理 JSON 数据而又不想引入大型依赖时。
1.移植
- 获取源码:直接从官方 GitHub 仓库 (https://github.com/DaveGamble/cJSON) 下载
cJSON.c和cJSON.h文件。 - 添加到项目:把这两个文件复制到你的项目目录中,并在你的源文件中包含头文件
#include "cJSON.h"。 - 编译链接:在编译时,确保将
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:布尔值1cJSON_False:布尔值0cJSON_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_Parse | const char *value | cJSON* | 核心解析函数。将给定的 JSON 字符串解析成一个 cJSON 对象树。解析失败返回 NULL。 |
cJSON_ParseWithOpts | const char *value, const char **return_parse_end, cJSON_bool require_null_terminated | cJSON* | 带选项的解析函数。功能更强大,可以获取解析结束的位置,并选择是否要求字符串以空字符结尾。 |
cJSON_GetErrorPtr | void | const 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_CreateObject | cJSON* | 创建一个空的 JSON 对象 ({})。 |
cJSON_CreateArray | cJSON* | 创建一个空的 JSON 数组 ([])。 |
cJSON_CreateString | cJSON* | 创建一个 字符串 类型的 cJSON 项。需要提供 const char *string 参数。 |
cJSON_CreateNumber | cJSON* | 创建一个 数字 类型的 cJSON 项。需要提供 double num 参数。 |
cJSON_CreateBool | cJSON* | 创建一个 布尔值 类型的 cJSON 项。需要提供 cJSON_bool boolean 参数(通常用 1 表示 true, 0 表示 false)。 |
cJSON_CreateTrue | cJSON* | 直接创建一个表示 true 的 cJSON 项。 |
cJSON_CreateFalse | cJSON* | 直接创建一个表示 false 的 cJSON 项。 |
cJSON_CreateNull | cJSON* | 创建一个表示 null 的 cJSON 项。 |
cJSON_CreateRaw | cJSON* | 创建一个原始的 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_GetObjectItem | cJSON* | 根据键名从对象中获取对应的 cJSON 项。不区分大小写。 |
cJSON_GetObjectItemCaseSensitive | cJSON* | 根据键名从对象中获取对应的 cJSON 项。区分大小写。推荐使用。 |
cJSON_GetArrayItem | cJSON* | 根据索引从数组中获取对应的 cJSON 项。参数:(const cJSON *array, int index) |
cJSON_GetArraySize | int | 获取数组的大小(即元素个数)。参数:(const cJSON *array) |
cJSON_HasObjectItem | cJSON_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_Print | char* | 将 cJSON 项格式化为可读的、带缩进和换行的 JSON 字符串。返回的字符串必须用 cJSON_free() 释放。 |
cJSON_PrintUnformatted | char* | 将 cJSON 项格式化为紧凑的、没有额外空格的 JSON 字符串。性能稍好,体积更小。同样需要释放。 |
cJSON_PrintBuffered | char* | 使用预分配的缓冲区进行打印,适用于对性能要求极高的场景。参数:(const cJSON *item, int prebuffer, cJSON_bool fmt) |
cJSON_PrintPreallocated | cJSON_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)。recurse 为 true 表示递归复制所有子节点。返回的副本也需要用 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_CreateArray 等 | cJSON_Delete |
| 生成 JSON 字符串 | cJSON_Print, cJSON_PrintUnformatted | cJSON_free (或自定义的 free 函数) |
掌握这些函数后,你就能高效、安全地在你的 C 项目中使用 cJSON 库来处理所有 JSON 数据交换任务了。核心原则永远是:检查返回值、检查类型、配对释放内存。