公司动态

C++ JSON序列化与反序列化实战:nlohmann/json库详解与工程实践

📅 2026/7/21 7:31:20
C++ JSON序列化与反序列化实战:nlohmann/json库详解与工程实践
1. 项目概述为什么C开发者需要关注JSON处理在C项目中处理JSON数据这听起来像是一个现代Web后端工程师才会频繁遇到的问题但实际情况是随着微服务、配置管理和数据交换格式的标准化JSON已经渗透到了几乎所有软件开发的角落。你可能正在开发一个游戏需要读取JSON格式的关卡配置或者你在编写一个系统监控工具需要将采集到的性能数据序列化成JSON上报又或者你只是在为一个传统的C服务添加一个简单的HTTP API接口。在这些场景下如何高效、安全、便捷地在C对象和JSON文本之间进行转换就成了一个无法回避的工程问题。与Java、Python等语言拥有Jackson、fastjson、json模块这样“开箱即用”且生态成熟的解决方案不同C的标准库并未原生支持JSON。这种“缺失”迫使C开发者要么手动拼接字符串极易出错且难以维护要么引入第三方库。而第三方库的选择又直接关系到项目的编译依赖、二进制大小、运行时性能以及代码的优雅程度。因此实现C中的JSON序列化与反序列化远不止是调用几个API那么简单它涉及到库的选型、内存模型的理解、异常安全的考量以及对现代C特性的熟练运用。简单来说这个项目的核心就是在C环境中构建一套可靠的数据桥梁让结构化的程序内存对象能够与通用的、文本化的JSON数据格式进行无损、高效的双向转换。无论你是刚接触C不久正在为如何解析一个配置文件而发愁还是经验丰富的老手在为一个高性能服务选择合适的数据交换方案深入理解这个过程都将大有裨益。接下来我将以一个实际项目为脉络拆解其中的技术选型、实现细节和避坑指南。2. 核心库选型nlohmann/json 为何成为事实标准当你决定在C项目中使用JSON时第一个问题就是该用哪个库GitHub上相关的库琳琅满目如rapidjson、jsoncpp、Boost.JSON以及我们今天重点讨论的nlohmann/json。经过多年的社区演化nlohmann/json几乎已经成为C JSON处理的事实标准这背后有非常实际的原因。2.1 主要竞争者分析我们先快速对比一下几个主流选择库名称主要特点优点缺点适用场景nlohmann/json现代CC11起头文件库API极其直观零依赖、单头文件、语法糖丰富像脚本语言、文档优秀、社区活跃编译时间较长头文件巨大运行时性能非极致最优快速开发、配置解析、API通信、对代码优雅度要求高的项目rapidjson高性能SAX/DOM风格APIC03兼容速度极快内存效率高支持SAX解析流式API较为底层和繁琐易用性差需要手动管理内存对性能有极致要求处理超大JSON文件嵌入式等资源受限环境jsoncpp老牌库API稳定历史久稳定部分系统已预装API较陈旧C风格不够现代需要编译链接维护遗留项目或在某些特定平台如旧版Android NDK中Boost.JSONBoost库的一部分强调正确性和性能与Boost生态集成质量有保障性能不错需要依赖Boost增加项目复杂度相对较新已经是Boost重度用户的项目追求标准库风格的API2.2 为何首选 nlohmann/json对于大多数应用开发场景我强烈推荐从nlohmann/json开始。它的设计哲学是“提供最符合直觉的API”。例如访问一个嵌套的JSON值你可以直接使用j[person][address][city]这样的语法就像在Python或JavaScript中一样。这种易用性极大地降低了开发者的心智负担减少了样板代码。它是一个仅有头文件header-only的库。这意味着集成成本极低你只需要下载一个json.hpp文件放到你的包含路径中然后在代码里#include “json.hpp”即可。没有复杂的编译和链接步骤特别适合跨平台项目或快速原型开发。虽然巨大的头文件会增加单个编译单元的编译时间但在拥有增量编译和分布式编译缓存的现代开发环境中这通常是可以接受的代价。此外它的文档README堪称开源项目的典范示例丰富几乎涵盖了所有常见用法。活跃的社区也保证了问题的快速响应和持续的功能更新如对C17、C20新特性的支持。因此除非你的项目有非常明确的、可量化的性能瓶颈并且经过 profiling 证实瓶颈就在JSON解析上否则nlohmann/json的易用性和开发效率优势将远远超过其微小的性能开销。注意关于“fastjson序列化不包括转义字符”这个热词需要澄清一下。fastjson是阿里巴巴开源的JavaJSON库以其高性能著称。在C语境下讨论“fastjson”通常是个误解。而“不包括转义字符”可能指的是在序列化字符串时对某些字符如引号、反斜杠不进行转义这通常是不符合JSON规范的危险行为除非你在进行非常特殊的定制化输出。nlohmann/json库会严格按照JSON标准处理转义字符。3. 基础实战从零开始玩转 nlohmann/json理论说了这么多我们直接上手。假设你已经将json.hpp放到了项目中。让我们从最基本的操作开始。3.1 创建与解析JSON对象nlohmann::json是这个库的核心类它可以表示JSON标准中的任何类型对象object、数组array、字符串string、数字number、布尔值boolean和空值null。#include “json.hpp” using json nlohmann::json; // 使用一个简短的别名是个好习惯 // 1. 创建一个空的JSON对象 json j; // 2. 像操作字典一样添加数据 j[“pi”] 3.141; j[“happy”] true; j[“name”] “Niels”; j[“nothing”] nullptr; // 添加一个数组 j[“list”] { 1, 0, 2 }; // 添加一个嵌套对象 j[“person”][“address”][“city”] “Shanghai”; j[“person”][“address”][“street”] “Nanjing Road”; // 此时j 的内容是 // { // “pi”: 3.141, // “happy”: true, // “name”: “Niels”, // “nothing”: null, // “list”: [1, 0, 2], // “person”: { // “address”: { // “city”: “Shanghai”, // “street”: “Nanjing Road” // } // } // } // 3. 序列化将json对象转换为字符串 std::string serialized_string j.dump(); // 紧凑格式 std::string pretty_string j.dump(4); // 缩进为4个空格的漂亮打印格式 std::cout pretty_string std::endl; // 4. 反序列化从字符串或文件解析JSON std::string json_text R“({“color”: “red”, “value”: “#f00”})”; // C11原始字符串字面量避免转义 json j2 json::parse(json_text); // 从字符串解析 std::ifstream i(“config.json”); // 从文件流解析 json j3; i j3; // 使用流操作符 // 5. 访问数据 std::string color j2[“color”]; // 直接取值类型自动转换 int first_item j[“list”][0]; // 访问数组元素 // 安全访问使用 .at() 会在键不存在时抛出异常 try { auto value j.at(“nonexistent_key”); } catch (json::out_of_range e) { std::cerr “Key not found: ” e.what() std::endl; } // 更安全的访问使用 .value(key, default_value) std::string default_color j2.value(“font”, “Arial”); // 如果“font”键不存在返回“Arial”3.2 处理JSON数组JSON数组对应C的std::vector库提供了无缝的转换。// 创建数组 json j_array { “one”, “two”, 3, 4.0, true }; // 迭代数组 for (auto element : j_array) { std::cout element std::endl; } // 像vector一样操作 j_array.push_back(“new item”); j_array.insert(j_array.begin() 1, “inserted item”); // 与std::vector互转 std::vectorint vec {1, 2, 3, 4}; json j_from_vec vec; // 自动转换 std::vectorint vec_from_j j_from_vec.getstd::vectorint(); // 显式转换回来实操心得使用dump()输出时默认是紧凑无空格的格式适合网络传输。调试时使用dump(4)可以生成格式化的字符串便于阅读。另外json::parse()在遇到格式错误的JSON时会抛出json::parse_error异常务必在调用处进行异常处理否则程序会崩溃。4. 进阶核心自定义类型与JSON的自动映射仅仅操作基础的JSON对象和数组还不够。在实际项目中我们更希望将JSON数据直接映射到我们自定义的C结构体或类上反之亦然。这才是“序列化/反序列化”的核心价值。nlohmann/json通过提供一套非常简洁的非侵入式序列化方法来实现这一点。所谓非侵入式是指你不需要修改你的现有类定义比如继承某个基类或添加特殊方法只需要在类的外部提供一个专门的函数告诉库如何转换即可。这符合C的“关注点分离”原则保持了业务类的纯洁性。4.1 为自定义结构体实现序列化假设我们有一个Person结构体struct Person { std::string name; int age; std::vectorstd::string hobbies; // 注意这里没有专门为JSON库设计的任何代码 };我们需要提供两个函数to_json和from_json。// 在命名空间内通常是全局或与Person相同的命名空间定义这两个函数 namespace nlohmann { // 序列化Person - json void to_json(json j, const Person p) { j json{ {“name”, p.name}, {“age”, p.age}, {“hobbies”, p.hobbies} }; // 你也可以分开赋值 // j[“name”] p.name; // j[“age”] p.age; // j[“hobbies”] p.hobbies; } // 反序列化json - Person void from_json(const json j, Person p) { j.at(“name”).get_to(p.name); // 使用.at()确保键存在 j.at(“age”).get_to(p.age); j.at(“hobbies”).get_to(p.hobbies); } } // namespace nlohmann注意我们将这两个函数放在了nlohmann命名空间内。这是利用C的参数依赖查找ADL机制使得json j person;这样的语句能自动找到我们定义的to_json函数。4.2 使用自动映射定义好上述函数后魔法就发生了Person alice {“Alice”, 30, {“Reading”, “Hiking”}}; // 自动序列化 json j alice; // 隐式调用 to_json std::cout j.dump(4) std::endl; // 输出 // { // “name”: “Alice”, // “age”: 30, // “hobbies”: [“Reading”, “Hiking”] // } // 自动反序列化 std::string json_str R“({“name”:”Bob”, “age”:25, “hobbies”:[“Gaming”,”Coding”]})”; json j_bob json::parse(json_str); Person bob j_bob.getPerson(); // 显式调用 from_json // 甚至可以直接赋值需要C17起支持的CTAD或显式类型 Person charlie json::parse(R“({“name”:”Charlie”, “age”:28})”);4.3 处理复杂场景可选字段、枚举和继承可选字段现实中的数据字段可能缺失。我们可以使用std::optional(C17) 或指针来表示。struct UserProfile { std::string username; std::optionalstd::string email; // 可能没有邮箱 std::optionalint level; }; // 在 to_json/from_json 中需要判断 optional 是否有值 void to_json(json j, const UserProfile u) { j[“username”] u.username; if (u.email.has_value()) { j[“email”] u.email.value(); } // 否则不创建 “email” 键 // 对于 optional库有内置支持也可以直接赋值null会被序列化为null j[“level”] u.level; }枚举类型JSON没有枚举通常用字符串或整数表示。我们需要手动转换。enum class Status { Pending, Active, Inactive }; void to_json(json j, const Status s) { switch (s) { case Status::Pending: j “pending”; break; case Status::Active: j “active”; break; case Status::Inactive: j “inactive”; break; } } void from_json(const json j, Status s) { auto str j.getstd::string(); if (str “pending”) s Status::Pending; else if (str “active”) s Status::Active; else if (str “inactive”) s Status::Inactive; else throw json::type_error::create(302, “invalid status value: ” str); }继承与多态这是比较棘手的部分。nlohmann/json本身不直接支持多态序列化。常见的做法是使用一个“类型标签”字段。struct Shape { virtual ~Shape() default; }; struct Circle : Shape { double radius; }; struct Rect : Shape { double width, height; }; void to_json(json j, const Shape s) { // 这不是好办法无法处理多态。需要更复杂的模式如工厂函数。 }对于多态更常见的做法是序列化一个包含类型标识符和具体数据的容器对象或者使用专门的序列化库如cereal、Boost.Serialization。避坑指南在from_json函数中务必使用j.at(“key”)而不是j[“key”]。j[“key”]在键不存在时会自动创建一个null值并返回这可能会掩盖数据错误导致反序列化出的对象状态异常。而j.at(“key”)会进行边界检查并在键不存在时抛出json::out_of_range异常这有助于在数据解析阶段就发现问题。这是保证数据完整性和程序健壮性的关键细节。5. 性能优化与高级特性探秘当你的JSON数据量很大或者序列化/反序列化操作处在性能关键路径上时就需要考虑优化了。nlohmann/json虽然以易用性著称但也提供了一些高级特性和配置选项来满足性能需求。5.1 理解解析与序列化的开销JSON处理的主要开销在于词法分析Tokenization将字符串分解成令牌如花括号、字符串、数字。语法分析Parsing构建内存中的DOM树即json对象。内存分配为DOM树中的字符串、数组、对象分配堆内存。类型转换在JSON类型和C类型之间转换特别是数字和字符串。nlohmann/json默认使用std::map来存储JSON对象这保证了元素的有序性按插入顺序但查找效率是O(log n)。如果你不关心顺序且需要更快的查找可以在包含头文件前定义一个宏#define JSON_USE_GLOBAL_NAMESPACE 1 // 确保宏在包含前定义 #define NLOHMANN_JSON_NAMESPACE_BEGIN namespace nlohmann { #define NLOHMANN_JSON_NAMESPACE_END } #define JSON_USE_ORDERED_MAP 0 // 关键禁用有序map #include “json.hpp”这样库会使用std::unordered_map哈希表查找效率提升到平均O(1)。但请注意生成的JSON字符串中对象的键顺序将是未定义的。5.2 使用JSON Patch和JSON Merge Patch这两个是RFC标准用于描述JSON文档的更改。nlohmann/json内置支持。JSON Patch描述一系列操作如add、remove、replace、move、copy、test来将文档A转换为文档B。适用于精确的增量更新。json doc {{“name”, “Alice”}, {“age”, 30}}; json patch {{{“op”, “replace”}, {“path”, “/age”}, {“value”, 31}}, {{“op”, “add”}, {“path”, “/city”}, {“value”, “NYC”}}}; doc doc.patch(patch); // 应用patchJSON Merge Patch类似于Git的合并用一个新的文档部分覆盖旧文档。更简单但不能处理像将数组元素设为null这样的特定操作。json doc {{“name”, “Alice”}, {“age”, 30}, {“hobbies”, [“reading”]}}; json merge_patch {{“age”, 31}, {“city”, “NYC”}}; doc.merge_patch(merge_patch); // age被更新city被添加hobbies保留在网络API中用Patch来更新资源比发送整个对象更高效。5.3 二进制格式与自定义序列化JSON是文本格式体积较大。对于需要频繁传输或存储的大数据可以考虑MessagePack一种高效的二进制序列化格式。nlohmann/json有一个姐妹库nlohmann/json-schema和社区扩展可以支持MessagePack。你可以先将json对象转换到MessagePack格式进行传输接收方再解析回json。BSONMongoDB使用的二进制JSON。也有相应的C库。自定义二进制格式对于极端性能场景可以定义自己的紧凑二进制协议。但这意味着你要放弃JSON的通用性和可读性。5.4 使用迭代器与SAX接口性能关键路径对于非常大的JSON文件将其全部解析到内存的DOM树中可能不可行。nlohmann/json也提供了SAXSimple API for XML风格的接口允许你在解析过程中接收事件如对象开始、键、值、对象结束从而可以流式处理数据极大减少内存占用。struct MySaxHandler : public nlohmann::json_sax_t { bool null() override { /* 处理null值 */ return true; } bool boolean(bool val) override { /* 处理布尔值 */ return true; } bool number_integer(int64_t val) override { /* 处理整数 */ return true; } bool number_float(double val, const std::string s) override { /* 处理浮点数 */ return true; } bool string(const std::string val) override { /* 处理字符串 */ return true; } bool start_object(std::size_t elements) override { /* 对象开始 */ return true; } bool key(const std::string val) override { /* 对象的键 */ return true; } bool end_object() override { /* 对象结束 */ return true; } // ... 还有其他方法 bool parse_error(std::size_t position, const std::string last_token, const json::exception ex) override { // 处理解析错误 return false; } }; MySaxHandler handler; json::sax_parse(json_text, handler); // 流式解析SAX接口更复杂但给了你完全的控制权是处理GB级别JSON日志文件的利器。6. 工程化实践集成、测试与安全将JSON处理集成到实际项目中远不止是写对to_json/from_json函数那么简单。我们需要考虑编译依赖、单元测试、异常处理和安全性。6.1 依赖管理包管理器与单头文件对于nlohmann/json这样的单头文件库管理方式很简单直接包含将json.hpp下载到项目源码树的third_party或include目录中。这是最直接、对构建系统侵入最小的方式。包管理器如果你使用CMake的FetchContent、Conan或vcpkg可以通过它们来管理依赖。CMake FetchContent:include(FetchContent) FetchContent_Declare( nlohmann_json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.11.2 # 指定一个稳定版本 ) FetchContent_MakeAvailable(nlohmann_json) # 然后你的目标可以 target_link_libraries(your_target PRIVATE nlohmann_json::nlohmann_json)vcpkg:vcpkg install nlohmann-json然后在CMake中find_package(nlohmann_json REQUIRED)。注意事项虽然单头文件方便但json.hpp文件很大超过2万行。这会导致包含它的每一个.cpp文件编译时间显著增加。一个最佳实践是在一个独立的、不经常变动的源文件如json_utils.cpp中包含该头文件并在其中实现所有相关的序列化函数。然后其他文件通过头文件声明来使用这些函数从而将编译依赖隔离加速增量编译。6.2 单元测试确保数据契约的稳定性序列化/反序列化逻辑是数据契约Data Contract的核心必须进行充分的单元测试。// 使用 Google Test 或 Catch2 等框架 TEST(PersonSerializationTest, BasicToJson) { Person p {“Test”, 20, {“A”, “B”}}; json j p; EXPECT_EQ(j[“name”], “Test”); EXPECT_EQ(j[“age”], 20); EXPECT_EQ(j[“hobbies”].size(), 2); } TEST(PersonSerializationTest, BasicFromJson) { std::string json_str R“({“name”:”Test”, “age”:20, “hobbies”:[“A”,”B”]})”; auto j json::parse(json_str); Person p j.getPerson(); EXPECT_EQ(p.name, “Test”); EXPECT_EQ(p.age, 20); EXPECT_EQ(p.hobbies, std::vectorstd::string({“A”, “B”})); } TEST(PersonSerializationTest, MissingFieldThrows) { std::string json_str R“({“name”:”Test”})”; // 缺少 age 和 hobbies auto j json::parse(json_str); EXPECT_THROW(j.getPerson(), json::out_of_range); // 因为 from_json 用了 .at() } TEST(PersonSerializationTest, RoundTrip) { Person original {“Alice”, 30, {“Read”}}; json j original; Person restored j.getPerson(); EXPECT_EQ(original.name, restored.name); EXPECT_EQ(original.age, restored.age); EXPECT_EQ(original.hobbies, restored.hobbies); }重点测试字段完整、字段缺失、字段类型错误、空值、特殊字符如包含引号的字符串、往返一致性Round-trip。6.3 安全考量防范反序列化风险“反序列化漏洞”是安全领域的一个高频词如热词中提到的Shiro、Java反序列化漏洞。其根源在于反序列化过程可能会执行一些意想不到的代码比如根据类型名动态创建对象并调用其特定方法。对于nlohmann/json这样的库其反序列化过程是类型安全的。它只是将JSON数据填充到你已经明确定义好内存布局的C结构体中。它不会根据JSON数据中的某个字段去动态加载类或执行函数。因此从机制上讲它比Java或Python中一些基于反射的序列化框架如pickle要安全得多。但这并不意味着绝对安全风险转移到了业务逻辑层面资源耗尽DoS恶意构造一个深度嵌套如10万层的JSON对象{“a”:{“a”:{…}}}可能导致解析时栈溢出。或者一个巨大的数字字符串”1e999999”可能导致浮点数转换异常。解决方案是在解析前对JSON字符串的大小和结构深度做初步检查或者使用SAX解析器并设置深度限制。内存耗尽一个超大的JSON数组或对象会消耗大量内存。对于不可信的数据源应该设置解析大小上限。逻辑漏洞反序列化后的数据如果未经校验就直接用于业务逻辑可能导致问题。例如一个Person对象的age字段被赋值为-100或10000。必须在from_json函数或后续业务逻辑中进行数据有效性校验。void from_json(const json j, Person p) { j.at(“name”).get_to(p.name); j.at(“age”).get_to(p.age); j.at(“hobbies”).get_to(p.hobbies); // 数据校验 if (p.name.empty()) { throw std::invalid_argument(“Name cannot be empty”); } if (p.age 0 || p.age 150) { throw std::out_of_range(“Age must be between 0 and 150”); } }依赖库漏洞确保你使用的nlohmann/json库版本是最新的没有已知的安全漏洞。定期关注其GitHub仓库的发布和安全公告。7. 常见问题排查与调试技巧实录在实际开发中你一定会遇到各种奇怪的问题。下面是我踩过的一些坑和总结的排查技巧。7.1 编译错误“没有匹配的成员函数”或“无法转换”这是最常见的问题通常是因为类型不匹配。场景1试图将json对象直接赋值给std::string。json j {{“key”, “value”}}; std::string s j; // 错误j是一个对象不是字符串。 std::string s j[“key”]; // 正确。j[“key”] 是字符串类型的json值。场景2自定义类型的to_json/from_json函数签名错误或找不到。检查函数签名是否完全正确必须是void to_json(json, const YourType)和void from_json(const json, YourType)。检查是否放在了正确的命名空间通常放在全局命名空间或nlohmann命名空间确保ADL能生效。最稳妥的方式是放在你的自定义类型的同一个命名空间里。检查头文件包含顺序确保to_json/from_json的函数声明在json.hpp被包含之后且在使用这些函数的地方可见。7.2 运行时错误解析失败parse_errorjson::parse失败时会抛出json::parse_error异常其中包含了位置和原因信息。try { auto j json::parse(“{invalid json}”); } catch (json::parse_error e) { std::cerr “Parse error at byte “ e.byte “: ” e.what() std::endl; // 输出类似Parse error at byte 2: syntax error while parsing object key - invalid literal; last read: ‘{i’ }排查步骤验证JSON格式将出错的字符串粘贴到在线的JSON验证器如 jsonlint.com中检查。检查不可见字符特别是从文件或网络读取时可能存在BOM头\xEF\xBB\xBF或末尾的空白字符。可以使用十六进制查看器检查。检查编码确保JSON文本是UTF-8编码。非UTF-8字符如中文GBK可能导致解析失败。使用dump()调试如果你是从一个复杂的json对象dump出来的字符串无法被重新parse这几乎肯定是库的bug但非常罕见。更可能的原因是在dump之后和parse之前字符串被意外修改了。7.3 数据错误反序列化后字段值不对现象from_json后结构体某个字段的值是默认值如0、空字符串而不是JSON中的值。可能原因1JSON中的键名与结构体字段名不匹配或者大小写不一致。检查from_json函数中使用的键字符串是否完全一致。可能原因2类型不兼容。例如JSON中是浮点数10.5但C结构体中对应字段是int库会进行隐式截断值变成10。建议在from_json中使用.getdecltype(field)()进行显式类型转换或者先获取为json值再检查其类型。void from_json(const json j, MyStruct s) { auto age_json j.at(“age”); if (age_json.is_number_integer()) { age_json.get_to(s.age); } else { throw json::type_error::create(302, “age must be an integer”); } }7.4 性能瓶颈排查如果发现JSON处理变慢可以使用性能分析工具如perf(Linux)、Instruments (macOS)、VTune (Windows) 来定位热点。瓶颈可能在parse、dump或你的to_json/from_json函数中。检查序列化函数自定义的to_json函数如果逻辑复杂如遍历大型容器会成为瓶颈。考虑优化算法。考虑更换数据结构如果频繁查找JSON对象中的键考虑使用unordered_map通过JSON_USE_ORDERED_MAP 0宏。减少临时对象避免在循环内创建大量临时json对象。终极方案对于确定性的性能瓶颈可以考虑换用rapidjson并接受其更复杂的API。7.5 内存泄漏排查罕见但需知nlohmann/json使用现代C的RAII机制管理内存在正常使用下不应有内存泄漏。如果你怀疑有泄漏确保你使用的是最新版本的库。检查是否有循环引用。虽然json对象本身不支持循环引用一个json对象不能直接包含自身但如果你在自定义类型中使用了原始指针或std::shared_ptr并形成了循环引用会导致内存无法释放。在这种情况下考虑使用std::weak_ptr打破循环。使用ValgrindLinux、AddressSanitizer或Visual Studio的内存诊断工具进行检测。最后一个非常实用的调试技巧是当你对一个复杂的json对象的结构不确定时不要只是std::cout整个对象可能输出太长。使用j.dump(4)漂亮打印到文件或者使用j.contains(“key”)检查键是否存在用j.type()检查某个值的类型返回json::value_t枚举用j.is_object(),j.is_array()等方法进行断言这能帮你快速理清数据结构。