30、综合项目:基于数组与字符串的 JSON 解析器雏形
好,终于到了最后一章。说实话,每次带项目课讲到这,我都挺感慨的。前面29章我们啃了数组、字符串、指针、结构体……现在,是时候把这些东西串起来了。
这个项目叫「JSON 解析器雏形」。别被名字吓到,我们不做完整的工业级解析器——那个太复杂了。我们要做的,是解析一个简化版 JSON,只支持对象、数组、字符串和数字。嗯,够用就行。
30.1 项目目标与核心思路
先说说我们要干什么。JSON 长什么样?你肯定见过:
{
"name": "Alice",
"age": 25,
"scores": [88, 92, 76],
"info": {
"city": "Beijing",
"job": "Engineer"
}
}
我们的解析器,要能读入这样的字符串,然后把它拆成 C 语言里的数据结构。说白了,就是把文本变成内存里的数组、结构体、嵌套关系。
核心思路其实就四个字:逐字符扫描。遇到 { 就进对象,遇到 [ 就进数组,遇到 " 就读字符串,遇到数字就解析数字。整个过程,全靠数组和字符串操作撑起来。
项目核心依赖:字符数组(存储输入)、字符串函数(strcmp、strcpy、strchr)、指针数组(存储键值对)、二维数组(存储嵌套层级)。
30.2 数据结构设计
我个人的习惯是,写解析器之前先把数据结构定死。数据结构定了,代码怎么写都不会跑偏。
我们定义这么几个东西:
// 值的类型
typedef enum {
JSON_NULL,
JSON_BOOL,
JSON_NUMBER,
JSON_STRING,
JSON_ARRAY,
JSON_OBJECT
} JsonType;
// 值节点
typedef struct {
JsonType type;
union {
int bool_val;
double num_val;
char* str_val;
struct {
struct JsonNode** items;
int count;
} array;
struct {
struct JsonNode** members;
int count;
} object;
} value;
} JsonNode;
// 键值对(用于对象)
typedef struct {
char* key;
JsonNode* value;
} JsonMember;
你看,这里用了联合体 union,就是为了节省内存。一个节点要么是数字,要么是字符串,不会同时是。我在项目中遇到过有人用结构体硬扛,结果内存翻了三倍……没必要。
30.3 解析器核心流程
解析器的骨架,其实就是一个状态机。我画了张图,你一看就明白:
流程不复杂,对吧?关键是每个分支里怎么处理细节。
30.4 关键函数实现
30.4.1 解析字符串
解析字符串,说白了就是找到两个引号之间的内容。但要注意转义字符,比如 \" 和 \\。
char* parse_string(const char** p) {
// 跳过开头的引号
(*p)++;
// 用数组暂存字符
char buf[256];
int idx = 0;
while (**p != '"' && **p != '\0') {
if (**p == '\\') {
(*p)++;
switch (**p) {
case '"': buf[idx++] = '"'; break;
case '\\': buf[idx++] = '\\'; break;
case 'n': buf[idx++] = '\n'; break;
case 't': buf[idx++] = '\t'; break;
default: buf[idx++] = **p; break;
}
} else {
buf[idx++] = **p;
}
(*p)++;
}
buf[idx] = '\0';
(*p)++; // 跳过结尾引号
// 动态分配内存并返回
char* result = malloc(idx + 1);
strcpy(result, buf);
return result;
}
这里有个坑:buf 用了固定大小 256。如果 JSON 里有个超长字符串怎么办?我曾经在解析一个配置文件时遇到过,字符串长度超过 1024,直接缓冲区溢出。后来我改成了动态扩容——但作为教学项目,256 够用了。
30.4.2 解析数字
数字解析相对简单,但要注意负号和小数点。
double parse_number(const char** p) {
double result = 0.0;
int sign = 1;
if (**p == '-') {
sign = -1;
(*p)++;
}
// 整数部分
while (**p >= '0' && **p <= '9') {
result = result * 10 + (**p - '0');
(*p)++;
}
// 小数部分
if (**p == '.') {
(*p)++;
double frac = 0.0;
double div = 10.0;
while (**p >= '0' && **p <= '9') {
frac += (**p - '0') / div;
div *= 10;
(*p)++;
}
result += frac;
}
return sign * result;
}
你想想看,如果输入是 "3.14",这个函数会先解析出 3,然后遇到小数点,再解析出 0.14,加起来就是 3.14。嗯,逻辑很直白。
30.4.3 解析数组
数组解析要用到动态数组。我们先用一个固定大小的数组暂存,满了再扩容。
JsonNode* parse_array(const char** p) {
(*p)++; // 跳过 '['
JsonNode* arr = malloc(sizeof(JsonNode));
arr->type = JSON_ARRAY;
arr->value.array.items = NULL;
arr->value.array.count = 0;
int capacity = 0;
skip_whitespace(p);
if (**p == ']') {
(*p)++;
return arr; // 空数组
}
while (1) {
skip_whitespace(p);
// 扩容
if (arr->value.array.count >= capacity) {
capacity = capacity == 0 ? 4 : capacity * 2;
arr->value.array.items = realloc(arr->value.array.items,
capacity * sizeof(JsonNode*));
}
// 递归解析元素
arr->value.array.items[arr->value.array.count++] = parse_value(p);
skip_whitespace(p);
if (**p == ',') {
(*p)++;
} else if (**p == ']') {
(*p)++;
break;
}
}
return arr;
}
注意这里调用了 parse_value——这是一个总入口函数,它会根据当前字符决定调用哪个子解析器。这就是递归下降解析的精髓。
30.5 完整解析流程演示
假设我们要解析这个 JSON:
{"name": "Bob", "age": 30}
解析过程是这样的:
parse_value遇到{,调用parse_objectparse_object读键"name",调用parse_string- 遇到
:,读值"Bob",再次调用parse_string - 遇到
,,继续读下一个键值对 - 读键
"age",遇到:,读值30,调用parse_number - 遇到
},解析结束
整个过程,数组和字符串操作贯穿始终。你想想看,如果没有数组来暂存字符,没有字符串函数来比较键名,这个解析器根本写不出来。
调试小技巧:在解析过程中,可以打印当前解析到的位置和字符。我习惯在每个解析函数入口加一句 printf("parse: %c\n", **p);,这样一眼就能看出解析到哪里卡住了。
30.6 避坑指南
我曾经踩过的坑:
- 内存泄漏:解析完 JSON 后,一定要递归释放所有节点。我早期写解析器时忘了释放字符串,跑一次内存涨几 KB,跑一晚上服务器直接 OOM。
- 递归深度:JSON 嵌套太深会导致栈溢出。C 语言的函数调用栈一般只有 1-8 MB。如果 JSON 有 10000 层嵌套,你的程序会直接崩掉。解决方案是用迭代代替递归,或者限制最大深度。
- 字符串边界:解析字符串时,一定要检查
\0终止符。如果 JSON 字符串不完整(比如缺少结尾引号),你的解析器可能会读到非法内存。
30.7 项目扩展思路
这个解析器虽然叫「雏形」,但骨架已经搭好了。如果你想继续完善,可以从这几个方向入手:
| 扩展方向 | 实现思路 | 涉及知识点 |
|---|---|---|
| 支持布尔值和 null | 遇到 true/false/null 时直接匹配字符串 | strncmp、字符串匹配 |
| 支持 Unicode 转义 | 解析 \uXXXX 为 UTF-8 编码 | 位运算、字符编码 |
| 错误报告 | 记录解析位置和错误类型,返回给调用者 | 结构体、枚举、错误码 |
| 序列化(输出 JSON) | 遍历节点树,递归输出字符串 | 递归、字符串拼接 |
我个人建议,先把这个雏形跑通,再考虑扩展。代码能跑起来,比什么都重要。
30.8 小结
这个项目,说白了就是把我们前面学的所有数组和字符串知识,揉在一起用了一次。你可能会觉得代码量有点大,但拆开看,每个函数都不复杂。
嗯,到这里,C 语言数组与字符串处理的内容就全部结束了。从最基础的数组声明,到指针、字符串函数,再到最后的 JSON 解析器——这条路我走了很多年,今天把它完整地讲给你听。
希望你在未来的项目中,能用好这些工具。遇到字符串处理的问题,别慌,回到数组和指针的本质上去想,答案往往就在那里。
公众号:蓝海资料掘金营,微信 deep3321