编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

    • README
    • 入门教程

    • 综合案例

      • README
      • 学生管理通讯录系统
      • 银行账户管理系统
      • 校园身份预约系统
      • Json与内存数据库
        • 📚 渐进学习节奏
        • 00.案例元信息
          • 项目结构
          • 一条命令编译运行
        • 目录快速导航
        • 01.项目需求和功能
          • 1.1 业务背景
          • 1.2 为什么用 variant
          • 1.3 涉及知识点
        • 02.异常体系设计
          • 灵魂三问:异常类要不要现在就写?
          • Step 1.1 异常基类骨架
          • Step 1.2 子异常多态捕获
        • 03.JsonNode 类型表达
          • Step 2.1 variant 与枚举
          • Step 2.2 六种构造函数
          • Step 2.3 类型查询接口
          • Step 2.4 裸 get 访问器
          • Step 2.5 升级友好访问器
          • Step 2.6 容器与工厂函数
        • 04.递归下降解析器
          • 灵魂三问:为什么手写解析器?
          • Step 3.1 游标工具实现
          • Step 3.2 字面量解析
          • Step 3.3 解析数字
          • Step 4.1 字符串无转义
          • Step 4.2 字符串转义补全
          • Step 4.3 数组与对象递归
        • 05.序列化器(缩进美化输出)
          • 灵魂三问:Writer 设计前先想清楚
          • Step 5.1 紧凑序列化模式
          • Step 5.2 Array 缩进美化
        • 06.KvDatabase 内存数据库
          • Step 6.1 空骨架与加载
          • Step 6.2 get/set 单层
          • Step 6.3 多级路径与 del
          • 🚨 故障演示:dirty 标记的"半成品"陷阱
          • 🧪 真的跑一遍看看
          • 🛠 防御方法:先校验路径再修改
        • 07.CLI 工具与 REPL
          • 灵魂三问:CLI 该如何设计?
          • Step 7.1 循环读命令兜底
        • 08.项目总结盘点
        • 09.项目技术思考
          • 9.1 unique_ptr 拷贝问题
          • 9.2 异常 vs 错误码
          • 9.3 string_view 危险
        • 10.衔接与延伸
          • 10.1 与上一案例的差异
          • 10.2 下一案例的递进
          • 10.3 三个延伸挑战
      • 订单票务购买系统
      • 迷你KV存储引擎器
      • 迷你编译器解释器
    • 专栏博客

    • 开发技巧

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Cpp入门到精通
  • 综合案例
杨充
2021-05-25
目录

Json与内存数据库

# 第四章:C++ Json与内存数据库

本章是综合案例的第四关·现代 C++ 升级战——在 03.校园身份预约 (opens new window) 把 STL 三件套吃透之后,我们正式告别裸指针 + 手动 delete + 错误码的 C 风格写法,把 C++17/20 的核心利器全部用上。本案例会做四件升级:

1.std::variant<...>:用类型安全的"和类型"表达 JSON 的 6 种节点(null/bool/number/string/array/object),告别 void* + 类型 tag 的脏写法。

2.std::unique_ptr 全面替代裸指针:JSON 的递归节点用 unique_ptr<JsonNode>,对象/数组的子节点也是——整个解析过程零裸 new、零手写 delete。

3.完整异常体系:从 std::runtime_error 派生 JsonParseError、KeyNotFoundError、TypeMismatchError,精确表达"哪一种错"。

4.std::optional + std::string_view:表达"可能没有结果" + "零拷贝传字符串"——这两个特性是现代 C++ 接口设计的标配。

学习方式:本案例按"阶段拆解 → 写代码 → 跑编译 → 看输出 → 避陷阱"五步法循环。总共 8 个阶段、约 12 小时,建议分 3 天完成(异常+节点骨架 · 解析器三阶段 · KvDB+CLI)。每个阶段都遵循"写一点 → 编译 → 看输出 → 再写下一点"的节奏。


# 📚 渐进学习节奏

💡 先读这段,再开始敲代码!本案例严格按照真实工程师的开发节奏推进,不会一上来甩你 1100 行代码让你抄。我们的节奏是这样的:

阶段 ① 异常底座(02 节) · 30 min · 2 Step
   └ Step 1.1: JsonError 基类骨架 → 编译过 + throw/catch 测试
   └ Step 1.2: 4 个子异常类 → 父类多态捕获验证

阶段 ② JsonNode:variant 容器(03 节) · 120 min · 6 Step
   └ Step 2.1: variant 6 路 + Type 枚举 → 编译过
   └ Step 2.2: 9 个构造函数 → 创建 4 种节点
   └ Step 2.3: isXxx() 类型查询 → 打印类型名
   └ Step 2.4: 裸 std::get → 体验 bad_variant_access
   └ Step 2.5: 升级为 TypeMismatchError 友好异常 ⭐
   └ Step 2.6: operator[] / at("a.b") + 工厂函数

阶段 ③ Parser 字面量与数字(04 节 4.1-4.3) · 60 min · 3 Step
   └ Step 3.1: 游标工具 peek/consume/match
   └ Step 3.2: parseLiteral(null/true/false)
   └ Step 3.3: parseNumber(整数/小数/科学计数法)

阶段 ④ Parser 字符串与递归(04 节 4.4-4.6) · 90 min · 3 Step  【高峰:递归】
   └ Step 4.1: parseString 无转义版 → 解析 "hello"
   └ Step 4.2: 补全 \n \t \" 7 种转义
   └ Step 4.3: parseArray + parseObject 递归 ⭐

阶段 ⑤ Writer 序列化(05 节) · 60 min · 2 Step
   └ Step 5.1: 紧凑模式 + 字符串反向转义
   └ Step 5.2: pretty 缩进美化 + 回环测试

阶段 ⑥ KvDatabase 持久化(06 节) · 90 min · 3 Step
   └ Step 6.1: 空骨架 + load/save + 析构 noexcept
   └ Step 6.2: get / set 单层版 + optional
   └ Step 6.3: 升级 a.b.c 多级路径 + del

阶段 ⑦ CLI REPL(07 节) · 30 min · 1 Step
   └ Step 7.1: 5 命令循环 + 顶层异常兜底 → 重启验证持久化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

🎯 每个 Step 必须做的三件事:

  1. 看 🎯 阶段目标卡片:明确这一阶段做什么、不做什么、验收标准
  2. 写一小段代码就编译运行一次(看到 ✅ 标志立刻动手)
  3. 看到预期输出再写下一个 Step(绝不一口气抄完整段代码)

⚠️ 本案例独有的"现代 C++ 反复横跳":阶段 ② 你刚学会 variant,阶段 ③ 立刻就要用 std::get;阶段 ⑤ 你又要回头改 ② 的访问器配合 Writer。这种"学一点 → 立刻反复用 → 强化记忆"的设计,是新手区分 C 风格 C++ 与现代 C++ 的关键时刻。

✅ 每个阶段的结构(你在正文里会反复看到):

┌─ 🎯 阶段目标 ────────────────┐  ← 阶段开头:明确做什么/不做什么
│  完成什么、不做什么、验收标准    │
└──────────────────────────────┘

  Step X.1:先写最小可编译版(5-20 行)
  Step X.2:编译 → 运行 → 看到输出 ✅
  Step X.3:再加一个小功能(10-30 行)
  Step X.4:编译 → 运行 → 看到新输出 ✅
  ...

┌─ 🧪 运行验证 ─────────────┐  ← 阶段结尾:完整命令 + 预期输出 + 排错
│  编译命令 / 预期输出 / 排错指南 │
└──────────────────────────┘

┌─ 📌 阶段小结 ─────────────┐  ← 阶段结尾:今天学到了什么
│  ✅ 已掌握 / ⏸ 暂未涉及        │
└──────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 00.案例元信息

项目 说明
难度 ★★★★☆
预估时长 12 小时(建议分 3 天)
前置章节 卷一第 8 章(指针引用)、第 11 章(内存模型)、第 12 章(动态内存 RAII)、第 14 章(异常)、第 18 章(现代特性)
覆盖知识点 std::variant / std::optional / std::string_view / std::unique_ptr + make_unique / 移动语义 / 自定义异常类 / 递归下降解析器 / 类型擦除
设计亮点 JSON 解析器(450 行)+ 基于 JSON 的 KV 数据库(300 行)= 自洽的"小而美"工程
最终产物 静态库 libjsonkv.a + 命令行工具 jsonkv,支持加载 JSON 文件并按路径查询
代码规模 约 1100 行 / 8 个文件

# 项目结构

jsonkv/
├── include/
│   ├── JsonNode.h         # 核心:JSON 节点类(基于 variant)
│   ├── JsonParser.h       # 解析器
│   ├── JsonWriter.h       # 序列化器
│   ├── KvDatabase.h       # 内存数据库
│   └── JsonExceptions.h   # 异常体系
├── src/
│   ├── JsonNode.cpp
│   ├── JsonParser.cpp
│   ├── JsonWriter.cpp
│   └── KvDatabase.cpp
├── main.cpp               # CLI 工具
├── tests/                 # 单测(可选)
│   └── test_basic.cpp
└── data/sample.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 一条命令编译运行

cd jsonkv
g++ -std=c++17 -Iinclude src/*.cpp main.cpp -o jsonkv
./jsonkv data/sample.json
1
2
3

# 目录快速导航

点击以下条目即可跳转到对应节。【🔑 重点节】推荐优先阅读。

  • 01.项目需求和功能
    • 1.1 业务背景
    • 1.2 为什么用 variant 【🔑】
    • 1.3 涉及知识点
  • 02.异常体系设计 【阶段①】
    • 灵魂三问:异常类设计
    • Step 1.1 异常基类骨架
    • Step 1.2 子异常多态捕获
  • 03.JsonNode 类型表达 【阶段②·OOP现代版】
    • Step 2.1 variant 与枚举
    • Step 2.2 六种构造函数
    • Step 2.3 类型查询接口
    • Step 2.4 裸 get 访问器
    • Step 2.5 升级友好访问器
    • Step 2.6 容器与工厂函数
  • 04.递归下降解析器 【阶段③④·递归高峰⭐】
    • 灵魂三问:为什么手写解析器
    • Step 3.1 游标工具实现
    • Step 3.2 字面量解析
    • Step 3.3 解析数字
    • Step 4.1 字符串无转义
    • Step 4.2 字符串转义补全
    • Step 4.3 数组与对象递归
  • 05.序列化器 【阶段⑤】
    • 灵魂三问:Writer 设计
    • Step 5.1 紧凑序列化模式
    • Step 5.2 Array 缩进美化
  • 06.KvDatabase 内存数据库 【阶段⑥】
    • Step 6.1 空骨架与加载
    • Step 6.2 get/set 单层
    • Step 6.3 多级路径与 del
  • 07.CLI 工具与 REPL 【阶段⑦】
    • 灵魂三问:CLI 设计
    • Step 7.1 循环读命令兜底
  • 08.项目总结盘点
  • 09.项目技术思考
    • 9.1 unique_ptr 拷贝问题
    • 9.2 异常 vs 错误码
    • 9.3 string_view 危险
  • 10.衔接与延伸
    • 10.1 与上一案例的差异
    • 10.2 下一案例的递进
    • 10.3 三个延伸挑战

# 01.项目需求和功能

# 1.1 业务背景

JSON 是当今互联网最通用的数据格式。无论是 HTTP API、配置文件、还是 NoSQL 数据库,JSON 几乎无处不在。本案例从零实现一个最小但完整的 JSON 解析器,并基于它搭建一个内存 KV 数据库——这是案例 06 KV 存储引擎的"前置版本"。

为什么不直接用 nlohmann/json 这种成熟库?

工业项目当然要用成熟库(卷四会教如何集成)。但本卷的目的是学习 C++ 现代特性——没有什么比"自己手写一个 JSON 解析器"更能让你领悟 variant、unique_ptr、异常的价值。

# 1.2 为什么用 variant

JSON 节点有 6 种可能类型。三种 C++ 实现方式对比:

// ❌ 方案 A:基类 + void*(C 风格)
struct Node { int type; void* data; };

// ❌ 方案 B:union(C++ 弱化版)
union Value { bool b; double n; std::string s; };  // string 不支持,需要 placement new 还无法自动析构

// ✅ 方案 C:std::variant(C++17)
using JsonValue = std::variant<
    std::nullptr_t,                                  // null
    bool,                                            // true/false
    double,                                          // number
    std::string,                                     // string
    std::vector<std::unique_ptr<JsonNode>>,          // array
    std::map<std::string, std::unique_ptr<JsonNode>> // object
>;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

variant 的好处:

  • 类型安全:std::get<bool>(v) 类型不匹配会抛 std::bad_variant_access
  • 析构自动:variant 切换状态时自动调当前持有类型的析构
  • 模式匹配:std::visit(visitor, v) 一次处理所有类型

# 1.3 涉及知识点

卷一章节 知识点 在本案例中的位置
第 11 章 内存模型 栈/堆/全局区分布 / 对象生命周期 全章
第 12 章 动态内存 RAII unique_ptr / make_unique / 移动语义 03、04、06 节
第 14 章 异常处理 自定义异常类 / try/catch / noexcept 02 节
第 18 章 现代特性 variant / optional / string_view / std::visit 03 节
第 16 章 STL vector / map / unordered_map 03、06 节
第 13 章 IO ifstream / 字符串流 04、07 节

# 02.异常体系设计

┌─ 🎯 阶段 ① 目标 ────────────────────────────────────────┐
│ 完成什么:搭起 JsonExceptions.h 异常底座(5 个类)       │
│ 不做什么:不写解析器、不抛真实异常                        │
│ 验收标准:写一段 throw / catch 测试代码,看到异常 what() │
│ 预计耗时:30 min                                         │
└──────────────────────────────────────────────────────────┘
1
2
3
4
5
6

# 灵魂三问:异常类要不要现在就写?

❓ 能不能不写自定义异常类,直接 throw std::runtime_error("xxx") 就完事?

来看反例:

// ❌ 全用 std::runtime_error
void Parser::parseNumber() { throw std::runtime_error("bad number"); }
void Node::asString()      { throw std::runtime_error("not string"); }
void KvDb::load()          { throw std::runtime_error("file open fail"); }

// 调用方怎么区分?
try { /* ... */ }
catch (const std::runtime_error& e) {
    // ⚠️ 三种错全混在一起,e.what() 字符串是唯一线索
    if (std::string(e.what()).find("bad number") != std::string::npos) { /* ... */ }
    // 这种"靠 what() 字符串猜异常种类"的代码 = 业务逻辑灾难
}
1
2
3
4
5
6
7
8
9
10
11
12

问题暴露:

  1. 无法按异常类型分别捕获——都是 runtime_error
  2. 错误信息不结构化——拼接字符串,行号列号难提取
  3. catch 写法脆弱——靠子串匹配判断错误种类,重构 what() 文案就崩

✅ 正确做法:自定义异常类层级——每种"出错原因"对应一个独立类。

❓ 为什么要在阶段 ① 第一个写它? 答:地基性决定时机。Parser/Node/Db 都会 throw,先有异常类型再写业务,可以避免"中途回头补"的反复改头文件。这和 03 案例 §5.2 先写最小 CampusSystem 骨架是同一个思想——先打通最底层的依赖项。

❓ 现在第一步要先做哪一个? 答:先写 JsonError 基类——4 个子异常都从它派生,没有它子类无从下手。

🔑 教学要点:异常类本身没有任何业务逻辑,但它决定了后续每一个模块的错误处理风格。这是为什么很多大型 C++ 项目第一周都在"定异常类型"。


# Step 1.1 异常基类骨架

新建 JsonExceptions.h:

// JsonExceptions.h
#pragma once
#include <stdexcept>
#include <string>

namespace jsonkv {

// class JsonError : public std::runtime_error {
// public:
//     // 手动写每个构造函数。即使基类有多个构造函数,你也需要在派生类中手动重写每个构造函数。
//     JsonError() : std::runtime_error("") {}
//     JsonError(const std::string& msg) : std::runtime_error(msg) {}
//     JsonError(const char* msg) : std::runtime_error(msg) {}
//     // ... 还有其他重载吗?需要去查 std::runtime_error 的文档
// };

// 顶层基类:所有 jsonkv 自定义异常的根
// 定义了一个自定义异常类 JsonError,它继承自标准库的 std::runtime_error。
class JsonError : public std::runtime_error {
public:
    // ⭐ using 继承构造函数的作用,C++11 引入了继承构造函数特性,允许派生类直接"继承"基类的所有构造函数。
    using std::runtime_error::runtime_error;
};

}  // namespace jsonkv
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

📚 两个关键点:

  1. 从 std::runtime_error 派生——它已经实现了 what() 返回字符串。如果直接派生 std::exception,要自己管 const char* 缓冲区。
  2. using std::runtime_error::runtime_error; 是 C++11 "继承构造函数",等价于"复制基类所有构造函数到当前类"。这一行让你能写 JsonError("hello")。

新建临时测试文件 test_exception.cpp 验证基类能用:

#include "JsonExceptions.h"
#include <iostream>

int main() {
    try {
        throw jsonkv::JsonError("base class works!");
    } catch (const std::exception& e) {     // 用基类捕获也行
        std::cout << "caught: " << e.what() << "\n";
    }
}
1
2
3
4
5
6
7
8
9
10
🧪 编译运行:
$ g++ -std=c++17 test_exception.cpp -o test_exc &amp;&amp; ./test_exc
caught: base class works!
1
2
3

✅ 看到 "caught: base class works!" 就说明基类骨架OK。如果编译报 runtime_error 不是构造函数 —— 检查是否 #include <stdexcept>。


# Step 1.2 子异常多态捕获

继续在 JsonExceptions.h 基类下方添加 4 个子类:

namespace jsonkv {

class JsonParseError: public JsonError {
public:
    JsonParseError(const std::string& msg, size_t line, size_t col)
        : JsonError("[Parse] line " + std::to_string(line) +
                    " col " + std::to_string(col) + ": " + msg) {}
};

// 类型不匹配(如对 number 调 asString())
class TypeMismatchError : public JsonError {
public:
    TypeMismatchError(const std::string& expected, const std::string& actual)
        : JsonError("[Type] expected " + expected + ", got " + actual) {}
};

// Key 不存在
class KeyNotFoundError : public JsonError {
public:
    explicit KeyNotFoundError(const std::string& key)
        : JsonError("[Key] not found: " + key) {}
};

// IO 错误(文件读写)
class JsonIoError : public JsonError {
public:
    using JsonError::JsonError;
};

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

升级 test_exception.cpp,分别 throw 4 种异常:

#include "JsonExceptions.h"
#include <iostream>

using namespace jsonkv;

void demo(int which) {
    switch (which) {
        case 1: throw jsonkv::JsonParseError("bad token", 3, 12);
        case 2: throw jsonkv::TypeMismatchError("string", "number");
        case 3: throw jsonkv::KeyNotFoundError("user.name");
        case 4: throw jsonkv::JsonIoError("cannot open kv.json");
        default: throw jsonkv::JsonError("base class works!");
    }
}

int main() {
    for (int i = 1; i <= 5; ++i) {
        try { demo(i); }
        catch (const JsonError& e) {                     // ⭐ 父类一次接住
            std::cout << "[" << i << "] " << e.what() << "\n";
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
🧪 编译运行:
$ g++ -std=c++17 test_exception.cpp -o test_exc &amp;&amp; ./test_exc
[1] [Parse] line 3 col 12: bad token
[2] [Type] expected string, got number
[3] [Key] not found: user.name
[4] cannot open kv.json
[5] base class works!
1
2
3
4
5
6
7

✅ 4 行各有前缀([Parse]/[Type]/[Key]/无),证明:4 个子异常都能被 JsonError 父类一并捕获——这就是异常多态。

┌─ 📌 阶段 ① 小结 ────────────────────────────────────────┐
│ ✅ 已完成                                                 │
│   • JsonError 基类(继承 runtime_error)                  │
│   • 4 个子异常类(Parse/Type/Key/Io)                     │
│   • 用父类捕获测试通过                                     │
│ 🔜 下一阶段                                                │
│   • 进入阶段 ②:写 JsonNode 类(variant 容器)              │
│ 💡 commit 建议:feat(jsonkv): add exception hierarchy     │
└──────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9

💼 试调结束后清理:test_exception.cpp 是临时验证文件,可以删除——但建议保留它作为单元测试的雏形(卷四会教 GoogleTest 框架)。


# 03.JsonNode 类型表达

┌─ 🎯 阶段 ② 目标 ────────────────────────────────────────┐
│ 完成什么:JsonNode 数据结构 + 类型查询 + 类型安全访问器     │
│ 不做什么:不写 Parser/Writer/Database,只玩单一节点         │
│ 验收标准:能在 main 里手工创建 6 种类型节点 + 触发类型异常   │
│ 预计耗时:120 min(拆 6 个 Step,每步都能编译运行)         │
└──────────────────────────────────────────────────────────┘
1
2
3
4
5
6

本阶段的灵魂三问💡

  1. JSON 节点有 6 种可能类型,C++ 怎么用一个类同时表达?→ std::variant
  2. 6 种类型混在一起,怎么知道当前是哪种?→ v.index() 或 std::holds_alternative<T>(v)
  3. 类型不匹配怎么报错?→ 上一阶段做好的 TypeMismatchError

# Step 2.1 variant 与枚举

新建 JsonNode.h——只放数据结构,不写方法:

// JsonNode.h(v1:仅类型骨架)
#pragma once
#include <variant>
#include <vector>
#include <map>
#include <string>
#include <memory>
#include "JsonExceptions.h"

namespace jsonkv {

// 前置声明
class JsonNode;
// 别名:节点智能指针
using JsonNodePtr = std::unique_ptr<JsonNode>;

class JsonNode {
public:
    // JSON 数组
    using Array = std::vector<JsonNodePtr>;
    // JSON 对象
    using Object = std::map<std::string, JsonNodePtr>;
    // ⭐ 核心:6 路 variant。6 种 JSON 类型
    // 使用 std::variant 替代传统 union 或继承体系
    using Value  = std::variant<
        std::nullptr_t,    // 0: null
        bool,              // 1: true/false
        double,            // 2: number
        std::string,       // 3: string
        Array,             // 4: array
        Object             // 5: object
    >;
    // 用 enum class 让"类型枚举"和 variant 索引一一对应
    // variant 索引:       0       1     2       3       4       5
    enum class Type { Null = 0, Bool, Number, String, Array, Object };
private:
    Value v;                // 持有 6 种之一。实际存储的值
};

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

写一个 1 行 main 验证骨架可编译:

// test_node.cpp
#include "JsonNode.h"
int main() {
    jsonkv::JsonNode n;     // 默认构造(编译器会自动 default-init variant 为第一种 = nullptr_t)
    return 0;
}
1
2
3
4
5
6
🧪 编译:
$ g++ -std=c++17 test_node.cpp -o test_node
$ echo $?
0
1
2
3
4

✅ 退出码 0 就是成功。注意:variant 默认初始化为第一个候选类型(即 nullptr_t = null),这就是为什么我们把 nullptr_t 放在 variant 第一位。


# Step 2.2 六种构造函数

继续在 JsonNode public 区域追加构造函数:

public:
    // ====== 构造函数:每种 JSON 类型一个 ======
    JsonNode() : v(nullptr) {}                                   // null
    explicit JsonNode(bool b)                : v(b) {}
    explicit JsonNode(double n)              : v(n) {}
    explicit JsonNode(int n)                 : v(static_cast<double>(n)) {}   // int 转 double
    explicit JsonNode(const std::string& s)  : v(s) {}
    explicit JsonNode(std::string&& s)       : v(std::move(s)) {}             // 移动版
    explicit JsonNode(const char* s)         : v(std::string(s)) {}           // 字面量便利
    explicit JsonNode(Array&& a)             : v(std::move(a)) {}
    explicit JsonNode(Object&& o)            : v(std::move(o)) {}
1
2
3
4
5
6
7
8
9
10
11

📚 explicit 的意义:禁止隐式转换。例如不写 explicit 时,JsonNode n = 3.14; 会偷偷把 double 转成 JsonNode;加上 explicit 强制写 JsonNode n{3.14} 或 JsonNode n(3.14),避免误用。

升级 test_node.cpp:手工创建 4 种基础类型节点:

#include "JsonNode.h"
#include <iostream>

int main() {
    using namespace jsonkv;

    JsonNode n_null;
    JsonNode n_bool(true);
    JsonNode n_num(3.14);
    JsonNode n_str(std::string("hello"));

    std::cout << "create 4 nodes OK\n";
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
🧪 编译运行:
$ g++ -std=c++17 test_node.cpp -o test_node &amp;&amp; ./test_node
create 4 nodes OK
1
2
3

⚠️ 如果你写 JsonNode n_str("hello"); 报 ambiguous,是因为 "hello" 既能匹配 const char* 也能匹配 const std::string&——这是 explicit 没拦住的边缘情况,加 std::string("hello") 显式声明即可。


# Step 2.3 类型查询接口

继续追加:

    // ====== 类型查询 ======
    Type type() const { return static_cast<Type>(v.index()); }   // ⭐ variant 的 index() 返回当前持有的索引
    bool isNull()   const { return type() == Type::Null; }
    bool isBool()   const { return type() == Type::Bool; }
    bool isNumber() const { return type() == Type::Number; }
    bool isString() const { return type() == Type::String; }
    bool isArray()  const { return type() == Type::Array; }
    bool isObject() const { return type() == Type::Object; }
1
2
3
4
5
6
7
8

升级测试——打印每个节点的真实类型:

#include "JsonNode.h"
#include <iostream>

const char* typeStr(jsonkv::JsonNode::Type t) {
    switch (t) {
        case jsonkv::JsonNode::Type::Null:   return "null";
        case jsonkv::JsonNode::Type::Bool:   return "bool";
        case jsonkv::JsonNode::Type::Number: return "number";
        case jsonkv::JsonNode::Type::String: return "string";
        case jsonkv::JsonNode::Type::Array:  return "array";
        case jsonkv::JsonNode::Type::Object: return "object";
    }
    return "?";
}

int main() {
    using namespace jsonkv;
    JsonNode nodes[] = { JsonNode{}, JsonNode{true}, JsonNode{3.14}, JsonNode{std::string("hi")} };

    for (auto& n : nodes) {
        std::cout << typeStr(n.type()) << "\n";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
🧪 预期输出:
null
bool
number
string
1
2
3
4
5

✅ 4 种类型查询全部正确。v.index() 是 variant 提供的"我现在装的是第几种"接口,强转成 Type 枚举就能拿到友好名字。


# Step 2.4 裸 get 访问器

在 JsonNode.h 类内声明:

    // ====== 访问器(先裸版,下一步再升级)======
    bool                asBool()   const;
    double              asNumber() const;
    const std::string&  asString() const;
1
2
3
4

新建 JsonNode.cpp——先写最朴素的访问器,故意不做类型检查:

// JsonNode.cpp(v1:裸版)
#include "JsonNode.h"

namespace jsonkv {

bool JsonNode::asBool() const {
    return std::get<bool>(v);                  // ⚠️ 类型错时抛 std::bad_variant_access
}
double JsonNode::asNumber() const {
    return std::get<double>(v);
}
const std::string& JsonNode::asString() const {
    return std::get<std::string>(v);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

升级 test_node.cpp:故意对 bool 节点调 asNumber():

JsonNode b(true);
std::cout << b.asBool() << "\n";       // OK:1
try {
    double x = b.asNumber();           // ⚠️ 类型不对
    std::cout << x << "\n";
} catch (const std::exception& e) {
    std::cout << "caught: " << e.what() << "\n";
}
1
2
3
4
5
6
7
8
🧪 编译运行:
$ g++ -std=c++17 test_node.cpp JsonNode.cpp -o test_node &amp;&amp; ./test_node
1
caught: std::bad_variant_access
1
2
3
4

⚠️ 看到 std::bad_variant_access —— 这是 STL 的原生异常,对用户极不友好:你只知道"类型错了",不知道"期望什么实际是什么"。下一步我们把它包装成更人性化的 TypeMismatchError。


# Step 2.5 升级友好访问器

在 JsonNode.h 中声明 Array/Object 访问器:

    const Array&        asArray()  const;
    const Object&       asObject() const;
    Array&  asArray();           // 可变版本
    Object& asObject();
1
2
3
4

把 JsonNode.cpp 整体替换为带类型检查的版本,并新增数组/对象访问器:

// JsonNode.cpp(v2:友好异常版)
#include "JsonNode.h"

namespace jsonkv {

// 工具:枚举转字符串(用于错误消息)
static std::string typeName(JsonNode::Type t) {
    switch (t) {
        case JsonNode::Type::Null:   return "null";
        case JsonNode::Type::Bool:   return "bool";
        case JsonNode::Type::Number: return "number";
        case JsonNode::Type::String: return "string";
        case JsonNode::Type::Array:  return "array";
        case JsonNode::Type::Object: return "object";
    }
    return "unknown";
}

bool JsonNode::asBool() const {
    if (!isBool()) throw TypeMismatchError("bool", typeName(type()));    // ⭐ 先校验
    return std::get<bool>(v);
}
double JsonNode::asNumber() const {
    if (!isNumber()) throw TypeMismatchError("number", typeName(type()));
    return std::get<double>(v);
}
const std::string& JsonNode::asString() const {
    if (!isString()) throw TypeMismatchError("string", typeName(type()));
    return std::get<std::string>(v);
}

const JsonNode::Array& JsonNode::asArray() const {
    if (!isArray()) throw TypeMismatchError("array", typeName(type()));
    return std::get<Array>(v);
}
const JsonNode::Object& JsonNode::asObject() const {
    if (!isObject()) throw TypeMismatchError("object", typeName(type()));
    return std::get<Object>(v);
}

// 可变版本:复用 const 版本的检查逻辑
JsonNode::Array&  JsonNode::asArray()  {
    return const_cast<Array&>(static_cast<const JsonNode*>(this)->asArray());
}
JsonNode::Object& JsonNode::asObject() {
    return const_cast<Object&>(static_cast<const JsonNode*>(this)->asObject());
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

测试:再次故意触发类型错误:

JsonNode c(true);
try {
    c.asString();
} catch (const TypeMismatchError& e) {
    std::cout << "friendly: " << e.what() << "\n";
}
1
2
3
4
5
6
🧪 预期输出:
friendly: [Type] expected string, got bool
1
2

✅ 现在错误消息一眼明白:期望 string,实际 bool。这就是"原生异常 → 业务异常"的包装价值。

📚 const_cast 复用模式:asArray() 可变版调用了 const 版做检查,再去掉 const 返回——这是 Effective C++ 推荐的"用 const 函数实现 non-const 函数"避免代码重复。


# Step 2.6 容器与工厂函数

最后追加 operator[]、size()、contains()、at() 路径访问,以及工厂函数。

JsonNode.h 类内声明:

    // ====== 容器便捷操作 ======
    JsonNode& operator[](size_t idx);                  // arr[0]
    JsonNode& operator[](const std::string& key);      // obj["name"]
    size_t size() const;
    bool   contains(const std::string& key) const;

    // 路径查询:obj.at("user.address.city")
    JsonNode& at(const std::string& path);
    const JsonNode& at(const std::string& path) const;
};   // class JsonNode 闭合

// ====== 工厂函数(类外)======
inline JsonNodePtr makeNull()                  { return std::make_unique<JsonNode>(); }
inline JsonNodePtr makeBool(bool b)            { return std::make_unique<JsonNode>(b); }
inline JsonNodePtr makeNumber(double n)        { return std::make_unique<JsonNode>(n); }
inline JsonNodePtr makeString(std::string s)   { return std::make_unique<JsonNode>(std::move(s)); }
inline JsonNodePtr makeArray()                 { return std::make_unique<JsonNode>(JsonNode::Array{}); }
inline JsonNodePtr makeObject()                { return std::make_unique<JsonNode>(JsonNode::Object{}); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

JsonNode.cpp 追加实现:

JsonNode& JsonNode::operator[](size_t idx) {
    if (!isArray()) {
        throw TypeMismatchError("array", typeName(type()));
    }
    auto& arr = std::get<Array>(v);
    if (idx >= arr.size()) {
        throw std::out_of_range("array index out of range");
    }
    return *arr[idx];
}

JsonNode& JsonNode::operator[](const std::string& key) {
    if (!isObject()) {
        throw TypeMismatchError("object", typeName(type()));
    }
    auto& obj = std::get<Object>(v);
    auto it = obj.find(key);
    if (it == obj.end()) {
        // 方案A:抛异常
        throw KeyNotFoundError(key);
        // 方案B:自动插入(类似 nlohmann/json 的行为)
        // auto [newIt, _] = obj.emplace(key, makeNull());
        // return *newIt->second;
    }
    return *it->second;
}

size_t JsonNode::size() const {
    if (isArray())  return std::get<Array>(v).size();
    if (isObject()) return std::get<Object>(v).size();
    if (isBool()) return 1;
    if (isString()) return std::get<std::string>(v).size();
    return 0;
}

bool JsonNode::contains(const std::string& key) const {
    return isObject() && std::get<Object>(v).count(key) > 0;
}

// 路径解析:递归查找 a.b.c
JsonNode& JsonNode::at(const std::string& path) {
    if (path.empty() || !isObject()) return *this;
    size_t dot = path.find('.');
    std::string head = (dot == std::string::npos) ? path : path.substr(0, dot);
    std::string rest = (dot == std::string::npos) ? "" : path.substr(dot + 1);

    auto& obj = std::get<Object>(v);
    auto it = obj.find(head);
    if (it == obj.end()) throw KeyNotFoundError(head);
    if (rest.empty()) return *it->second;
    return it->second->at(rest);             // ⭐ 递归
}
const JsonNode& JsonNode::at(const std::string& path) const {
    return const_cast<JsonNode*>(this)->at(path);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

最终验证——手工搭一棵小树:

// test_node.cpp(最终版)
#include "JsonNode.h"
#include <iostream>

int main() {
    using namespace jsonkv;

    // 手搭:{ "user": { "name": "Alice", "age": 30 } }
    auto user = makeObject();
    user->asObject()["name"] = makeString("Alice");
    user->asObject()["age"]  = makeNumber(30);

    auto root = makeObject();
    root->asObject()["user"] = std::move(user);

    // 用 at 路径查询
    std::cout << "name = " << root->at("user.name").asString() << "\n";
    std::cout << "age  = " << root->at("user.age").asNumber() << "\n";

    // 故意查不存在的路径
    try { root->at("user.email"); }
    catch (const KeyNotFoundError& e) { std::cout << "miss: " << e.what() << "\n"; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

编译运行一下:

🧪 预期输出:
$ g++ -std=c++17 test_node.cpp JsonNode.cpp -o test_node &amp;&amp; ./test_node
name = Alice
age  = 30
miss: [Key] not found: email
1
2
3
4
5

✅ 一棵两层 JSON 树用 at("a.b") 路径查询全跑通,类型错和 Key 不存在都有友好异常。

┌─ 📌 阶段 ② 小结 ────────────────────────────────────────┐
│ ✅ 已掌握                                                 │
│   • Step 2.1 variant 6 路类型 + Type 枚举对应             │
│   • Step 2.2 9 个构造函数(一种 JSON 类型一个)            │
│   • Step 2.3 v.index() + isXxx() 类型查询                  │
│   • Step 2.4 裸 std::get → bad_variant_access 体验          │
│   • Step 2.5 包装为 TypeMismatchError 友好异常 ⭐           │
│   • Step 2.6 operator[]/size/contains/at + 工厂函数        │
│ 🔜 下一阶段 ③:JsonParser 递归下降(要把字符串解析成 Node)│
│ 💡 commit 建议:feat(jsonkv): JsonNode with variant types │
└──────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11

💼 make_unique 而不是 new —— 工厂函数全部用 std::make_unique<JsonNode>(...),杜绝裸 new。这是 C++14 起的最佳实践:异常安全 + 一行更清晰 + 编译器优化空间更大。


# 04.递归下降解析器

┌─ 🎯 阶段 ③ 目标 ────────────────────────────────────────┐
│ 完成什么:JsonParser 骨架 + 字面量(null/true/false) + 数字 │
│ 不做什么:不解析字符串、数组、对象(下个阶段)              │
│ 验收标准:parser.parse("true") 能得到一个 bool=true 的节点 │
│ 预计耗时:60 min · 3 Step                                 │
└──────────────────────────────────────────────────────────┘
1
2
3
4
5
6

💡 递归下降是什么? 每种语法结构对应一个 parseXxx() 函数,遇到嵌套就递归调用。它是最容易手写的解析器架构,你将在阶段 ④ 看到 parseArray 调 parseValue、parseValue 又调 parseArray 这样自然的递归循环。


# 灵魂三问:为什么手写解析器?

❓ 为什么不用正则表达式?

// ❌ 想用正则匹配 JSON
std::regex jsonRe(R"(\{.*\}|\[.*\]|".*"|...)");   // 完全写不出来
1
2

根本问题:JSON 是上下文无关文法,可以无限嵌套——[[[[...]]]]。正则表达式(正则文法)连"括号配对"都做不到——这是计算理论的"乔姆斯基层级"硬伤。所以 JSON / XML / 编程语言的语法分析永远不能只靠正则。

❓为什么不用 yacc/bison/ANTLR 这些"解析器生成器"?

维度 工具生成 手写递归下降
学习曲线 要学专门的语法(如 expr ::= NUM '+' expr;) 一个 cpp 写完
错误消息 自动生成的,难以定制 想说什么说什么(行/列/上下文)
调试 生成的代码无法 step into 自己代码可以一行一行调
性能 通用算法(LALR)慢一些 业务定制可极致优化

结论:JSON 这种简单文法 + 教学场景,手写递归下降是最佳选择。工业级语言(如 Rust 的 syn / Go 的 go/parser)也是手写的。

❓ 递归下降的"递归"在哪?

  • parseValue 看到 [ → 调 parseArray
  • parseArray 在循环里 → 又调 parseValue(解析每个元素)
  • 元素如果是 [...] → 再调 parseArray
  • 这就是互递归——本节阶段 ④ 你会亲眼看到这个循环

🔑 教学要点:递归下降的精髓是 "语法规则一条 = parseXxx 函数一个"。RFC 8259 JSON 文法只有 6 条规则——所以解析器恰好 6 个 parseXxx 函数。写起来就像翻译文档。


# Step 3.1 游标工具实现

先打地基:解析器需要"扫描器"——能往前看一个字符、消费当前字符、按需匹配特定字符。

新建 JsonParser.h:

// JsonParser.h(v1:只放工具方法和成员)
#pragma once
#include "JsonNode.h"
#include <string>
#include <string_view>

namespace jsonkv {

class JsonParser {
private:
    std::string_view src;   // 待解析的 JSON 源字符串(轻量引用,不拷贝)
    size_t pos = 0;         // 当前解析到的位置(索引)
    size_t line = 1, col = 1; // 当前行号和列号(用于报错时定位)

    // ===== 游标工具 =====
    char   peek() const;         // 看当前字符(不前进)
    char   consume();            // 吃掉当前字符并前进
    bool   match(char c);        // 当前字符如果是 c 就吃掉
    void   skipWhitespace();
    [[noreturn]] void error(const std::string& msg);
public:
    explicit JsonParser(std::string_view s) : src(s) {}
};

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

思考一下:为什么这里表示索引,行号等用size_t而不是int?因为 size_t 是"不会为负数"的整数,而 int 可能会变成负数。

size_t 是 C++ 标准库中用来表示大小和索引的无符号类型。索引、行号、列号天然是非负的,用 size_t 更符合语义。

新建 JsonParser.cpp:

// JsonParser.cpp(v1)
#include "JsonParser.h"
#include <cctype>

namespace jsonkv {

char JsonParser::peek() const {
    return pos < src.size() ? src[pos] : '\0';   // 越界返回 \0
}

char JsonParser::consume() {
    char c = src[pos++];
    if (c == '\n') { line++; col = 1; }          // 跨行就更新行号
    else { col++; }
    return c;
}

bool JsonParser::match(char c) {
    if (peek() == c) { consume(); return true; }
    return false;
}

void JsonParser::skipWhitespace() {
    while (pos < src.size() &&
           std::isspace(static_cast<unsigned char>(src[pos]))) {
        consume();
    }
}

void JsonParser::error(const std::string& msg) {
    throw JsonParseError(msg, line, col);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

写一个 main 玩转游标——但因为这些是私有方法,临时把它们改成 public: 测试,或者写一个友元测试函数。最简单的做法是在类里加一个公开的"调试钩子":

// JsonParser.h public 临时加:
public:
    // 调试用:打印源串信息
    void debugDump() const {
        std::cout << "src len=" << src.size()
                  << " pos="    << pos
                  << " line="   << line
                  << " col="    << col << "\n";
    }
1
2
3
4
5
6
7
8
9

然后在做一下测试:

// test_parser.cpp
#include "JsonParser.h"
#include <iostream>
int main() {
    jsonkv::JsonParser p("hello");
    p.debugDump();
}
1
2
3
4
5
6
7
🧪 编译运行:
$ g++ -std=c++17 test_parser.cpp JsonParser.cpp JsonNode.cpp -o test_parser &amp;&amp; ./test_parser
src len=5 pos=0 line=1 col=1
1
2
3

✅ 看到游标信息正确。下一步把 debugDump 删掉,进入正式解析逻辑。

📚 std::string_view:是"指向字符串的视图"——内部就是一个 const char* + 长度,不拥有数据。这就是它"零拷贝"的来源——传 1 GB 的 JSON 也只复制 16 字节的视图。⚠️ 注意它的危险:原字符串先析构 view 就成野指针,这就是 9.3 节 要专门讨论的话题。


# Step 3.2 字面量解析

给 JsonParser 加 parse() + parseValue() + parseLiteral() 三个方法。

JsonParser.h 类内追加:

private:
    JsonNodePtr parseValue();
    JsonNodePtr parseLiteral();      // null / true / false

public:
    JsonNodePtr parse();             // 入口
1
2
3
4
5
6

JsonParser.cpp 追加:

JsonNodePtr JsonParser::parse() {
    skipWhitespace();
    auto node = parseValue();
    skipWhitespace();
    if (pos < src.size()) error("trailing characters after value");
    return node;
}

JsonNodePtr JsonParser::parseValue() {
    skipWhitespace();
    char c = peek();
    if (c == 'n' || c == 't' || c == 'f') return parseLiteral();
    error(std::string("unexpected char: ") + c);
}

JsonNodePtr JsonParser::parseLiteral() {
    if (src.compare(pos, 4, "null") == 0)  { pos += 4; col += 4; return makeNull(); }
    if (src.compare(pos, 4, "true") == 0)  { pos += 4; col += 4; return makeBool(true); }
    if (src.compare(pos, 5, "false") == 0) { pos += 5; col += 5; return makeBool(false); }
    error("invalid literal");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

升级 test_parser.cpp——分别解析 3 个字面量:

#include "JsonParser.h"
#include <iostream>

int main() {
    using namespace jsonkv;
    for (const char* input : {"null", "true", "false"}) {
        JsonParser p(input);
        auto node = p.parse();
        std::cout << input << " => isNull=" << node->isNull()
                  << " isBool=" << node->isBool();
        if (node->isBool()) std::cout << " val=" << node->asBool();
        std::cout << "\n";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

🧪 预期输出:

null  => isNull=1 isBool=0
true  => isNull=0 isBool=1 val=1
false => isNull=0 isBool=1 val=0
1
2
3

✅ 三个字面量都正确解析。故意输入错的试试:

JsonParser p("nul");        // 缺一个 l
try { p.parse(); }
catch (const JsonParseError& e) { std::cout << e.what() << "\n"; }
1
2
3

🧪 预期输出:

[Parse] line 1 col 1: invalid literal
1

✅ 错误带行列号——这就是阶段 ① 异常底座的回报。


# Step 3.3 解析数字

继续追加:

JsonParser.h 类内:

    JsonNodePtr parseNumber();
1

parseValue() 末尾追加分支:

JsonNodePtr JsonParser::parseValue() {
    skipWhitespace();
    char c = peek();
    if (c == 'n' || c == 't' || c == 'f') return parseLiteral();
    if (c == '-' || std::isdigit(static_cast<unsigned char>(c))) return parseNumber();   // ⭐ 新增
    error(std::string("unexpected char: ") + c);
}
1
2
3
4
5
6
7

JsonParser.cpp 追加:

JsonNodePtr JsonParser::parseNumber() {
    size_t start = pos;
    if (match('-')) {}                                                         // 可能有负号
    while (std::isdigit(static_cast<unsigned char>(peek()))) consume();        // 整数部分
    if (peek() == '.') {                                                       // 小数部分
        consume();
        while (std::isdigit(static_cast<unsigned char>(peek()))) consume();
    }
    if (peek() == 'e' || peek() == 'E') {                                      // 科学计数法
        consume();
        if (peek() == '+' || peek() == '-') consume();
        while (std::isdigit(static_cast<unsigned char>(peek()))) consume();
    }
    try {
        double v = std::stod(std::string(src.substr(start, pos - start)));
        return makeNumber(v);
    } catch (...) {
        error("invalid number");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

测试 4 种数字:

for (const char* s : {"42", "-3.14", "1e3", "-2.5e-2"}) {
    JsonParser p(s);
    std::cout << s << " => " << p.parse()->asNumber() << "\n";
}
1
2
3
4
🧪 预期输出:
42       => 42
-3.14    => -3.14
1e3      => 1000
-2.5e-2  => -0.025
1
2
3
4
5

✅ 整数、负小数、正指数、负指数全部正确。注意我们不用 atoi(只能识别整数),用 std::stod(支持完整 IEEE 754 浮点格式)。

┌─ 📌 阶段 ③ 小结 ────────────────────────────────────────┐
│ ✅ 已掌握                                                 │
│   • Step 3.1 游标工具 peek/consume/match + 行列追踪       │
│   • Step 3.2 字面量 null/true/false 三选一                 │
│   • Step 3.3 数字(整数/小数/科学计数法)                   │
│ 🔜 下一阶段 ④:字符串转义 + 数组对象递归                   │
│ 💡 commit 建议:feat(jsonkv): parser literals & numbers   │
└──────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8

# Step 4.1 字符串无转义

┌─ 🎯 阶段 ④ 目标 ────────────────────────────────────────┐
│ 完成什么:parseString + parseArray + parseObject         │
│ 不做什么:不做 \uXXXX Unicode 转义(留作挑战)             │
│ 验收标准:能解析 [{"name":"Alice","age":30}] 这种嵌套结构  │
│ 预计耗时:90 min · 3 Step                                 │
└──────────────────────────────────────────────────────────┘
1
2
3
4
5
6

先写最朴素的版本——只识别 "hello" 这种无转义字符串:

JsonParser.h 类内:

    JsonNodePtr parseString();
1

parseValue() 追加:

    if (c == '"') return parseString();          // 在数字分支之前加
1

JsonParser.cpp 追加(v1:暂不处理转义):

JsonNodePtr JsonParser::parseString() {
    if (!match('"')) error("expected '\"'");
    std::string result;
    while (pos < src.size() && peek() != '"') {
        result += consume();
    }
    if (!match('"')) error("unterminated string");
    return makeString(std::move(result));
}
1
2
3
4
5
6
7
8
9

测试:

for (const char* s : {"\"hello\"", "\"\"", "\"abc 123\""}) {
    JsonParser p(s);
    std::cout << "[" << p.parse()->asString() << "]\n";
}
1
2
3
4
🧪 预期输出:
[hello]
[]
[abc 123]
1
2
3
4

✅ 三个普通字符串解析正确,包括空串。但遇到转义字符就翻车——下一步处理。


# Step 4.2 字符串转义补全

把 parseString 整体替换为带转义版本:

JsonNodePtr JsonParser::parseString() {
    if (!match('"')) error("expected '\"'");
    std::string result;
    while (pos < src.size() && peek() != '"') {
        char c = consume();
        if (c == '\\') {                                  // ⭐ 转义起始
            char esc = consume();
            switch (esc) {
                case '"':  result += '"';  break;
                case '\\': result += '\\'; break;
                case '/':  result += '/';  break;
                case 'n':  result += '\n'; break;
                case 't':  result += '\t'; break;
                case 'r':  result += '\r'; break;
                case 'b':  result += '\b'; break;
                case 'f':  result += '\f'; break;
                // \uXXXX Unicode 转义留作挑战 B
                default:   error(std::string("bad escape \\") + esc);
            }
        } else {
            result += c;
        }
    }
    if (!match('"')) error("unterminated string");
    return makeString(std::move(result));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

测试:

JsonParser p(R"("a\nb\t\"c\"")");                        // 原始字符串字面量更直观
std::string s = p.parse()->asString();
std::cout << "len=" << s.size() << "\n";
std::cout << s << "\n";
1
2
3
4
🧪 预期输出:
len=7
a
b	"c"
1
2
3
4

✅ 7 个字符(a, \n, b, \t, ", c, "),换行和制表符都正确转换。

📚 C++ 原始字符串 R"(...)":内层不需要再二次转义。R"("a\nb")" 表示字面量 "a\nb"(共 5 个字符)。教学时强烈推荐——避免被反斜杠绕晕。


# Step 4.3 数组与对象递归

这是整个解析器的高潮——递归下降的本质就是 parseValue → parseArray → parseValue → parseObject → ... 这样的相互调用。

JsonParser.h 类内:

    JsonNodePtr parseArray();
    JsonNodePtr parseObject();
1
2

parseValue() 补全为最终版:

JsonNodePtr JsonParser::parseValue() {
    skipWhitespace();
    char c = peek();
    if (c == 'n' || c == 't' || c == 'f') return parseLiteral();
    if (c == '"') return parseString();
    if (c == '[') return parseArray();                                                 // ⭐ 新增
    if (c == '{') return parseObject();                                                // ⭐ 新增
    if (c == '-' || std::isdigit(static_cast<unsigned char>(c))) return parseNumber();
    error(std::string("unexpected char: ") + c);
}
1
2
3
4
5
6
7
8
9
10

JsonParser.cpp 追加:

JsonNodePtr JsonParser::parseArray() {
    if (!match('[')) error("expected '['");
    auto arr = JsonNode::Array{};
    skipWhitespace();
    if (match(']')) return std::make_unique<JsonNode>(std::move(arr));     // 空数组

    while (true) {
        arr.push_back(parseValue());                                       // ⭐ 递归
        skipWhitespace();
        if (match(']')) break;
        if (!match(',')) error("expected ',' or ']' in array");
    }
    return std::make_unique<JsonNode>(std::move(arr));
}

JsonNodePtr JsonParser::parseObject() {
    if (!match('{')) error("expected '{'");
    auto obj = JsonNode::Object{};
    skipWhitespace();
    if (match('}')) return std::make_unique<JsonNode>(std::move(obj));     // 空对象

    while (true) {
        skipWhitespace();
        auto keyNode = parseString();                                       // key 必须是字符串
        std::string key = keyNode->asString();
        skipWhitespace();
        if (!match(':')) error("expected ':'");
        auto value = parseValue();                                          // ⭐ 递归
        obj[key] = std::move(value);
        skipWhitespace();
        if (match('}')) break;
        if (!match(',')) error("expected ',' or '}' in object");
    }
    return std::make_unique<JsonNode>(std::move(obj));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

最终一战——解析一个嵌套 JSON:

const char* json = R"([
  {"name":"Alice","age":30,"vip":true},
  {"name":"Bob","age":25,"vip":false}
])";

JsonParser p(json);
auto root = p.parse();

std::cout << "size = " << root->size() << "\n";                      // 2
for (size_t i = 0; i < root->size(); ++i) {
    auto& item = (*root)[i];
    std::cout << item.at("name").asString()
              << " / " << item.at("age").asNumber()
              << " / vip=" << item.at("vip").asBool() << "\n";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
🧪 预期输出:
size = 2
Alice / 30 / vip=1
Bob / 25 / vip=0
1
2
3
4

✅ 一个 200 行的解析器就能搞定嵌套数组+对象+布尔+数字+字符串 5 种类型混合。

┌─ 📌 阶段 ④ 小结 ────────────────────────────────────────┐
│ ✅ 已掌握                                                 │
│   • Step 4.1 字符串无转义版本                              │
│   • Step 4.2 7 种 \X 转义字符                              │
│   • Step 4.3 数组+对象递归 ⭐ —— 整章高潮                  │
│ 🔜 下一阶段 ⑤:JsonWriter 反向把节点序列化成字符串          │
│ 💡 commit 建议:feat(jsonkv): full json parser            │
└──────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8

📚 回头看:解析器只有约 200 行,能完整解析 RFC 8259 的 95% 语法(差 \uXXXX 和数字边缘 case)。这就是递归下降的威力——文法越规整、写起来越像翻译文档。完整支持留作 挑战 B。


# 05.序列化器(缩进美化输出)

┌─ 🎯 阶段 ⑤ 目标 ────────────────────────────────────────┐
│ 完成什么:JsonWriter——把 JsonNode 树反向写回字符串        │
│ 不做什么:暂不接 KvDatabase,专心调试格式化输出            │
│ 验收标准:parser → writer 来回转一遍,输出与输入语义一致   │
│ 预计耗时:60 min · 2 Step                                 │
└──────────────────────────────────────────────────────────┘
1
2
3
4
5
6

💡 为什么要 Writer? —— 不光是"美化输出"。整个 KvDatabase 的 save() 落盘、get() 返回字符串值、CLI 打印结果,全靠它。可以把 Writer 看作 Parser 的反函数。


# 灵魂三问:Writer 设计前先想清楚

❓ 为什么 Writer 要支持"紧凑"和"pretty"两种模式?

两种模式的真实场景:

  • 紧凑:保存到文件 / 网络传输 / 嵌入到日志——省字节
  • pretty:人工查看 / 调试输出 / 配置文件——省眼睛
// 紧凑模式:1 行
{"users":[{"name":"Alice","age":30}]}

// pretty 模式:缩进美化
{
  "users": [
    {
      "name": "Alice",
      "age": 30
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12

如果只支持其中一种就要写两遍 Writer 类——所以一个 step 参数(缩进空格数)控制两种模式是最优雅的设计。

❓ 为什么用 std::ostringstream 而不是 std::string + +=?

// ❌ 反例:直接 string 拼接
std::string result;
result += '"';
result += s;
result += '"';
result += std::to_string(3.14);   // ⚠️ to_string 不支持精确 double 格式化(会丢精度)
1
2
3
4
5
6

问题:

  1. std::to_string(double) 固定 6 位小数——3.14159265 会变 3.141593
  2. 大量 += 会触发多次 realloc——每次扩容拷贝整个串

✅ ostringstream 的优势:

  • oss << 3.14 调用 operator<<(double)——按 IEEE 754 默认精度
  • 内部缓冲指数扩容,比手动 += 快得多
  • oss.str() 一次性取出最终字符串——零额外拷贝

❓ Writer 是 Parser 的反函数,那 parse(write(node)) == node 必须成立吗?

  • 答:逻辑上必须——这叫"序列化往返一致性"(roundtrip)。这是 §5.2 末尾要做的回环测试。

🔑 教学要点:写一个序列化器看似简单,但"紧凑/pretty 双模式 + 字符串反向转义 + roundtrip 一致性"这三件事缺一不可——这是工业级 JSON 库必备能力。


# Step 5.1 紧凑序列化模式

新建 JsonWriter.h:

// JsonWriter.h
#pragma once
#include "JsonNode.h"
#include <string>
#include <sstream>

namespace jsonkv {

class JsonWriter {
private:
    std::ostringstream oss;
    int indent = 0;        // 当前缩进深度
    int step;              // 每级缩进多少空格(0 = 紧凑)
    bool pretty;           // step > 0 即 pretty=true

    void writeIndent() { for (int i = 0; i < indent; ++i) oss << ' '; }
    void writeNode(const JsonNode& n);
    void writeString(const std::string& s);

public:
    explicit JsonWriter(int spaces = 2) : step(spaces), pretty(spaces > 0) {}
    std::string write(const JsonNode& root);
};

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

新建 JsonWriter.cpp——先实现简单类型 + 字符串转义:

// JsonWriter.cpp
#include "JsonWriter.h"

namespace jsonkv {

std::string JsonWriter::write(const JsonNode& root) {
    oss.str("");          // 重置缓冲区
    indent = 0;
    writeNode(root);
    return oss.str();
}

void JsonWriter::writeString(const std::string& s) {
    oss << '"';
    for (char c : s) {
        switch (c) {
            case '"':  oss << "\\\""; break;
            case '\\': oss << "\\\\"; break;
            case '\n': oss << "\\n";  break;
            case '\t': oss << "\\t";  break;
            case '\r': oss << "\\r";  break;
            case '\b': oss << "\\b";  break;
            case '\f': oss << "\\f";  break;
            default:   oss << c;      break;
        }
    }
    oss << '"';
}

void JsonWriter::writeNode(const JsonNode& n) {
    switch (n.type()) {
        case JsonNode::Type::Null:   oss << "null"; break;
        case JsonNode::Type::Bool:   oss << (n.asBool() ? "true" : "false"); break;
        case JsonNode::Type::Number: oss << n.asNumber(); break;
        case JsonNode::Type::String: writeString(n.asString()); break;
        case JsonNode::Type::Array:  oss << "[/* TODO */]"; break;        // 先占位
        case JsonNode::Type::Object: oss << "{/* TODO */}"; break;        // 先占位
    }
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

测试 4 种简单类型——构造一棵小树喂给 Writer:

// test_writer.cpp
#include "JsonParser.h"
#include "JsonWriter.h"
#include <iostream>

int main() {
    using namespace jsonkv;
    JsonParser p(R"({"a":null,"b":true,"c":3.14,"d":"hi\nyou"})");
    auto root = p.parse();

    JsonWriter w(0);                 // 紧凑模式
    std::cout << w.write(*root) << "\n";
}
1
2
3
4
5
6
7
8
9
10
11
12
13

🧪 预期输出:

{/* TODO */}
1

⚠️ 因为还没实现 Array/Object,输出是占位符。但字符串转义已经能通过测试——把 *root 改成 *root->at("d").something 之类的能直接打印 string 类型的话,会看到输出 "hi\nyou"(注意里面的换行被转义)。


# Step 5.2 Array 缩进美化

把 writeNode 中的两个占位分支换成完整版:

case JsonNode::Type::Array: {
    const auto& arr = n.asArray();
    if (arr.empty()) { oss << "[]"; break; }
    oss << "[";
    indent += step;
    for (size_t i = 0; i < arr.size(); ++i) {
        if (pretty) { oss << "\n"; writeIndent(); }
        writeNode(*arr[i]);                                  // ⭐ 递归
        if (i + 1 < arr.size()) oss << ",";
    }
    indent -= step;
    if (pretty) { oss << "\n"; writeIndent(); }
    oss << "]";
    break;
}
case JsonNode::Type::Object: {
    const auto& obj = n.asObject();
    if (obj.empty()) { oss << "{}"; break; }
    oss << "{";
    indent += step;
    size_t i = 0;
    for (const auto& [k, v] : obj) {                          // ⭐ 结构化绑定
        if (pretty) { oss << "\n"; writeIndent(); }
        writeString(k);
        oss << (pretty ? ": " : ":");
        writeNode(*v);
        if (++i < obj.size()) oss << ",";
    }
    indent -= step;
    if (pretty) { oss << "\n"; writeIndent(); }
    oss << "}";
    break;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

写一个回环测试——用 Parser 解析,再用 Writer 写回:

const char* json = R"({"users":[{"name":"Alice","age":30},{"name":"Bob","age":25}]})";

JsonParser p(json);
auto root = p.parse();

std::cout << "--- 紧凑模式 ---\n";
JsonWriter w0(0);
std::cout << w0.write(*root) << "\n\n";

std::cout << "--- 缩进 2 空格 ---\n";
JsonWriter w2(2);
std::cout << w2.write(*root) << "\n";
1
2
3
4
5
6
7
8
9
10
11
12
🧪 预期输出:
--- 紧凑模式 ---
{"users":[{"age":30,"name":"Alice"},{"age":25,"name":"Bob"}]}

--- 缩进 2 空格 ---
{
  "users": [
    {
      "age": 30,
      "name": "Alice"
    },
    {
      "age": 25,
      "name": "Bob"
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

✅ Parser→Writer 回环通过,缩进版完美格式化。

📚 观察:std::map<string, ...> 是有序的,所以 name 和 age 输出顺序变成了字母序(age 排在 name 前面)。如果想保持插入顺序,要换 std::vector<std::pair<...>>——这是教学版的简化。

┌─ 📌 阶段 ⑤ 小结 ────────────────────────────────────────┐
│ ✅ 已掌握                                                 │
│   • Step 5.1 紧凑输出 + 字符串反向转义                    │
│   • Step 5.2 Array/Object 递归 + pretty 缩进              │
│ 🔜 下一阶段 ⑥:把 Parser+Writer 串起来做 KvDatabase        │
│ 💡 commit 建议:feat(jsonkv): json writer with pretty    │
└──────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7

# 06.KvDatabase 内存数据库

┌─ 🎯 阶段 ⑥ 目标 ────────────────────────────────────────┐
│ 完成什么:KvDatabase(启动加载、get/set/del、退出保存)    │
│ 不做什么:暂不写 CLI,先用 main 直接调 API                 │
│ 验收标准:set 后退出再启动,能 get 到原值(即真正落了盘)   │
│ 预计耗时:90 min · 3 Step                                 │
└──────────────────────────────────────────────────────────┘
1
2
3
4
5
6

💡 本阶段的灵魂三问

  1. 数据存哪?→ 内存中一棵 JsonNode 树(顶层 object),文件里一份 JSON 文本
  2. 启动如何恢复?→ 析构时 save,构造后 load
  3. 没找到怎么表达?→ std::optional(C++17 标准答案)

# Step 6.1 空骨架与加载

新建 KvDatabase.h:

// KvDatabase.h
#pragma once
#include "JsonNode.h"
#include <optional>
#include <string>

namespace jsonkv {

class KvDatabase {
private:
    JsonNodePtr root;          // 顶层永远是 object
    std::string dataFile;
    bool dirty = false;        // 有未保存的修改?

public:
    explicit KvDatabase(std::string file = "kv.json");
    ~KvDatabase();

    // 后续 Step 逐步添加:get/set/del
    void load();
    void save();

    size_t size() const { return root->size(); }
};

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

新建 KvDatabase.cpp:

// KvDatabase.cpp
#include "KvDatabase.h"
#include "JsonParser.h"
#include "JsonWriter.h"
#include <fstream>
#include <sstream>

namespace jsonkv {

KvDatabase::KvDatabase(std::string file)
    : root(makeObject()), dataFile(std::move(file)) {}

KvDatabase::~KvDatabase() {
    if (dirty) {
        try { save(); }
        catch (...) { /* ⚠️ 析构不能让异常逃出 */ }
    }
}

void KvDatabase::load() {
    std::ifstream ifs(dataFile);
    if (!ifs) return;                       // 首次运行无文件,OK
    std::stringstream ss;
    ss << ifs.rdbuf();
    JsonParser p(ss.str());
    root = p.parse();
    if (!root->isObject()) {                // 兜底:必须是 object
        root = makeObject();
    }
    dirty = false;
}

void KvDatabase::save() {
    JsonWriter w(2);                        // 美化输出
    std::ofstream ofs(dataFile);
    if (!ofs) throw JsonIoError("cannot open " + dataFile);
    ofs << w.write(*root);
    dirty = false;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

测试 —— 空数据库的加载/保存:

// test_db.cpp
#include "KvDatabase.h"
#include <iostream>

int main() {
    using namespace jsonkv;
    KvDatabase db("test.json");
    db.load();
    std::cout << "size = " << db.size() << "\n";   // 0(空)
}
1
2
3
4
5
6
7
8
9
10
🧪 编译运行:
$ g++ -std=c++17 *.cpp -o test_db &amp;&amp; ./test_db
size = 0
1
2
3

✅ 空数据库加载不报错。手工创建一个 test.json 试试:

$ echo '{"hello":"world","num":42}' > test.json
$ ./test_db
size = 2
1
2
3

✅ 文件存在时 size=2,证明 load() 接通了 Parser。

📚 if (dirty) try { save(); } catch (...) 是析构函数的标准写法。析构时若有异常逃出,遇到正在异常展开的栈会触发 std::terminate 直接崩溃——所以析构里所有可能抛异常的操作都必须 try/catch 兜住。


# Step 6.2 get/set 单层

KvDatabase.h 类内追加:

public:
    // ⭐ optional 表达"可能没有"
    std::optional<std::string> get(const std::string& path) const;
    void set(const std::string& path, JsonNodePtr value);
1
2
3
4

KvDatabase.cpp 追加(先做单层版,下一步再升级多级):

std::optional<std::string> KvDatabase::get(const std::string& path) const {
    try {
        const JsonNode& node = root->at(path);
        JsonWriter w(0);                     // 紧凑序列化
        return w.write(node);
    } catch (const KeyNotFoundError&) {
        return std::nullopt;                 // ⭐ 找不到返回 nullopt
    }
}

void KvDatabase::set(const std::string& path, JsonNodePtr value) {
    // ⚠️ v1:暂只支持单层 key,不带点号
    root->asObject()[path] = std::move(value);
    dirty = true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

测试:

KvDatabase db("test.json");
db.load();

db.set("name", makeString("Alice"));
db.set("age",  makeNumber(30));

std::cout << "name = " << db.get("name").value_or("(nil)") << "\n";
std::cout << "age  = " << db.get("age").value_or("(nil)") << "\n";
std::cout << "miss = " << db.get("city").value_or("(nil)") << "\n";

db.save();      // 落盘
1
2
3
4
5
6
7
8
9
10
11
🧪 预期输出:
name = "Alice"
age  = 30
miss = (nil)

🧪 检查文件:
$ cat test.json
{
  "age": 30,
  "name": "Alice"
}
1
2
3
4
5
6
7
8
9
10
11

✅ 三件事一起验证通过:set 能改、get 能查、save 真落盘。

📚 std::optional<T>::value_or(default) —— 没有值时返回默认。比 "返回 bool 同时通过出参传值" 清晰得多。


# Step 6.3 多级路径与 del

把 set 整体替换为支持 a.b.c 路径的版本:

void KvDatabase::set(const std::string& path, JsonNodePtr value) {
    JsonNode* cur = root.get();
    size_t start = 0;
    while (true) {
        size_t dot = path.find('.', start);
        std::string seg = (dot == std::string::npos)
                          ? path.substr(start)
                          : path.substr(start, dot - start);

        if (dot == std::string::npos) {                    // 终点:直接赋值
            cur->asObject()[seg] = std::move(value);
            dirty = true;
            return;
        }

        // 中间路径:不存在或不是 object 就建一个空 object
        auto& obj = cur->asObject();
        if (obj.find(seg) == obj.end() || !obj[seg]->isObject()) {
            obj[seg] = makeObject();
        }
        cur = obj[seg].get();
        start = dot + 1;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

并新增 del:

KvDatabase.h 类内追加:

    bool del(const std::string& path);
1

KvDatabase.cpp 追加:

bool KvDatabase::del(const std::string& path) {
    size_t dot = path.rfind('.');
    if (dot == std::string::npos) {
        // 单层:直接 erase
        bool ok = root->asObject().erase(path) > 0;
        if (ok) dirty = true;
        return ok;
    }
    // 多层:先找到父节点
    try {
        JsonNode& parent = root->at(path.substr(0, dot));
        if (!parent.isObject()) return false;
        bool ok = parent.asObject().erase(path.substr(dot + 1)) > 0;
        if (ok) dirty = true;
        return ok;
    } catch (const KeyNotFoundError&) {
        return false;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

最终测试 —— 多级路径 + 删除 + 持久化:

KvDatabase db("test.json");
db.load();

db.set("user.name", makeString("Alice"));
db.set("user.address.city", makeString("Beijing"));
db.set("user.age", makeNumber(30));

std::cout << *db.get("user.name") << "\n";              // "Alice"
std::cout << *db.get("user.address.city") << "\n";      // "Beijing"
std::cout << *db.get("user") << "\n";                   // 整个 user 对象

bool deleted = db.del("user.age");
std::cout << "del user.age: " << deleted << "\n";       // 1
std::cout << db.get("user.age").value_or("(nil)") << "\n";  // (nil)

db.save();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
🧪 预期输出:
"Alice"
"Beijing"
{"address":{"city":"Beijing"},"age":30,"name":"Alice"}
del user.age: 1
(nil)

🧪 检查持久化:
$ cat test.json
{
  "user": {
    "address": {
      "city": "Beijing"
    },
    "name": "Alice"
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

✅ 多级路径自动建中间 object、del 真删除并 dirty、save 真落盘——这就是一个"Mini Redis"已经长好的样子。

# 🚨 故障演示:dirty 标记的"半成品"陷阱

阶段 ⑥ 已经能跑了——但埋了一个真实工程师必踩的坑。看这段会出问题的代码:

// ⚠️ 假设我们这样调用(连续 set 但中间一个会抛异常)
db.set("a.b", makeNumber(1));     // OK
db.set("a", makeNumber(99));      // 把 a 直接覆盖成 number
db.set("a.x", makeString("hi"));  // ⚠️ a 已经不是 object 了!
1
2
3
4

会发生什么? 来看 set 的多级路径循环:

// 中间路径:不存在或不是 object 就建一个空 object
auto& obj = cur->asObject();      // ⚠️ cur 是 a,但 a 是 number,asObject() 抛 TypeMismatchError
1
2

执行流:第 1 次 set 成功 → dirty=true → 第 2 次 set 成功 → 第 3 次 set 中途 throw → dirty 还是 true,但 a.x 没真正写入。

# 🧪 真的跑一遍看看

KvDatabase db("test.json");
db.load();

db.set("a.b", makeNumber(1));
db.set("a", makeNumber(99));         // a 变成 number

try {
    db.set("a.x", makeString("hi"));  // ⚠️ 这里抛
} catch (const TypeMismatchError& e) {
    std::cout << "caught: " << e.what() << "\n";
}

std::cout << *db.get("a") << "\n";    // 99,没受影响
db.save();                            // dirty 是 true,会落盘——**但落盘的是部分修改后的状态**
1
2
3
4
5
6
7
8
9
10
11
12
13
14

预期输出:

caught: [Type] expected object, got number
99
1
2

💡 这不算 bug 但是教训:set 内部应保证"要么全成功、要么全不改"——这就是数据库的**ACID 之 A(原子性)**的微观体现。本案例为了简洁不做事务回滚,但你在挑战 D(下面追加)会回头加上。

# 🛠 防御方法:先校验路径再修改

void KvDatabase::set(const std::string& path, JsonNodePtr value) {
    // ⭐ 第一遍:只走读路径,确保中间节点要么不存在要么是 object
    JsonNode* check = root.get();
    size_t s = 0;
    while (true) {
        size_t d = path.find('.', s);
        if (d == std::string::npos) break;
        std::string seg = path.substr(s, d - s);
        if (!check->isObject()) throw TypeMismatchError("object", "non-object on path");
        auto& obj = check->asObject();
        if (obj.find(seg) != obj.end() && !obj[seg]->isObject()) {
            throw TypeMismatchError("object", "non-object at " + seg);
        }
        if (obj.find(seg) == obj.end()) break;   // 后面要建空 object,无需继续校验
        check = obj[seg].get();
        s = d + 1;
    }

    // ⭐ 第二遍:真正修改(这一遍不会再抛 TypeMismatch)
    // ...(原 set 实现)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

🎯 这正是 03 案例 §9.6 "外键校验" 的同款思路——先全部校验通过,再开始修改状态。这就是真实数据库系统每天都在做的事。

┌─ 📌 阶段 ⑥ 小结 ────────────────────────────────────────┐
│ ✅ 已掌握                                                 │
│   • Step 6.1 空骨架 + load/save + 析构 noexcept            │
│   • Step 6.2 get(optional)+ set 单层版                   │
│   • Step 6.3 多级路径 + del + dirty 管理                   │
│ 🔜 下一阶段 ⑦:包一层 CLI REPL 让人能交互                  │
│ 💡 commit 建议:feat(jsonkv): kv database with persist   │
└──────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8

# 07.CLI 工具与 REPL

┌─ 🎯 阶段 ⑦ 目标 ────────────────────────────────────────┐
│ 完成什么:5 命令 REPL(GET/SET/DEL/SAVE/EXIT)              │
│ 不做什么:不做命令历史、不做 readline 自动补全              │
│ 验收标准:交互式 SET 用户数据 → EXIT 重启 → GET 还在        │
│ 预计耗时:30 min · 1 Step                                  │
└──────────────────────────────────────────────────────────┘
1
2
3
4
5
6

# 灵魂三问:CLI 该如何设计?

❓ 为什么 main 函数本身不直接处理命令,要包一层 repl() 函数?

来看反例:

// ❌ 全部塞进 main
int main(int argc, char** argv) {
    KvDatabase db(argv[1]);
    db.load();
    while (...) {
        // 命令解析、try/catch、输出格式化全堆在 main
    }
}
1
2
3
4
5
6
7
8

问题:

  1. 职责混乱——main 应该只做"组装 + 启动",命令循环是业务逻辑
  2. 测试不友好——想单测 REPL 就必须启动整个 main
  3. 无法复用——将来想加 TCP 模式时,只能复制这段循环

✅ 正确做法:把 REPL 抽成独立函数,main 仅做依赖装配(这是依赖注入的最简形式)。

❓ 顶层那个 try { ... } catch (const JsonError& e) 兜底真的有必要吗?

绝对必要!这是 §02 阶段 ① 写的异常底座的最终大结算:

异常种类 在哪 throw 在哪 catch
JsonParseError Parser 在用户 SET 错误 JSON 时 ⭐ REPL 顶层
TypeMismatchError Node 在 GET 类型不符时 ⭐ REPL 顶层
KeyNotFoundError Node 在 GET 不存在的路径时 (转 optional 化为 (nil))
JsonIoError KvDb 在 save 文件失败时 ⭐ REPL 顶层

没有这个 catch 会怎样:用户随便输 SET k {{{ → 异常向上传播 → main 函数没有 catch → std::terminate 直接崩溃——一个错命令搞挂整个 REPL。这是工业级 CLI 必须的"防御层"。

❓ 为什么 SET 命令支持任意 JSON 而不是 SET path string-only?

  • 答:因为这是 Mini Redis 不是 Mini-Mini Redis。SET user.tags ["c++", "json"] 这种业务必须支持。所以 SET 后面剩下的整行都喂给 Parser——既复用了 Parser,又让 SET 命令一行支持 6 种类型。

🔑 教学要点:到这一节,你才真正看到**§02 异常体系的远期回报**——80 行的异常类换来了 §07 这里3 个 throw 点对应 1 个顶层 catch 的清爽防御层。这就是"先打地基"的工程价值。


# Step 7.1 循环读命令兜底

新建 main.cpp:

// main.cpp
#include "KvDatabase.h"
#include "JsonParser.h"
#include <iostream>
#include <sstream>

using namespace jsonkv;

void repl(KvDatabase& db) {
    std::cout << "jsonkv REPL.\n"
                 "Commands:\n"
                 "  GET <path>          查询路径值\n"
                 "  SET <path> <json>   设置(json 是合法 JSON)\n"
                 "  DEL <path>          删除\n"
                 "  SAVE                立即落盘\n"
                 "  EXIT                退出(自动落盘)\n";

    std::string line;
    while (std::cout << "> ", std::getline(std::cin, line)) {
        std::istringstream iss(line);
        std::string cmd; iss >> cmd;

        try {                                     // ⭐ 顶层一把抓所有 JsonError
            if (cmd == "GET") {
                std::string path; iss >> path;
                auto v = db.get(path);
                std::cout << (v ? *v : "(nil)") << "\n";
            }
            else if (cmd == "SET") {
                std::string path; iss >> path;
                std::string rest;
                std::getline(iss, rest);          // path 后面剩下的整行 = JSON
                JsonParser p(rest);
                db.set(path, p.parse());
                std::cout << "OK\n";
            }
            else if (cmd == "DEL") {
                std::string path; iss >> path;
                std::cout << (db.del(path) ? "OK" : "(nil)") << "\n";
            }
            else if (cmd == "SAVE") {
                db.save();
                std::cout << "saved\n";
            }
            else if (cmd == "EXIT") {
                break;
            }
            else if (cmd.empty()) {
                continue;
            }
            else {
                std::cout << "unknown command\n";
            }
        }
        catch (const JsonError& e) {              // 解析错/类型错/IO错都进这里
            std::cout << "ERR " << e.what() << "\n";
        }
    }
}

int main(int argc, char** argv) {
    KvDatabase db(argc > 1 ? argv[1] : "kv.json");
    db.load();
    repl(db);
    db.save();
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

编译并交互一把:

🧪 编译运行:
$ g++ -std=c++17 *.cpp -o jsonkv
$ ./jsonkv kv.json
jsonkv REPL.
Commands: ...
> SET user.name "Alice"
OK
> SET user.age 30
OK
> SET user.address.city "Beijing"
OK
> GET user
{"address":{"city":"Beijing"},"age":30,"name":"Alice"}
> GET user.address.city
"Beijing"
> DEL user.age
OK
> SET bad {{{
ERR [Parse] line 1 col 2: unexpected char: {
> SAVE
saved
> EXIT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
🧪 重启验证持久化:
$ ./jsonkv kv.json
> GET user.name
"Alice"
> EXIT
1
2
3
4
5

✅ 重启后数据还在——"内存 + 文件双层存储"完整闭环。故意打错 JSON 也不会崩——异常被顶层 catch 兜住,REPL 继续等下一行。

┌─ 📌 阶段 ⑦ 小结 ────────────────────────────────────────┐
│ ✅ 已掌握                                                 │
│   • Step 7.1 5 命令 REPL + 顶层异常兜底                   │
│ 🎉 整个项目跑通,进入 [08.项目总结] 复盘                  │
│ 💡 commit 建议:feat(jsonkv): cli repl                   │
└──────────────────────────────────────────────────────────┘
1
2
3
4
5
6

💼 生产化补丁建议:替换 std::getline 为 linenoise (opens new window) 或 GNU readline 即获得命令历史 + Tab 补全;加 WATCH <path> 命令配合 inotify 即可作为配置中心使用。


# 08.项目总结盘点

现代特性 出现次数 典型用法
std::variant 1 处(核心) 表达 6 种 JSON 类型
std::unique_ptr 全篇 节点所有权
make_unique 10+ 处 替代 new
std::optional 3 处 get 返回值
std::string_view Parser 零拷贝字符串引用
移动语义 std::move 全篇 字符串、unique_ptr 转移
结构化绑定 5 处 for (auto& [k, v] : obj)
std::visit (挑战) variant 模式匹配
[[noreturn]] error() 静态分析友好
继承构造 using base::base 异常类 复用基类构造

项目数据:1100 行 / 8 文件 / 0 处裸 new / 0 处手动 delete / 5 个自定义异常类。


# 09.项目技术思考

# 9.1 unique_ptr 拷贝问题

unique_ptr<T> 表达独占所有权——同一时间只能有一个 unique_ptr 指向同一对象。它禁用了拷贝构造和拷贝赋值,只允许移动构造和移动赋值。

auto p1 = std::make_unique<JsonNode>();
auto p2 = p1;              // ❌ 编译错误:拷贝构造被删除
auto p3 = std::move(p1);   // ✅ 转移所有权,p1 变 nullptr
1
2
3

为什么这样设计?避免"两个指针指向同一对象、各自 delete 一次"的灾难(double-free)。如果你需要共享所有权,用 shared_ptr。

# 9.2 异常 vs 错误码

场景 推荐 理由
用户输入错误(如菜单选错) 错误码 频繁发生,开销不可忽略
资源不可用(文件打不开、内存不足) 异常 偶发但严重,必须处理
不变量被破坏(数组越界、变体类型错) 异常 程序员错误,应当立即崩溃
跨多层调用栈传递 异常 中间层不需要逐层传 errno
性能敏感的内层循环 错误码 异常路径开销大(几百纳秒到微秒)

本案例的选择:所有 JSON 解析错、类型错、文件错都用异常;CLI 的"未知命令"用错误码(cout 提示)。

# 9.3 string_view 危险

string_view 是指针 + 长度的视图,不拥有数据。如果原始字符串先析构,view 就成了野指针:

std::string_view bad() {
    std::string s = "hello";
    return s;             // ⚠️ s 析构后,返回的 view 指向无效内存
}
1
2
3
4

安全用法:

  • 函数参数(接收方不会保存)
  • 局部变量(不返回、不存放)
  • 解析器内部(解析过程中原字符串保持存活)

本案例 JsonParser 在构造时接收 string_view,使用方必须保证传入的字符串在 parse() 期间不被析构。


# 10.衔接与延伸

# 10.1 与上一案例的差异

维度 案例 03 校园 案例 04 JSON+KV
容器节点类型 同质 异质(variant 6 种)
内存管理 shared_ptr unique_ptr 全 RAII
错误处理 错误码 完整异常体系
数据结构 扁平表 递归树
字符串 std::string string_view 零拷贝

# 10.2 下一案例的递进

下一案例 05.多线程订单与线程池 会做四件升级:

  1. 加并发:多个线程同时读写 KvDatabase(mutex / shared_mutex)
  2. 生产者-消费者:condition_variable 实现订单队列
  3. future / promise:异步任务结果获取
  4. 线程池组件:可重用的工业级线程池

# 10.3 三个延伸挑战

挑战 A(基础)· 实现 std::visit

用 std::visit 重写 JsonWriter::writeNode:

void writeNode(const JsonNode& n) {
    std::visit([this](auto&& arg){
        using T = std::decay_t<decltype(arg)>;
        if constexpr (std::is_same_v<T, std::nullptr_t>) oss << "null";
        else if constexpr (std::is_same_v<T, bool>)     oss << (arg ? "true" : "false");
        // ...
    }, n.value());
}
1
2
3
4
5
6
7
8

挑战 B(进阶)· 加 Unicode \uXXXX 转义解析

完整支持 RFC 8259 的字符串转义,包括 \u4E2D 这种中文 Unicode。

挑战 C(现代化)· 用 PMR(多态内存资源)

用 std::pmr::vector 替换 std::vector,所有 JSON 节点从同一个内存池分配——大幅减少堆碎片。这是 C++17 的高级特性。

挑战 D(事务原子性)· set 操作要么全成功要么全失败

回到 §6.3 故障演示 提到的"半成品陷阱"。重写 set() 让它具备真实数据库的原子性——这里给两种实现思路:

  1. 预校验法(已在 §6.3 给出代码片段):先遍历一遍校验路径合法,再开始修改
  2. 影子拷贝法:修改前先拷贝整个 root 树(深拷贝),如果 set 中途抛异常就 root = std::move(backup) 回滚

实现哪种取决于你的取舍:预校验法零额外内存但要遍历两遍;影子拷贝法只走一遍但需要深拷贝整个树。这个权衡正是现实数据库的"乐观并发 vs 悲观锁"的雏形。

挑战 E(生产化)· 配置热加载

让 KvDatabase 监听 kv.json 文件变化(inotify/FSEvents),文件被外部修改时自动重新 load——这样这个 KV 数据库就变成了一个配置中心。提示:用 std::thread + 文件 mtime 轮询是最简实现,挑战 E+ 是用真正的 inotify。


  • ⬅ 上一案例:03.校园身份预约系统 (opens new window)
  • ➡ 下一案例:05.多线程订单与线程池 —— 并发栈:mutex / cv / future / 线程池
上次更新: 2026/06/10, 18:57:18
校园身份预约系统
订单票务购买系统

← 校园身份预约系统 订单票务购买系统→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式