JSON 核心概念
JSON(JavaScript Object Notation)是 “JavaScript 对象表示法” 的简称,是一种轻量级数据交换文本格式,不依赖任何编程语言。它具有简洁清晰的层次结构,易于人类阅读编写,同时便于机器解析和生成,现已成为 Web 开发、跨平台应用间数据传输的主流格式。
JSON 数据格式
JSON 的核心数据结构只有两种,可相互嵌套组成复杂数据:
对象(无序键值对集合)
- 用花括号
{}包围,键值对以:分隔,多个键值对用,分隔。 - 键必须是字符串(用双引号
""包裹),值可是任意合法 JSON 类型。 - 示例(修正原文档键名重复错误):
{"name": "lmx", // 值类型:字符串"age": 29, // 值类型:整型"score": [ // 值类型:数组(元素为对象){"math": 85.5, // 值类型:浮点型"english": 92 // 值类型:整型}]
}
数组(有序值集合)
- 用方括号
[]包围,多个值用,分隔。 - 数组中的值可是任意合法 JSON 类型,包括对象和数组。
- 示例:
["apple", "banana", 100, true, null, {"city": "Beijing"}]
合法值类型
- 字符串(双引号包裹,不可用单引号)
- 数值(整型、浮点型,无引号)
- 布尔值(
true或false,小写) - 空值(
null,小写) - 对象(
{}包裹的键值对集合) - 数组(
[]包裹的值集合)
cJSON 库介绍
cJSON 是基于 C 语言的轻量级 JSON 解析 / 构造库,开源且跨平台(兼容 C89 标准),适用于嵌入式开发等场景。
获取与使用
- 下载地址:https://github.com/DaveGamble/cJSON
- 核心文件:仅需cJSON.c(实现文件)和cJSON.h(头文件),直接复制到项目中即可使用。
- 编译方式:将
cJSON.c与项目源码一起编译(如gcc main.c cJSON.c -o main)。
核心结构体(cJSON)
用于存储 JSON 数据的树形结构节点,每个节点对应一个 JSON 值:
/*** cJSON核心结构体,存储单个JSON节点数据* @brief 构成JSON树形结构的基本单元,每个节点对应一个JSON值(字符串、数字、对象等)* @param next 链表下一个节点指针(用于遍历数组/对象的子节点)* @param prev 链表上一个节点指针(用于双向遍历)* @param child 子节点指针(对象/数组类型节点的子元素链表头)* @param type 节点类型(如cJSON_String、cJSON_Number、cJSON_Object等)* @param valuestring 字符串类型值(仅type为cJSON_String或cJSON_Raw时有效)* @param valueint 整型值(type为cJSON_Number时有效,建议用cJSON_SetNumberValue设置)* @param valuedouble 浮点型值(type为cJSON_Number时有效)* @param string 节点名称(仅当节点是对象的子节点时有效,即键名)*/
typedef struct cJSON {struct cJSON *next;struct cJSON *prev;struct cJSON *child;int type;char *valuestring;int valueint;double valuedouble;char *string;
} cJSON;
JSON 解析流程(cJSON 实现)
解析流程:获取 JSON 字符串 → 解析为 cJSON 树形结构 → 提取数据 → 释放资源
关键函数
解析 JSON 字符串
/*** 解析JSON格式字符串为cJSON树形结构* @brief 将零终止的JSON字符串转换为cJSON结构体指针,后续可通过该指针遍历提取数据* @param string 输入的JSON格式字符串(必须零终止,格式合法)* @return cJSON* 成功返回指向cJSON树形结构根节点的指针,失败返回NULL* @note 解析成功后需调用cJSON_Delete释放内存,避免内存泄漏* 若JSON字符串格式错误(如括号不匹配、键未用双引号),返回NULL*/
cJSON *cJSON_Parse(const char *string);
调试输出 JSON
/*** 将cJSON树形结构转换为格式化的JSON字符串* @brief 用于调试验证解析结果,生成易读的JSON字符串* @param json 指向cJSON树形结构根节点的指针* @return char* 成功返回动态分配的JSON字符串,失败返回NULL* @note 返回的字符串需手动用free释放,否则会造成内存泄漏* 生成的字符串包含换行和缩进,便于人类阅读*/
char *cJSON_Print(const cJSON *json);
获取对象中的键值对
/*** 从JSON对象中获取指定键名的子节点(不区分大小写)* @brief 用于提取对象类型节点中某个键对应的值节点* @param object 指向JSON对象节点的指针(type必须为cJSON_Object)* @param string 要查找的键名(字符串)* @return cJSON* 成功返回对应值节点的指针,失败返回NULL* @note 若对象中无该键,或传入的节点不是对象类型,返回NULL* 区分大小写版本可用cJSON_GetObjectItemCaseSensitive*/
cJSON *cJSON_GetObjectItem(const cJSON *const object, const char *const string);
获取数组信息
/*** 获取JSON数组的元素个数* @brief 用于确定数组节点中包含的子元素数量* @param array 指向JSON数组节点的指针(type必须为cJSON_Array)* @return int 成功返回数组元素个数,失败返回0* @note 若传入的节点不是数组类型,返回0*/
int cJSON_GetArraySize(const cJSON *array);/*** 获取数组中指定索引的元素节点* @brief 按索引遍历数组,提取对应位置的子节点* @param array 指向JSON数组节点的指针(type必须为cJSON_Array)* @param index 元素索引(从0开始)* @return cJSON* 成功返回对应索引的元素节点指针,失败返回NULL* @note 索引超出数组范围(>=元素个数)时返回NULL*/
cJSON *cJSON_GetArrayItem(const cJSON *array, int index);
释放资源
/*** 释放cJSON树形结构占用的内存* @brief 递归释放整个cJSON链表的内存,包括所有子节点* @param item 指向cJSON树形结构根节点的指针* @return void 无返回值* @note 必须在解析完成后调用,否则会造成严重内存泄漏* 仅需传入根节点指针,会自动递归释放所有子节点*/
void cJSON_Delete(cJSON *item);
完整解析示例(解析天气 API 响应)
#include <stdio.h>
#include <stdlib.h>
#include "cJSON.h"int main() {// 假设这是从API获取的JSON字符串const char *json_str = "{""\"results\": [""{""\"path\": \"Beijing, Beijing, China\",""\"timezone\": \"Asia/Shanghai\",""\"timezone_offset\": \"+08:00\",""\"txt\": \"sunny\",""\"code\": \"0\",""\"temperature\": \"32\",""\"last_update\": \"2024-06-11T11:27:51+08:00\"""},""{""\"path\": \"Shanghai, Shanghai, China\",""\"timezone\": \"Asia/Shanghai\",""\"timezone_offset\": \"+08:00\",""\"txt\": \"cloudy\",""\"code\": \"1\",""\"temperature\": \"28\",""\"last_update\": \"2024-06-11T11:27:51+08:00\"""}""]""}";// 解析JSONcJSON *root = cJSON_Parse(json_str);if (root == NULL) {printf("JSON解析失败:%s\n", cJSON_GetErrorPtr());return -1;}// 获取results数组cJSON *results_array = cJSON_GetObjectItem(root, "results");if (results_array == NULL || !cJSON_IsArray(results_array)) {printf("未找到results数组\n");cJSON_Delete(root);return -1;}// 使用 cJSON_ArrayForEach 宏遍历数组// 宏的参数:当前元素节点指针, 数组节点指针cJSON *weather_obj;cJSON_ArrayForEach(weather_obj, results_array) {// 提取基础类型值cJSON *city = cJSON_GetObjectItem(weather_obj, "path");cJSON *weather = cJSON_GetObjectItem(weather_obj, "txt");cJSON *temp = cJSON_GetObjectItem(weather_obj, "temperature");cJSON *update_time = cJSON_GetObjectItem(weather_obj, "last_update");// 输出提取的信息if (city && cJSON_IsString(city) && weather && cJSON_IsString(weather) &&temp && cJSON_IsString(temp) && update_time && cJSON_IsString(update_time)) {printf("\n城市:%s\n", city->valuestring);printf("天气:%s\n", weather->valuestring);printf("温度:%s℃\n", temp->valuestring);printf("最后更新时间:%s\n", update_time->valuestring);}}// 释放资源cJSON_Delete(root);return 0;
}
JSON 构造流程(cJSON 实现)
构造流程:创建根对象 → 添加子节点 / 键值对 → 生成 JSON 字符串 → 释放资源
关键构造函数
/*** 创建JSON对象节点(对应{})* @brief 生成一个空的JSON对象节点,作为树形结构的根或子对象* @return cJSON* 成功返回对象节点指针,失败返回NULL* @note 需配合cJSON_AddItemToObject等函数添加键值对*/
cJSON *cJSON_CreateObject(void);/*** 创建JSON数组节点(对应[])* @brief 生成一个空的JSON数组节点* @return cJSON* 成功返回数组节点指针,失败返回NULL* @note 需配合cJSON_AddItemToArray等函数添加元素*/
cJSON *cJSON_CreateArray(void);/*** 创建字符串类型节点* @brief 生成存储指定字符串的JSON节点* @param string 要存储的字符串(零终止)* @return cJSON* 成功返回字符串节点指针,失败返回NULL*/
cJSON *cJSON_CreateString(const char *string);/*** 创建数值类型节点* @brief 生成存储指定浮点型数值的JSON节点(支持整型和浮点型)* @param num 要存储的数值(整型可直接传入,自动转换为double)* @return cJSON* 成功返回数值节点指针,失败返回NULL*/
cJSON *cJSON_CreateNumber(double num);/*** 向JSON对象添加键值对* @brief 将子节点作为指定键的值,添加到对象节点中* @param object 目标对象节点指针* @param string 键名(字符串)* @param item 要添加的子节点指针(任意JSON类型节点)* @return void 无返回值* @note 子节点的内存由cJSON管理,无需手动释放*/
void cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item);/*** 向JSON数组添加元素* @brief 将子节点添加到数组节点的末尾* @param array 目标数组节点指针* @param item 要添加的子节点指针(任意JSON类型节点)* @return void 无返回值*/
void cJSON_AddItemToArray(cJSON *array, cJSON *item);
完整构造示例
#include <stdio.h>
#include <stdlib.h>
#include "cJSON.h"
int main() {// 创建顶层对象(根节点)cJSON *root = cJSON_CreateObject();if (root == NULL) {printf("创建根对象失败\n");return -1;}// 向根对象添加基础类型键值对cJSON_AddItemToObject(root, "name", cJSON_CreateString("zhangsan"));cJSON_AddItemToObject(root, "age", cJSON_CreateNumber(25));cJSON_AddItemToObject(root, "is_student", cJSON_CreateBool(0)); // false// 创建数组并添加元素cJSON *hobbies = cJSON_CreateArray();cJSON_AddItemToArray(hobbies, cJSON_CreateString("reading"));cJSON_AddItemToArray(hobbies, cJSON_CreateString("coding"));cJSON_AddItemToArray(hobbies, cJSON_CreateString("hiking"));// 将数组添加到根对象cJSON_AddItemToObject(root, "hobbies", hobbies);// 创建嵌套对象并添加cJSON *address = cJSON_CreateObject();cJSON_AddItemToObject(address, "province", cJSON_CreateString("Guangdong"));cJSON_AddItemToObject(address, "city", cJSON_CreateString("Shenzhen"));cJSON_AddItemToObject(address, "detail", cJSON_CreateString("Nanshan District"));// 将嵌套对象添加到根对象cJSON_AddItemToObject(root, "address", address);// 生成JSON字符串(格式化输出)char *json_str = cJSON_Print(root);if (json_str == NULL) {printf("生成JSON字符串失败\n");cJSON_Delete(root);return -1;}printf("构造的JSON:\n%s\n", json_str);// 释放资源free(json_str);cJSON_Delete(root);return 0;
}
输出:
构造的JSON:
{"name": "zhangsan","age": 25,"is_student": false,"hobbies": ["reading", "coding", "hiking"],"address": {"province": "Guangdong","city": "Shenzhen","detail": "Nanshan District"}
}
实战练习:调用聚合天气 API 解析 JSON
核心步骤
- 申请聚合天气 API 接口,获取 API 密钥。
- 用 C 语言通过 HTTP 协议发送请求(如使用 libcurl 库),获取 JSON 格式的响应数据。
- 用 cJSON 库解析响应数据,提取城市、天气、温度、风力等信息。
- 将提取的信息输出到终端。
方式一:HTTP 请求实现(基于 libcurl)
需安装 libcurl 库(sudo apt-get install libcurl4-openssl-dev),编译时链接(gcc main.c cJSON.c -o main -lcurl):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>
#include "cJSON.h" /*** @brief 用于存储动态增长的响应数据的结构体*/
typedef struct {char *data;size_t size;
} ResponseBuffer;/*** @brief libcurl 的回调函数,用于接收 HTTP 响应数据* * @param ptr 指向接收到的数据的指针* @param size 每个数据块的大小(单位:字节)* @param nmemb 数据块的数量* @param userdata 用户自定义数据,这里是 ResponseBuffer 结构体指针* @return size_t 实际处理的数据大小(通常是 size * nmemb)*/
size_t write_callback(void *ptr, size_t size, size_t nmemb, void *userdata) {size_t realsize = size * nmemb;ResponseBuffer *buffer = (ResponseBuffer *)userdata;// 重新分配内存,为新数据腾出空间char *temp = (char *)realloc(buffer->data, buffer->size + realsize + 1);if (temp == NULL) {// 内存分配失败,这是一个严重错误fprintf(stderr, "Failed to allocate memory in write_callback\n");return 0; }buffer->data = temp;// 将新数据复制到缓冲区末尾memcpy(&(buffer->data[buffer->size]), ptr, realsize);buffer->size += realsize;// 确保字符串以 null 字符结尾buffer->data[buffer->size] = '\0';return realsize;
}/*** @brief 发送 HTTP GET 请求并返回响应体的 JSON 字符串* * @param url 请求的 URL* @return char* 成功则返回动态分配的 JSON 字符串,失败则返回 NULL*/
char *get_weather_json(const char *url) {CURL *curl;CURLcode res;ResponseBuffer buffer = {0}; // 初始化缓冲区// 全局初始化 libcurl (建议在程序开始时调用一次)curl_global_init(CURL_GLOBAL_DEFAULT);// 初始化一个 curl 会话curl = curl_easy_init();if (!curl) {fprintf(stderr, "curl_easy_init() failed\n");curl_global_cleanup();return NULL;}// 设置 curl 选项curl_easy_setopt(curl, CURLOPT_URL, url);// 设置回调函数和用户数据curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&buffer);// 执行请求res = curl_easy_perform(curl);if (res != CURLE_OK) {fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));free(buffer.data); // 释放已分配的内存buffer.data = NULL;}// 清理 curl 会话curl_easy_cleanup(curl);// 全局清理 libcurl (建议在程序结束时调用一次)curl_global_cleanup();// 返回响应数据(调用者需要负责 free)return buffer.data;
}int main() {// !!! 重要:请替换为你的聚合数据 API 密钥 !!!const char *api_key = "你的API密钥";const char *city = "Beijing";char api_url[256];// 构建完整的 API 请求 URLsnprintf(api_url, sizeof(api_url), "http://v.juhe.cn/tianqi/query?city=%s&key=%s", city, api_key);printf("正在请求天气数据...\nURL: %s\n", api_url);// 调用函数获取 JSON 字符串char *json_str = get_weather_json(api_url);if (json_str == NULL) {fprintf(stderr, "获取天气数据失败\n");return 1;}printf("\n成功获取到 JSON 数据:\n%s\n", json_str);// --- 使用 cJSON 解析 JSON 数据 ---cJSON *root = cJSON_Parse(json_str);if (root == NULL) {const char *error_ptr = cJSON_GetErrorPtr();if (error_ptr != NULL) {fprintf(stderr, "JSON 解析失败: %s\n", error_ptr);}free(json_str); // 释放 JSON 字符串return 1;}// 检查返回状态cJSON *error_code = cJSON_GetObjectItem(root, "error_code");if (error_code && error_code->valueint != 0) {cJSON *reason = cJSON_GetObjectItem(root, "reason");fprintf(stderr, "API 请求失败: %s (错误码: %d)\n", reason ? reason->valuestring : "未知错误", error_code->valueint);cJSON_Delete(root);free(json_str);return 1;}// 获取 results 数组cJSON *results_array = cJSON_GetObjectItem(root, "result");if (results_array == NULL || !cJSON_IsArray(results_array)) {printf("未找到 'result' 数组\n");cJSON_Delete(root);free(json_str);return 1;}// 使用 cJSON_ArrayForEach 宏遍历数组cJSON *weather_obj;cJSON_ArrayForEach(weather_obj, results_array) {cJSON *city_name = cJSON_GetObjectItem(weather_obj, "city");cJSON *weather = cJSON_GetObjectItem(weather_obj, "weather");cJSON *temperature = cJSON_GetObjectItem(weather_obj, "temperature");cJSON *humidity = cJSON_GetObjectItem(weather_obj, "humidity");cJSON *wind = cJSON_GetObjectItem(weather_obj, "wind");cJSON *update_time = cJSON_GetObjectItem(weather_obj, "update_time");// 打印提取的信息printf("\n--------------------\n");printf("城市: %s\n", city_name ? city_name->valuestring : "N/A");printf("天气: %s\n", weather ? weather->valuestring : "N/A");printf("温度: %s\n", temperature ? temperature->valuestring : "N/A");printf("湿度: %s\n", humidity ? humidity->valuestring : "N/A");printf("风向: %s\n", wind ? wind->valuestring : "N/A");printf("更新时间: %s\n", update_time ? update_time->valuestring : "N/A");}// --- 释放所有资源 ---cJSON_Delete(root); // 释放 cJSON 对象树free(json_str); // 释放从 get_weather_json 返回的字符串return 0;
}
方式二:popen 调用 curl 命令,并通过 fread 读取其输出
#include <stdio.h>
#include <stdlib.h>
#include <string.h>/*** @brief 使用 popen 和 curl 命令获取 HTTP 响应* * @param url 请求的 URL 地址* @return char* 成功则返回动态分配的响应字符串,失败则返回 NULL*/
char *get_weather_json_with_popen(const char *url) {if (url == NULL) {return NULL;}// 构建 curl 命令// -s: 静默模式,不显示进度条等额外信息// "%s": 要请求的 URLchar command[1024];int ret = snprintf(command, sizeof(command), "curl -s \"%s\"", url);if (ret < 0 || ret >= sizeof(command)) {fprintf(stderr, "命令字符串太长,可能会被截断。\n");return NULL;}// 使用 popen 执行命令,并以只读方式打开管道FILE *fp = popen(command, "r");if (fp == NULL) {perror("popen 失败");return NULL;}// 读取管道内容// 使用动态缓冲区来存储读取的数据char *response = NULL;size_t buffer_size = 1024; // 初始缓冲区大小size_t total_bytes_read = 0;// 初始分配内存response = (char *)malloc(buffer_size);if (response == NULL) {perror("malloc 失败");pclose(fp);return NULL;}response[0] = '\0'; // 确保初始为空字符串char chunk[256];size_t bytes_read;printf("正在通过 curl 命令获取数据...\n");// 循环读取,直到文件结束while ((bytes_read = fread(chunk, 1, sizeof(chunk) - 1, fp)) > 0) {// 确保 chunk 是一个以 null 结尾的字符串,方便 strcatchunk[bytes_read] = '\0';// 检查缓冲区是否足够容纳新读取的数据if (total_bytes_read + bytes_read >= buffer_size - 1) { // 预留一个字节给 null 结束符buffer_size *= 2; // 翻倍扩容char *temp = (char *)realloc(response, buffer_size);if (temp == NULL) {perror("realloc 失败");free(response);pclose(fp);return NULL;}response = temp;}// 将新读取的数据追加到响应字符串末尾strcat(response, chunk);total_bytes_read += bytes_read;}// 检查 fread 是否因为错误而退出if (ferror(fp)) {perror("fread 失败");free(response);response = NULL;}// 关闭管道// pclose 会等待子进程 (curl) 结束并回收其资源if (pclose(fp) == -1) {perror("pclose 失败");// 即使 pclose 失败,我们也已经读取了数据,所以可以选择返回它// 这里为了简化,我们在发生任何错误时都返回 NULLfree(response);response = NULL;}return response;
}int main() {// !!! 重要:请替换为你的聚合数据 API 密钥 !!!const char *api_key = "你的API密钥";const char *city = "Beijing";char api_url[256];// 构建完整的 API 请求 URLsnprintf(api_url, sizeof(api_url), "http://v.juhe.cn/tianqi/query?city=%s&key=%s", city, api_key);// 调用函数获取 JSON 字符串char *json_str = get_weather_json_with_popen(api_url);if (json_str == NULL) {fprintf(stderr, "获取天气数据失败\n");return 1;}printf("成功获取到 JSON 数据:\n%s\n", json_str);// 在这里可以继续使用 cJSON 解析 json_str ...// ...// 释放动态分配的内存free(json_str);return 0;
}
拓展:Base64 编码 + 百度 AI 物体识别 API 实战
Base64 编码核心规则
Base64 是一种基于 64 个可打印字符的编码方式,用于将二进制数据转换为文本格式(便于 HTTP 传输):
- 编码表:A-Z(0-25)、a-z(26-51)、0-9(52-61)、+(62)、/(63)。
- 编码原理:3 个字节(24 位)拆分为 4 个 6 位组,每个 6 位组对应编码表中的一个字符。
- 不足 3 字节处理:缺 1 字节补 2 个
=,缺 2 字节补 1 个=。
图片转 Base64 编码(C 语言实现)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Base64编码表
const char base64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";// 将图片文件转换为Base64字符串
char *image_to_base64(const char *img_path) {// 读取图片二进制数据FILE *fp = fopen(img_path, "rb");if (!fp) return NULL;fseek(fp, 0, SEEK_END);long img_size = ftell(fp);fseek(fp, 0, SEEK_SET);unsigned char *img_data = malloc(img_size);fread(img_data, 1, img_size, fp);fclose(fp);// 计算Base64字符串长度long base64_len = (img_size + 2) / 3 * 4;char *base64_str = malloc(base64_len + 1);memset(base64_str, 0, base64_len + 1);// 编码核心逻辑for (long i = 0, j = 0; i < img_size; i += 3, j += 4) {// 取3个字节unsigned char byte1 = i < img_size ? img_data[i] : 0;unsigned char byte2 = i+1 < img_size ? img_data[i+1] : 0;unsigned char byte3 = i+2 < img_size ? img_data[i+2] : 0;// 拆分为4个6位组unsigned int triple = (byte1 << 16) | (byte2 << 8) | byte3;base64_str[j] = base64_table[(triple >> 18) & 0x3F];base64_str[j+1] = base64_table[(triple >> 12) & 0x3F];base64_str[j+2] = i+1 < img_size ? base64_table[(triple >> 6) & 0x3F] : '=';base64_str[j+3] = i+2 < img_size ? base64_table[triple & 0x3F] : '=';}free(img_data);return base64_str;
}
调用百度 AI 物体识别 API
- 百度 AI 开放平台申请 “物体识别” API,获取 API Key 和 Secret Key,生成访问令牌(Access Token)。
- 将图片 Base64 编码字符串构造为 JSON 请求体:
{"image": "图片的Base64编码(去掉前缀data:image/jpg;base64,)","image_type": "BASE64"
}
- 用 HTTP POST 请求发送 JSON 数据,接收响应后用 cJSON 解析识别结果(如物体名称、置信度等)。