c/c++:序列化和反序列化的方式

一、目标

c/或c++如何进行序列化和反序列化?

二、序列化是什么

序列化:将一个内存对象(如结构体)转化成字节数组

反序列化:将字节数组还原成内存对象

目的:本地文件保存或网络传输

总结:对象和字节流的转化

三、方式

c(自己实现)

简易结构体

不考虑复杂场景的简易版本

#include <iostream>
#include <vector>
#include <cstring> // For memcpy

// 定义一个简单的结构体
struct MyStruct
{
    int x;
    double y;
    char name[10]; // 固定长度的字符数组

    MyStruct(int xVal, double yVal, const std::string &str)
        : x(xVal), y(yVal)
    {
        // 确保字符数组足够大来存放字符串
        size_t len = std::min(str.size(), sizeof(name) - 1);
        std::memcpy(name, str.c_str(), len);
        name[len] = '\0'; // 确保字符串以 '\0' 结尾
    }
};

// 序列化函数:将结构体转化为 std::vector<unsigned char>
std::vector<unsigned char> serialize(const MyStruct &obj)
{
    // 获取结构体的内存大小
    size_t size = sizeof(obj);
    std::vector<unsigned char> data(size);

    // 将结构体的内存拷贝到字节数组中
    std::memcpy(data.data(), &obj, size);

    return data;
}

// 反序列化函数:将 std::vector<unsigned char> 转化为结构体
MyStruct deserialize(const std::vector<unsigned char> &data)
{
    MyStruct obj(0, 0.0, "");

    // 将字节数组的内容拷贝回结构体
    std::memcpy(&obj, data.data(), sizeof(obj));

    return obj;
}

int main()
{
    // 创建一个结构体实例
    MyStruct original(42, 3.14, "Boost");

    // 序列化
    std::vector<unsigned char> serializedData = serialize(original);

    // 输出序列化后的字节内容
    std::cout << "Serialized Data: ";
    for (unsigned char byte : serializedData)
    {
        std::cout << static_cast<int>(byte) << " "; // 打印字节值
    }
    std::cout << std::endl;

    // 反序列化
    MyStruct deserialized = deserialize(serializedData);

    // 输出反序列化后的结构体内容
    std::cout << "Deserialized Struct:" << std::endl;
    std::cout << "x: " << deserialized.x << std::endl;
    std::cout << "y: " << deserialized.y << std::endl;
    std::cout << "name: " << deserialized.name << std::endl;

    return 0;
}

// g++ main.cpp
// ./a.out

输出

Serialized Data: 42 0 0 0 40 127 0 0 31 133 235 81 184 30 9 64 66 111 111 115 116 0 0 0 136 142 162 203 40 127 0 0 
Deserialized Struct:
x: 42
y: 3.14
name: Boost

结构体中存在可变部分

结构示例:

struct MyStruct 
{
    int size;          // 固定部分
    char data[0];      // 柔性数组成员,表示可变大小的数据部分
};
  • data[0] 只是一个占位符,表示后续有可变长度的数组。它并不分配任何内存,实际的数组大小是在运行时动态分配的。
  • size 用来表示数据部分的大小(通常用于序列化时跟踪实际数据的长度)。

基本使用方式:

#include <iostream>
#include <cstring>
#include <stdexcept>

struct MyStruct
{
    int size;     // 固定部分,表示 data 的大小
    char data[0]; // 柔性数组成员,数据部分的长度是可变的

    // 构造函数,用于设置数据
    MyStruct(int s, const std::string &str) : size(s)
    {
        // 使用 malloc 分配内存给结构体及其柔性数组部分
        char *data_ptr = reinterpret_cast<char *>(this + 1); // 获取结构体后面的内存

        // 拷贝数据到柔性数组部分
        std::memcpy(data_ptr, str.c_str(), size);
    }

    // 获取 data 部分的指针
    char *get_data()
    {
        return reinterpret_cast<char *>(this + 1);
    }
};

int main()
{
    {
        // 创建一个结构体实例
        std::string str = "Hello, world!";
        MyStruct *original = reinterpret_cast<MyStruct *>(malloc(sizeof(MyStruct) + str.size()));

        // 使用构造函数初始化
        new (original) MyStruct(str.size(), str);

        // 输出结构体内容
        std::cout << "size: " << original->size << std::endl;
        std::cout << "data: " << std::string(original->get_data(), original->size) << std::endl;

        // 释放内存
        free(original);
    }

    {
        // 使用强制转换在初始的时候指定好内存
        unsigned char input[7] = {1, 0, 0, 0, 0x61, 0x62, 0x63};
        MyStruct *original  = (MyStruct *)(void *)input;
        std::cout << "size: " << original->size << std::endl;
        std::cout << "data: " << std::string(original->get_data(), original->size) << std::endl;
    }

    return 0;
}

// 第13-17行为什么会产生段错误?
// this 是指向当前对象的指针。对于结构体 MyStruct 来说,this 指向的是 MyStruct 的第一个字节。
// this + 1 是在指针上加 1,相当于跳过了结构体的第一个字节(即 size 字段),指向了结构体之后的内存区域。
// 问题的关键在于:在结构体中,char data[0] 并不分配任何实际的内存,它只是一个“占位符”,用来表示结构体后面有一个可变大小的数组部分。在声明时,data 只是一个没有大小的空数组。因此,结构体的内存布局并不会为 data 部分分配任何空间。结构体本身的内存只包含 int size 这部分内容,而 data[0] 并没有为其分配内存。
// 当你通过 reinterpret_cast<char*>(this + 1) 来尝试访问 data 部分时,实际上你并没有为 data 部分分配任何内存空间,导致指向的内存位置是未定义的,或者你访问的是非法的内存区域。这会导致段错误(访问冲突)。
// 如何解决?
// 为了正确地为柔性数组部分分配内存,我们必须确保在结构体创建时,给柔性数组分配足够的内存。
// 因为 char data[0] 本身不分配内存,我们可以通过 malloc 或 new 来为结构体分配内存,并确保 data 部分有足够的空间来存储数据。

输出:

size: 13
data: Hello, world!
size: 1
data: a

序列化与反序列化:

#include <iostream>
#include <vector>
#include <cstring> // For memcpy

// 定义结构体,包含柔性数组成员
struct MyStruct
{
    int size;     // 固定部分,表示 data 的大小
    char data[0]; // 柔性数组成员,数据部分的长度是可变的

    // 构造函数,用于设置数据
    MyStruct(int s, const std::string &str) : size(s)
    {
        // 使用 malloc 分配内存给柔性数组部分
        // 这里是分配总大小(固定部分 + 数据部分的大小)
        // 注意,此部分会造成段错误
        char *data_ptr = reinterpret_cast<char*>(this + 1); // 获取结构体后面的内存
        std::memcpy(data_ptr, str.c_str(), size); // 拷贝数据到柔性数组部分
    }
};

// 序列化函数:将结构体转化为 std::vector<unsigned char>
std::vector<unsigned char> serialize(const MyStruct *obj)
{
    size_t total_size = sizeof(int) + obj->size; // 固定部分 + 数据部分的大小
    std::vector<unsigned char> data(total_size);

    // 将整个结构体的内容序列化到字节数组中
    std::memcpy(data.data(), &obj->size, sizeof(int));           // 先拷贝固定部分
    std::memcpy(data.data() + sizeof(int), obj->data, obj->size); // 然后拷贝数据部分

    return data;
}

// 反序列化函数:将 std::vector<unsigned char> 转化为结构体
MyStruct* deserialize(const std::vector<unsigned char> &data)
{
    // 先读取 size(即 data 部分的长度)
    int size;
    std::memcpy(&size, data.data(), sizeof(int));

    // 计算整个结构体的大小
    size_t total_size = sizeof(int) + size; // 固定部分 + 数据部分的大小

    // 使用 malloc 分配内存
    MyStruct *obj = (MyStruct *)malloc(total_size); // 分配内存,包含 size 字段和 data 部分

    if (!obj)
    {
        throw std::bad_alloc();
    }

    // 将数据拷贝到结构体中
    std::memcpy(&obj->size, data.data(), sizeof(int));       // 拷贝 size
    std::memcpy(obj->data, data.data() + sizeof(int), size); // 拷贝 data 部分

    return obj; // 返回新分配的结构体指针
}

int main()
{
    // 使用指针创建实例:一开始指定好data[0]的内存分配
    {
        // 创建一个结构体实例
        std::string str = "Hello, world!";
        MyStruct *original = reinterpret_cast<MyStruct *>(malloc(sizeof(MyStruct) + str.size()));

        // 使用构造函数初始化
        new (original) MyStruct(str.size(), str);

         // 序列化
        std::vector<unsigned char> serializedData = serialize(original);

        // 输出序列化后的字节内容
        std::cout << "Serialized Data: ";
        for (unsigned char byte : serializedData)
        {
            std::cout << static_cast<int>(byte) << " "; // 打印字节值
        }
        std::cout << std::endl;

        // 反序列化
        MyStruct *deserialized = deserialize(serializedData);

        // 输出反序列化后的结构体内容
        std::cout << "Deserialized Struct:" << std::endl;
        std::cout << "size: " << deserialized->size << std::endl;
        std::cout << "data: " << std::string(deserialized->data, deserialized->size) << std::endl;

        // 释放内存
        free(deserialized);
    }


    // 使用强制转换已经存在的内存空间:一开始指定好data[0]的内存分配
    {
        // 使用强制转换在初始的时候指定好内存
        unsigned char input[17] = {13, 00, 00, 00, 72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33};
        MyStruct *original  = (MyStruct *)(void *)input;

         // 序列化
        std::vector<unsigned char> serializedData = serialize(original);

        // 输出序列化后的字节内容
        std::cout << "Serialized Data: ";
        for (unsigned char byte : serializedData)
        {
            std::cout << static_cast<int>(byte) << " "; // 打印字节值
        }
        std::cout << std::endl;

        // 反序列化
        MyStruct *deserialized = deserialize(serializedData);

        // 输出反序列化后的结构体内容
        std::cout << "Deserialized Struct:" << std::endl;
        std::cout << "size: " << deserialized->size << std::endl;
        std::cout << "data: " << std::string(deserialized->data, deserialized->size) << std::endl;

        // 释放内存
        free(deserialized);
    }

    return 0;
}

输出:

Serialized Data: 13 0 0 0 72 101 108 108 111 44 32 119 111 114 108 100 33 
Deserialized Struct:
size: 13
data: Hello, world!
Serialized Data: 13 0 0 0 72 101 108 108 111 44 32 119 111 114 108 100 33 
Deserialized Struct:
size: 13
data: Hello, world!

c++ (用库)

对比:https://code.google.com/archive/p/plumgo/wikis/HSNNS_ObjectSerialization.wiki

Google Protocol Buffers(protobuf)

官方文档:https://protobuf.dev/overview/

使用过:grpc、谷歌云

使用:

(1)安装

# 安装 protobuf 编译器和开发库
sudo apt-get update
sudo apt-get install -y protobuf-compiler libprotobuf-dev

(2)定义 .proto 文件:定义序列化结构体

syntax = "proto3";

message MyStruct {
  int32 x = 1;
  double y = 2;
  string name = 3;
}

(3)生成c++代码:使用 protoc 编译器将 .proto 文件生成 C++ 代码:

protoc --cpp_out=. example.proto

① 序列化到文件

(4)编写 C++ 代码进行序列化和反序列化

#include <iostream>
#include <fstream>
#include <string>
#include "example.pb.h"  // 由 protoc 生成的头文件

using namespace std;

int main()
{
    // 初始化 Protobuf 库
    GOOGLE_PROTOBUF_VERIFY_VERSION;

    // 创建 MyStruct 的实例并赋值
    MyStruct data;
    data.set_x(10);
    data.set_y(3.14);
    data.set_name("Boost");

    // 序列化到文件
    {
        fstream output("data.bin", ios::out | ios::trunc | ios::binary);
        if (!data.SerializeToOstream(&output)) {
            cerr << "Failed to write data." << endl;
            return -1;
        }
        cout << "Data serialized to data.bin" << endl;
    }

    // 反序列化从文件
    MyStruct new_data;
    {
        fstream input("data.bin", ios::in | ios::binary);
        if (!new_data.ParseFromIstream(&input)) {
            cerr << "Failed to read data." << endl;
            return -1;
        }
    }

    // 输出反序列化后的数据
    cout << "Deserialized MyStruct:" << endl;
    cout << "x: " << new_data.x() << endl;
    cout << "y: " << new_data.y() << endl;
    cout << "name: " << new_data.name() << endl;

    // 清理 Protobuf 库
    google::protobuf::ShutdownProtobufLibrary();

    return 0;
}

// g++ -std=c++11 -o main main.cpp example.pb.cc `pkg-config --cflags --libs protobuf`
// ./main

输出

Data serialized to data.bin
Deserialized MyStruct:
x: 10
y: 3.14
name: Boost

② 序列化到字符数组

#include <iostream>
#include <fstream>
#include <vector>
#include <google/protobuf/io/coded_stream.h>
#include "example.pb.h"

using namespace std;

int main()
{
    // 初始化 Protocol Buffers 库
    GOOGLE_PROTOBUF_VERIFY_VERSION;

    // 创建结构体并赋值
    MyStruct data;
    data.set_x(10);
    data.set_y(3.14);
    data.set_name("Boost\0Boost");

    // 序列化到字节数组(std::vector<uint8_t>)
    std::vector<uint8_t> serialized_data;
    {
        std::string serialized_string;
        if (data.SerializeToString(&serialized_string))
        {
            // 将字符串中的字节存储到 vector<uint8_t> 中
            serialized_data.assign(serialized_string.begin(), serialized_string.end());
        }
        else
        {
            cerr << "Failed to serialize data!" << endl;
            return -1;
        }
    }

    // 输出序列化后的数据
    cout << "Serialized data (in bytes): ";
    for (auto byte : serialized_data)
    {
        cout << static_cast<int>(byte) << " "; // 打印每个字节
    }
    cout << endl;

    // 反序列化回 MyStruct
    MyStruct new_data;
    if (new_data.ParseFromString(std::string(serialized_data.begin(), serialized_data.end())))
    {
        // 打印反序列化后的数据
        cout << "Deserialized MyStruct:" << endl;
        cout << "x: " << new_data.x() << endl;
        cout << "y: " << new_data.y() << endl;
        cout << "name: " << new_data.name() << endl;
    }
    else
    {
        cerr << "Failed to parse data!" << endl;
        return -1;
    }

    // 清理 Protobuf 库
    google::protobuf::ShutdownProtobufLibrary();

    return 0;
}

// g++ -std=c++11 -o main main.cpp example.pb.cc `pkg-config --cflags --libs protobuf`
// ./main

输出:

Serialized data (in bytes): 8 10 17 31 133 235 81 184 30 9 64 26 5 66 111 111 115 116 
Deserialized MyStruct:
x: 10
y: 3.14
name: Boost

Boost.Serializaticon

① 序列化到文件

#include <iostream>
#include <fstream>
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>

class Test
{
public:
    friend class boost::serialization::access;

    Test() {}

    Test(bool b, char c, int i, double d, std::string str)
        : m_bool(b), m_char(c), m_int(i), m_double(d), m_str(str) {}

    template <class Archive>
    void serialize(Archive &ar, const unsigned int version)
    {
        ar & m_bool;
        ar & m_char;
        ar & m_int;
        ar & m_double;
        ar & m_str;
    }

public:
    bool m_bool;
    char m_char;
    int m_int;
    double m_double;
    std::string m_str;
};

int main()
{
    Test test(true, 'm', 50, 17.89, "fuzhijie");

    // 序列化到文件
    std::ofstream ofs("testfile.txt");
    boost::archive::text_oarchive oa(ofs);
    oa << test;
    ofs.close();

    // 从文件反序列化
    Test new_test;
    std::ifstream ifs("testfile.txt");
    boost::archive::text_iarchive ia(ifs);
    ia >> new_test;
    ifs.close();

    // 输出反序列化后的对象状态
    std::cout << "Bool: " << new_test.m_bool << std::endl;
    std::cout << "Char: " << new_test.m_char << std::endl;
    std::cout << "Int: " << new_test.m_int << std::endl;
    std::cout << "Double: " << new_test.m_double << std::endl;
    std::cout << "String: " << new_test.m_str << std::endl;

    return 0;
}

// g++ main.cpp -I/home/work/boost/boost_1_86_0/ -L/home/work/boost/boost_1_86_0/stage/lib -lboost_serialization
// export LD_LIBRARY_PATH=/home/work/boost/boost_1_86_0/stage/lib:$LD_LIBRARY_PATH
// a.out

输出:

Bool: 1
Char: m
Int: 50
Double: 17.89
String: fuzhijie

② 序列化到字符数组

#include <iostream>
#include <boost/serialization/serialization.hpp>
#include <boost/serialization/vector.hpp>
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <sstream>
#include <vector>

using namespace std;

// 定义一个结构体
struct MyStruct
{
    int x;
    double y;
    std::string name;

    // Boost 序列化函数
    template <class Archive>
    void serialize(Archive &ar, const unsigned int version)
    {
        ar & x;
        ar & y;
        ar & name;
    }
};

int main()
{
    // 创建结构体实例
    MyStruct data;
    data.x = 10;
    data.y = 3.14;
    data.name = "Boost\0Boost";

    // 使用 stringstream 序列化到 stringstream
    std::stringstream ss;
    boost::archive::text_oarchive oa(ss);
    oa << data;

    // 将序列化数据存储到 std::vector<unsigned char> 中
    std::vector<unsigned char> serialized_data;
    char byte;
    while (ss.get(byte))
    {
        serialized_data.push_back(static_cast<unsigned char>(byte));
    }

    // 输出序列化后的数据(打印字节)
    cout << "Serialized data: ";
    for (size_t i = 0; i < serialized_data.size(); ++i)
    {
        cout << static_cast<int>(serialized_data[i]) << " "; // 打印每个字节的值
    }
    cout << endl;

    // 反序列化回结构体
    std::stringstream ss_in;
    for (unsigned char byte : serialized_data)
    {
        ss_in.put(byte); // 将 vector 数据写入 stringstream
    }

    MyStruct new_data;
    boost::archive::text_iarchive ia(ss_in);
    ia >> new_data;

    // 打印反序列化后的数据
    cout << "Deserialized MyStruct:" << endl;
    cout << "x: " << new_data.x << endl;
    cout << "y: " << new_data.y << endl;
    cout << "name: " << new_data.name << endl;

    return 0;
}

// g++ -std=c++11 main.cpp -I/home/work/boost/boost_1_86_0/ -L/home/work/boost/boost_1_86_0/stage/lib -lboost_serialization -lboost_system -lboost_filesystem
// export LD_LIBRARY_PATH=/home/work/boost/boost_1_86_0/stage/lib:$LD_LIBRARY_PATH
// a.out

输出:

Serialized data: 50 50 32 115 101 114 105 97 108 105 122 97 116 105 111 110 58 58 97 114 99 104 105 118 101 32 50 48 32 48 32 48 32 49 48 32 51 46 49 52 48 48 48 48 48 48 48 48 48 48 48 48 48 49 50 101 43 48 48 32 53 32 66 111 111 115 116 
Deserialized MyStruct:
x: 10
y: 3.14
name: Boost

四、总结

序列化的方式: protobuf 或 Boost.Serializaticon

比较

特性 Protocol Buffers (protobuf) Boost.Serialization
易用性 简单(通过 .proto 文件自动生成类) 灵活(需要手动定义序列化函数)
性能 高效,适合大规模数据 性能较低,灵活性较高
数据格式 二进制格式 支持二进制、XML、文本格式
跨语言支持 支持多种语言(C++, Java, Python 等) 主要是 C++,没有跨语言支持
兼容性与版本控制 支持向前和向后兼容,字段编号保证兼容性 手动管理,灵活但复杂
库大小与依赖 较小,少依赖 依赖 Boost 库,库较大
posted @ 2024-12-01 20:52  circlelll  阅读(207)  评论(0编辑  收藏  举报