现代 C++ JSON 编程入门教程 – wiki基地


现代 C++ JSON 编程入门:从零到精通的详细指南

在当今的软件开发中,JSON (JavaScript Object Notation) 已成为数据交换的通用语言。无论是 Web API、配置文件,还是各种应用程序之间的数据通信,JSON 的身影无处不在。对于 C++ 开发者而言,高效、优雅地处理 JSON 数据是一项必备技能。幸运的是,随着 C++ 语言的现代化,涌现了许多优秀的库,使得在 C++ 中操作 JSON 变得前所未有的简单。

本文将作为一篇详尽的入门教程,带领您走进现代 C++ 的 JSON 编程世界。我们将重点使用目前社区中最受欢迎、功能最强大的库之一—— nlohmann/json,通过丰富的代码示例,从环境搭建、基本操作,到与自定义类型的无缝转换,一步步掌握 C++ JSON 编程的核心技巧。

1. 为什么选择 nlohmann/json?

在选择 C++ JSON 库时,我们有多种选择,例如 RapidJSON、jsoncpp、simdjson 等。它们各有优劣,但在教学和日常开发的便利性方面,nlohmann/json 库(通常简称为 json 库)具有无与伦比的优势:

  • 现代化语法:它的 API 设计得非常直观,使用起来如同操作 Python 的字典或 JavaScript 的对象,极大地降低了学习曲线。
  • 单一头文件:整个库仅由一个头文件 json.hpp 组成。您只需将其下载并包含到您的项目中即可,无需复杂的编译和链接过程。
  • 功能全面:支持完整的 JSON 标准,包括序列化(C++ 对象转 JSON 字符串)、反序列化(JSON 字符串转 C++ 对象)、JSON Pointer、JSON Patch 等高级功能。
  • 与 STL 的无缝集成:可以像使用 std::vectorstd::map 一样自然地使用 json 对象。
  • 强大的类型转换:能够轻松地将 JSON 数据与 C++ 的原生类型(int, string, bool)以及自定义的 structclass 进行相互转换。
  • 详细的错误处理:提供清晰的异常类型,帮助您轻松定位解析或访问错误。

正是这些特性,使其成为现代 C++ 项目中处理 JSON 的首选。

2. 环境准备与项目设置

要开始使用 nlohmann/json,您需要先将其集成到您的项目中。以下是几种常见的方法:

方法一:直接下载头文件(最简单)

  1. 访问 nlohmann/json 的 GitHub 仓库:https://github.com/nlohmann/json
  2. single_include/nlohmann/ 目录下找到 json.hpp 文件。
  3. 将其下载到您的项目目录中,例如放在一个名为 include 的子文件夹里。
  4. 在您的 C++ 源代码中,通过 #include "include/json.hpp" 来引入它。

方法二:使用 CMake 和 FetchContent(推荐用于现代 C++ 项目)

如果您的项目使用 CMake,这是最推荐的现代化管理方式。它会自动下载并配置好依赖。

在您的 CMakeLists.txt 文件中添加以下内容:

“`cmake
cmake_minimum_required(VERSION 3.14)
project(MyJsonProject)

C++ 标准

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

引入 FetchContent 模块

include(FetchContent)

声明 nlohmann_json 依赖

FetchContent_Declare(
nlohmann_json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.2 # 您可以使用最新的稳定版本
)

使依赖可用

FetchContent_MakeAvailable(nlohmann_json)

添加您的可执行文件

add_executable(main main.cpp)

链接 nlohmann_json 库(它会自动处理 include 目录)

target_link_libraries(main PRIVATE nlohmann_json::nlohmann_json)
“`

通过这种方式,CMake 会在构建时自动处理好一切,非常干净和可移植。

方法三:使用包管理器(vcpkg 或 Conan)

对于大型项目,使用像 vcpkg 这样的包管理器是最佳实践。

  • 使用 vcpkg:
    bash
    vcpkg install nlohmann-json

    然后在 CMake 中使用 find_package(nlohmann_json REQUIRED)

无论您选择哪种方式,最终目标都是让编译器能够找到并使用 json.hpp。在本文的示例中,我们假设您已经完成了这一步。

3. JSON 的基本创建与操作

让我们开始编写代码。首先,引入头文件并使用命名空间:

“`cpp

include

include “nlohmann/json.hpp” // 假设 json.hpp 在 nlohmann 目录下

// 使用 using 指令简化代码
using json = nlohmann::json;

int main() {
// 我们的 JSON 探索之旅从这里开始
return 0;
}
“`

3.1 创建 JSON 对象

创建 JSON 对象有多种方式,下面介绍最常用的几种。

a. 使用初始化列表(类似 JavaScript/Python)

这是最直观的方式,语法几乎和 JSON 本身一样。

“`cpp
// 创建一个复杂的 JSON 对象
json j = {
{“name”, “Alice”},
{“age”, 30},
{“is_student”, false},
{“courses”, {“C++”, “Python”, “Data Structures”}},
{“address”, {
{“street”, “123 Main St”},
{“city”, “Anytown”}
}},
{“account_balance”, nullptr} // 表示 JSON 的 null
};

// 打印创建的 JSON 对象
// .dump() 方法可以将其序列化为字符串,参数 4 表示缩进空格数,用于美化输出
std::cout << j.dump(4) << std::endl;
“`

输出结果:

json
{
"account_balance": null,
"address": {
"city": "Anytown",
"street": "123 Main St"
},
"age": 30,
"courses": [
"C++",
"Python",
"Data Structures"
],
"is_student": false,
"name": "Alice"
}

b. 逐个添加键值对(类似 std::map

您可以先创建一个空的 JSON 对象,然后像操作 std::map 一样动态添加元素。

“`cpp
json j_person;

j_person[“name”] = “Bob”;
j_person[“age”] = 25;
j_person[“skills”] = json::array({“Java”, “Go”}); // 创建一个 JSON 数组
j_person[“address”] = json::object(); // 创建一个空的子对象
j_person[“address”][“city”] = “Metropolis”;

std::cout << j_person.dump(4) << std::endl;
“`

3.2 解析 JSON 字符串

在实际应用中,我们通常从文件或网络 API 接收 JSON 字符串,然后需要将其解析为 C++ 中的 json 对象。

使用 json::parse()静态方法即可完成:

“`cpp
// 假设这是从 API 返回的字符串
std::string json_string = R”({
“id”: 1001,
“product”: “Laptop”,
“in_stock”: true,
“variants”: [
{“sku”: “LPT-001”, “price”: 999.99},
{“sku”: “LPT-002”, “price”: 1299.99}
]
})”;

try {
// 使用 C++11 的原始字符串字面量 (R”()”) 可以避免转义双引号
json parsed_json = json::parse(json_string);
std::cout << “Parse successful!\n”;
std::cout << parsed_json.dump(4) << std::endl;
} catch (json::parse_error& e) {
// 如果字符串格式不正确,会抛出异常
std::cerr << “JSON parse error: ” << e.what() << std::endl;
}
“`

重点提示:始终使用 try-catch 块来包裹 json::parse() 调用,因为外部输入的 JSON 字符串格式可能不合法,这能保证程序的健壮性。

4. 访问 JSON 数据

一旦我们有了 json 对象,就需要从中提取数据。

4.1 使用 [] 操作符访问

这是最直接的方式,但如果键不存在,它会创建一个 null 值的键并返回,或者在某些情况下(常量对象)抛出异常。

“`cpp
json j = {{“name”, “Alice”}, {“age”, 30}};

// 获取值并隐式转换为 C++ 类型
std::string name = j[“name”];
int age = j[“age”];

std::cout << “Name: ” << name << “, Age: ” << age << std::endl;

// 访问不存在的键
std::cout << “Country: ” << j[“country”] << std::endl; // 会输出 “null”
std::cout << “After access: ” << j.dump() << std::endl; // j 现在包含了 {“country”: null}
“`

4.2 使用 .at() 方法(更安全)

.at() 的行为类似于 std::map::at()。如果键存在,它返回对应的值;如果不存在,它会抛出一个 json::out_of_range 异常。

“`cpp
try {
std::string city = j_person.at(“address”).at(“city”);
std::cout << “City: ” << city << std::endl;

// 尝试访问不存在的键
std::string state = j_person.at("address").at("state");

} catch (json::out_of_range& e) {
std::cerr << “Access error: ” << e.what() << std::endl;
}
“`

4.3 使用 .value() 方法(最推荐用于可选字段)

.value() 方法是处理可选字段的最佳方式。它接受一个键和一个默认值。如果键存在,返回其值;如果不存在,返回您提供的默认值,并且不会修改原始 JSON 对象。

“`cpp
json config = {{“theme”, “dark”}, {“font_size”, 14}};

// 键存在,返回 “dark”
std::string theme = config.value(“theme”, “light”);

// 键 “language” 不存在,返回默认值 “en”
std::string lang = config.value(“language”, “en”);

std::cout << “Theme: ” << theme << std::endl;
std::cout << “Language: ” << lang << std::endl;
std::cout << “Original config: ” << config.dump() << std::endl; // config 未被修改
“`

4.4 迭代 JSON 对象和数组

  • 迭代对象(键值对):使用 .items() 方法。

cpp
json j_obj = {{"one", 1}, {"two", 2}, {"three", 3}};
for (auto& [key, value] : j_obj.items()) {
std::cout << "Key: " << key << ", Value: " << value << std::endl;
}

  • 迭代数组:像操作 std::vector 一样使用范围 for 循环。

cpp
json j_arr = {"apple", "banana", "cherry"};
for (auto& element : j_arr) {
std::cout << "Fruit: " << element.get<std::string>() << std::endl;
}

注意: element 本身是 json 类型,需要使用 .get<T>() 显式转换为目标类型。

5. 序列化与反序列化:与自定义类型的交互

这是 nlohmann/json 库最强大的功能之一:自动将 JSON 与您的 C++ structclass 进行转换。这极大地简化了代码,避免了手动逐个字段赋值的繁琐工作。

假设我们有以下 Person 结构体:

“`cpp
struct Address {
std::string street;
std::string city;
};

struct Person {
std::string name;
int age;
Address address;
std::vector skills;
};
“`

要让 nlohmann/json 知道如何转换这个结构体,我们需要提供两个函数:to_jsonfrom_json

方法一:定义 to_jsonfrom_json 函数

这是最灵活的方式。您需要在您的 struct 所在的命名空间中定义这两个函数。

“`cpp
// 在 Person 结构体所在的命名空间中(这里是全局命名空间)

void to_json(json& j, const Address& addr) {
j = json{{“street”, addr.street}, {“city”, addr.city}};
}

void from_json(const json& j, Address& addr) {
j.at(“street”).get_to(addr.street);
j.at(“city”).get_to(addr.city);
}

void to_json(json& j, const Person& p) {
j = json{
{“name”, p.name},
{“age”, p.age},
{“address”, p.address}, // 会自动调用 Address 的 to_json
{“skills”, p.skills}
};
}

void from_json(const json& j, Person& p) {
j.at(“name”).get_to(p.name);
j.at(“age”).get_to(p.age);
j.at(“address”).get_to(p.address); // 会自动调用 Address 的 from_json
j.at(“skills”).get_to(p.skills);
}

// 现在,我们可以无缝转换了!

int main() {
// 1. C++ 对象 -> JSON (序列化)
Person p = {“Charlie”, 35, {“456 Oak Ave”, “Bigcity”}, {“Management”, “Public Speaking”}};
json j_from_person = p; // 是的,就是这么简单!
std::cout << “— C++ to JSON —\n” << j_from_person.dump(4) << std::endl;

// 2. JSON -> C++ 对象 (反序列化)
json j_to_person = json::parse(R"({
    "name": "Diana",
    "age": 28,
    "address": {
        "street": "789 Pine Ln",
        "city": "Smalltown"
    },
    "skills": ["Archery", "Diplomacy"]
})");

Person p_from_json = j_to_person.get<Person>(); // 同样简单!
// 或者使用 p_from_json = j_to_person;

std::cout << "\n--- JSON to C++ ---\n";
std::cout << "Name: " << p_from_json.name << std::endl;
std::cout << "City: " << p_from_json.address.city << std::endl;
std::cout << "First skill: " << p_from_json.skills[0] << std::endl;

return 0;

}
“`

方法二:使用 NLOHMANN_DEFINE_TYPE_INTRUSIVE

对于简单的结构体,如果所有成员都是公开的,并且您不想写 to/from_json 函数,可以使用库提供的宏来自动生成它们。

“`cpp

include “nlohmann/json.hpp”

include

include

include

using json = nlohmann::json;

struct SimplePerson {
std::string name;
int age;

// 将此宏放在结构体定义内部
NLOHMANN_DEFINE_TYPE_INTRUSIVE(SimplePerson, name, age)

};

int main() {
SimplePerson sp = {“Eve”, 22};
json j = sp; // 序列化
std::cout << j.dump(4) << std::endl;

auto sp2 = j.get<SimplePerson>(); // 反序列化
std::cout << "Deserialized: " << sp2.name << ", " << sp2.age << std::endl;

}
“`

这个宏非常方便,但它的灵活性不如手动定义函数。例如,您无法自定义 JSON 键的名称或处理可选字段。

6. 错误处理与最佳实践

6.1 详细的异常类型

nlohmann/json 在出错时会抛出特定类型的异常,继承自 json::exception

  • json::parse_error:当输入的字符串不是有效的 JSON 格式时抛出。
  • json::type_error:当您尝试对一个值进行不相容的操作时,例如,试图将一个 JSON 字符串当作数字访问,或者对一个非数组类型进行迭代。
  • json::out_of_range:当使用 .at() 访问一个不存在的键或索引时抛出。

“`cpp
try {
// 错误1: 无效的 JSON 字符串
// json j1 = json::parse(“{‘name’: ‘invalid’}”); // 使用了单引号

// 错误2: 类型错误
json j2 = {{"name", "Frank"}};
int age = j2["age"]; // j2["age"] 是 null,转为 int 会失败

} catch (json::parse_error& e) {
std::cerr << “Parse error at byte ” << e.byte << “: ” << e.what() << std::endl;
} catch (json::type_error& e) {
std::cerr << “Type error: ” << e.what() << std::endl;
} catch (json::exception& e) {
// 捕获所有 json 相关的异常
std::cerr << “An unexpected JSON error occurred: ” << e.what() << std::endl;
}
“`

6.2 性能考量与最佳实践

  • 便利性 vs 性能nlohmann/json 的首要设计目标是易用性和功能完整性,而非极致的解析速度。对于绝大多数应用场景,它的性能绰绰有余。但在需要处理海量 JSON 数据或对延迟有极端要求的场景(如高频交易、游戏引擎),您可能需要考虑 RapidJSONsimdjson 这种性能优先的库。
  • 使用 const auto&:在迭代 JSON 时,使用 const auto& 可以避免不必要的拷贝,提升性能。
  • value() 优于 []:对于可能不存在的字段,优先使用 value() 方法,它更安全,也更清晰地表达了您的意图。
  • 预先检查类型:在访问数据前,可以使用 j.is_string(), j.is_number(), j.is_object(), j.is_array() 等方法来检查类型,避免 type_error 异常。

结论

通过本教程,我们系统地学习了如何使用 nlohmann/json 这个强大的库在现代 C++ 中进行 JSON 编程。从简单的创建、解析和访问,到最亮眼的与自定义 struct 的自动转换,您现在应该已经具备了在实际项目中自信地处理 JSON 数据的能力。

nlohmann/json 的成功,在于它将 C++ 的强大性能与脚本语言般的便利语法完美结合,真正体现了“现代 C++”的设计哲学——让复杂的事情变简单,同时不失效率。

这篇教程仅仅是一个开始。nlohmann/json 还提供了 JSON Pointer、JSON Patch、CBOR/MessagePack 支持等更多高级功能。强烈建议您将官方文档 (https://json.nlohmann.me/) 作为参考,继续深入探索,解锁更多高级用法。祝您在 C++ 的世界中,享受 JSON 编程的乐趣!

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部