前置知识: C++

C++序列化

4 minIntermediate2026/6/14

JSON与二进制序列化

概述

序列化是将内存中的数据结构转换为可存储或传输的格式(如字符串或字节流)的过程,反序列化则是其逆过程。C++ 标准库目前没有内置的序列化支持,但社区提供了多种优秀的第三方库,如 nlohmann/json(JSON 序列化)、protobuf(二进制序列化)、cereal(轻量级序列化)等。

为什么需要序列化?当你需要将数据保存到文件、通过网络发送、或者在不同程序之间交换数据时,就需要序列化。JSON 是最通用的格式,人可读且跨语言支持好;二进制格式(如 protobuf)更紧凑高效,适合对性能和带宽敏感的场景。

基础概念

JSON 序列化:将数据转换为 JSON 格式的字符串。JSON 是键值对格式,支持数字、字符串、布尔值、数组和对象。人可读,调试方便。

二进制序列化:将数据转换为紧凑的二进制字节流。不可读但体积小、速度快。适合网络传输和持久化存储。

Schema:数据结构的定义文件。protobuf 使用 .proto 文件定义数据结构,然后自动生成 C++ 代码。有了 Schema,不同语言之间可以安全地交换数据。

向前/向后兼容:当数据结构发生变化时(如新增字段),旧代码能否读取新数据(向前兼容),新代码能否读取旧数据(向后兼容)。protobuf 天然支持,JSON 需要手动处理。

快速上手

使用 nlohmann/json

# 安装(vcpkg)
vcpkg install nlohmann-json

# 或者单头文件
# 从 https://github.com/nlohmann/json 下载 json.hpp
#include <nlohmann/json.hpp>
#include <iostream>
#include <string>

using json = nlohmann::json;

int main() {
    // 创建 JSON 对象
    json person = {
        {"name", "张三"},
        {"age", 25},
        {"isStudent", false},
        {"scores", {95, 87, 92}},
        {"address", {
            {"city", "北京"},
            {"district", "海淀"}
        }}
    };

    // 序列化为字符串
    std::string jsonStr = person.dump(4);  // 4 空格缩进
    std::cout << jsonStr << std::endl;

    // 反序列化
    json parsed = json::parse(jsonStr);

    // 访问字段
    std::string name = parsed["name"];
    int age = parsed["age"];
    bool isStudent = parsed["isStudent"];

    std::cout << "姓名: " << name << ", 年龄: " << age << std::endl;

    // 访问嵌套字段
    std::string city = parsed["address"]["city"];
    std::cout << "城市: " << city << std::endl;

    // 访问数组
    for (int score : parsed["scores"]) {
        std::cout << "成绩: " << score << std::endl;
    }

    return 0;
}

详细用法

自定义型序列化

#include <nlohmann/json.hpp>
#include <string>
#include <vector>

using json = nlohmann::json;

// 定义数据结构
struct Address {
    std::string city;
    std::string district;
    std::string street;
};

struct Person {
    std::string name;
    int age;
    Address address;
    std::vector<std::string> hobbies;
};

// 为自定义类型提供 to_json 和 from_json 函数
void to_json(json& j, const Address& addr) {
    j = json{
        {"city", addr.city},
        {"district", addr.district},
        {"street", addr.street}
    };
}

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

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

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("hobbies").get_to(p.hobbies);
}

// 使用
void customTypeDemo() {
    Person person{
        .name = "张三",
        .age = 25,
        .address = {"北京", "海淀", "中关村大街"},
        .hobbies = {"编程", "阅读", "游泳"}
    };

    // 序列化
    json j = person;
    std::string jsonStr = j.dump(4);

    // 反序列化
    Person parsed = json::parse(jsonStr).get<Person>();
}

安全地访问 JSON 字段

void safeAccessDemo() {
    json data = json::parse(R"({
        "name": "张三",
        "age": 25,
        "scores": [95, 87, 92]
    })");

    // 方式一:直接访问(如果字段不存在会抛异常)
    try {
        std::string name = data.at("name");
    } catch (const json::out_of_range& e) {
        std::cerr << "字段不存在: " << e.what() << std::endl;
    }

    // 方式二:使用 value 方法,提供默认值
    std::string name = data.value("name", "未知");
    std::string email = data.value("email", "无邮箱");  // 字段不存在时返回默认值
    int age = data.value("age", 0);

    // 方式三:检查字段是否存在
    if (data.contains("scores")) {
        for (int score : data["scores"]) {
            std::cout << score << " ";
        }
    }

    // 方式四:检查字段类型
    if (data["age"].is_number_integer()) {
        int age = data["age"];
    }

    // 方式五:使用 count 检查
    if (data.count("name") > 0) {
        std::cout << "name 字段存在" << std::endl;
    }
}

从文件读写 JSON

#include <fstream>

// 从文件读取 JSON
json readJsonFile(const std::string& path) {
    std::ifstream file(path);
    if (!file.is_open()) {
        throw std::runtime_error("无法打开文件: " + path);
    }

    try {
        return json::parse(file);
    } catch (const json::parse_error& e) {
        throw std::runtime_error("JSON 解析错误: " + std::string(e.what()));
    }
}

// 写入 JSON 到文件
void writeJsonFile(const std::string& path, const json& data) {
    std::ofstream file(path);
    if (!file.is_open()) {
        throw std::runtime_error("无法创建文件: " + path);
    }
    file << data.dump(4);  // 4 空格缩进,方便阅读
}

// 使用
void fileDemo() {
    // 写入
    json config = {
        {"database", {
            {"host", "localhost"},
            {"port", 5432},
            {"name", "mydb"}
        }},
        {"server", {
            {"port", 8080},
            {"debug", true}
        }}
    };
    writeJsonFile("config.json", config);

    // 读取
    json loaded = readJsonFile("config.json");
    std::string dbHost = loaded["database"]["host"];
    int dbPort = loaded["database"]["port"];
}

使用 protobuf 二进制序列化

# 安装 protobuf
# Ubuntu: sudo apt install protobuf-compiler libprotobuf-dev
# Windows: vcpkg install protobuf

定义数据结构(.proto 文件):

// person.proto
syntax = "proto3";

message Address {
    string city = 1;
    string district = 2;
    string street = 3;
}

message Person {
    string name = 1;
    int32 age = 2;
    Address address = 3;
    repeated string hobbies = 4;  // 列表
}

生成 C++ 代码:

protoc --cpp_out=. person.proto
# 生成 person.pb.h 和 person.pb.cc

使用生成的代码:

#include "person.pb.h"
#include <fstream>
#include <iostream>

void protobufDemo() {
    // 验证库版本
    GOOGLE_PROTOBUF_VERIFY_VERSION;

    // 创建 Person 对象
    Person person;
    person.set_name("张三");
    person.set_age(25);

    // 设置嵌套消息
    Address* address = person.mutable_address();
    address->set_city("北京");
    address->set_district("海淀");
    address->set_street("中关村大街");

    // 添加重复字段
    person.add_hobbies("编程");
    person.add_hobbies("阅读");
    person.add_hobbies("游泳");

    // 序列化为字符串
    std::string serialized;
    person.SerializeToString(&serialized);
    std::cout << "序列化大小: " << serialized.size() << " 字节" << std::endl;

    // 反序列化
    Person parsed;
    parsed.ParseFromString(serialized);
    std::cout << "姓名: " << parsed.name() << std::endl;
    std::cout << "年龄: " << parsed.age() << std::endl;
    std::cout << "城市: " << parsed.address().city() << std::endl;

    // 序列化到文件
    std::ofstream output("person.bin", std::ios::binary);
    person.SerializeToOstream(&output);
    output.close();

    // 从文件反序列化
    Person fromFile;
    std::ifstream input("person.bin", std::ios::binary);
    fromFile.ParseFromIstream(&input);
    input.close();

    // 释放 protobuf 库资源
    google::protobuf::ShutdownProtobufLibrary();
}

使用 cereal 轻量级序列化

#include <cereal/cereal.hpp>
#include <cereal/archives/json.hpp>
#include <cereal/archives/binary.hpp>
#include <cereal/types/string.hpp>
#include <cereal/types/vector.hpp>
#include <sstream>
#include <iostream>

struct Student {
    std::string name;
    int age;
    std::vector<double> scores;

    // cereal 序列化函数
    template<typename Archive>
    void serialize(Archive& archive) {
        archive(
            CEREAL_NVP(name),     // NVP 表示 Name-Value Pair
            CEREAL_NVP(age),
            CEREAL_NVP(scores)
        );
    }
};

void cerealDemo() {
    Student student{"李四", 20, {88.5, 92.0, 95.5}};

    // JSON 序列化
    std::ostringstream jsonOs;
    {
        cereal::JSONOutputArchive archive(jsonOs);
        archive(CEREAL_NVP(student));
    }
    std::cout << "JSON: " << jsonOs.str() << std::endl;

    // JSON 反序列化
    Student loaded;
    std::istringstream jsonIs(jsonOs.str());
    {
        cereal::JSONInputArchive archive(jsonIs);
        archive(CEREAL_NVP(loaded));
    }

    // 二进制序列化(更紧凑)
    std::ostringstream binOs;
    {
        cereal::BinaryOutputArchive archive(binOs);
        archive(student);
    }
    std::cout << "二进制大小: " << binOs.str().size() << " 字节" << std::endl;
}

常见场景

配置文件管理

#include <nlohmann/json.hpp>
#include <fstream>
#include <iostream>

using json = nlohmann::json;

class Config {
public:
    struct Database {
        std::string host;
        int port;
        std::string name;
        std::string user;
        std::string password;
    };

    struct Server {
        int port;
        bool debug;
        int maxConnections;
    };

    Database database;
    Server server;

    // 从文件加载配置
    static Config load(const std::string& path) {
        Config config;
        std::ifstream file(path);
        if (!file.is_open()) {
            // 配置文件不存在,使用默认值
            config = defaultConfig();
            config.save(path);
            return config;
        }

        json data = json::parse(file);

        // 安全地读取配置,提供默认值
        config.database.host = data.value("/database/host"_json_pointer, "localhost");
        config.database.port = data.value("/database/port"_json_pointer, 5432);
        config.database.name = data.value("/database/name"_json_pointer, "mydb");
        config.database.user = data.value("/database/user"_json_pointer, "postgres");
        config.database.password = data.value("/database/password"_json_pointer, "");

        config.server.port = data.value("/server/port"_json_pointer, 8080);
        config.server.debug = data.value("/server/debug"_json_pointer, false);
        config.server.maxConnections = data.value("/server/maxConnections"_json_pointer, 100);

        return config;
    }

    // 保存配置到文件
    void save(const std::string& path) const {
        json data = {
            {"database", {
                {"host", database.host},
                {"port", database.port},
                {"name", database.name},
                {"user", database.user},
                {"password", database.password}
            }},
            {"server", {
                {"port", server.port},
                {"debug", server.debug},
                {"maxConnections", server.maxConnections}
            }}
        };

        std::ofstream file(path);
        file << data.dump(4);
    }

private:
    static Config defaultConfig() {
        return Config{
            .database = {"localhost", 5432, "mydb", "postgres", ""},
            .server = {8080, false, 100}
        };
    }
};

注意事项

JSON 的性能:JSON 解析和序列化比二进制格式慢很多。如果性能是首要考虑,使用 protobuf 或 cereal 的二进制格式。

数值精度:JSON 中的数字可能丢失精度。大整数和浮点数在 JSON 中可能无法精确表示。对于精确数值,使用字符串存储。

安全问题:不要信任来自外部的 JSON 数据。验证所有字段的型和范围,避免 JSON 注入攻击。

protobuf 的代码生成:protobuf 需要额外的代码生成步骤,增加了构建复杂度。但生成的代码型安全,性能优秀。

版本兼容:当数据结构变化时,JSON 需要手动处理缺失字段(使用默认值),protobuf 通过字段编号自动处理。

进阶用法

JSON Schema 验证

#include <nlohmann/json.hpp>
#include <nlohmann/json-schema.hpp>

using json = nlohmann::json;

// 定义 JSON Schema 来验证数据格式
void validateJsonSchema() {
    // 定义 Schema
    json schema = R"({
        "type": "object",
        "required": ["name", "age"],
        "properties": {
            "name": {"type": "string", "minLength": 1},
            "age": {"type": "integer", "minimum": 0, "maximum": 150},
            "email": {"type": "string", "format": "email"}
        }
    })"_json;

    // 验证数据
    json validData = {{"name", "张三"}, {"age", 25}};
    // 使用 json-schema-validator 库验证
    // 如果数据不符合 Schema,会抛出异常
}

自定义序列化格式

// 为枚举类型提供自定义序列化
enum class Status {
    Active,
    Inactive,
    Pending
};

void to_json(json& j, Status s) {
    switch (s) {
        case Status::Active:   j = "active"; break;
        case Status::Inactive: j = "inactive"; break;
        case Status::Pending:  j = "pending"; break;
    }
}

void from_json(const json& j, Status& s) {
    std::string str = j;
    if (str == "active") s = Status::Active;
    else if (str == "inactive") s = Status::Inactive;
    else if (str == "pending") s = Status::Pending;
    else throw std::runtime_error("未知状态: " + str);
}