结构体序列化和反序列化

0. 背景

将内存中的数据持久化到硬盘上,即将某些对象的属性(类或结构体成员变量的值)等信息保存为文件,并在需要使用这些数据时加载文件中记录的信息,重新构造出所需对象。

  • 序列化 Serialize:对象(内存) ===> 文件(硬盘)
  • 反序列化 Deserialize:文件(硬盘) ===> 对象(内存)

1. 序列化方式

存储类别

  • 字节(二进制文件)
  • 字符(文本文件)

“简单”结构体序列化

这里的“简单”特指平凡数据 POD(Plain Old Data),具体特征是布局有序,即成员变量在内存上是连续的,可以直接使用memcpy()memset等函数操作数据。

“复杂”结构体序列化

这里的“复杂”特指成员变量在内存上不连续,如:成员变量同时包含基本数据类型和指针,数据分别存储在栈和堆内存上,因此无法通过memcpy()memset函数直接操作。

2. 代码实现

常规版

本节提供成员变量中包含vector的结构体的序列化和反序列化方式。

#include <iostream>
#include <fstream>
#include <vector>
#include <string.h>
using namespace std;

struct Point2f {
    float x, y;
};

struct Data {
    int a, b;
    vector<Point2f> points;
};

bool serialize(const string& filename, const Data& data) {
    ofstream out;
    out.open(filename, ios::binary);
    if (!out.is_open()) {
        cerr << "Open file failed." << endl;
        return false;
    }

    out.write((const char*)(&data), sizeof(int) * 2); // a, b
    size_t size = data.points.size();
    out.write((const char*)(&size), sizeof(size_t));                // points.size()
    out.write((const char*)(data.points.data()), size * sizeof(Point2f)); // points.data

    out.close();
    return true;
}

bool deserialize(const string& filename, Data& data) {
    ifstream in;
    in.open(filename, ios::binary);
    if (!in.is_open()) {
        cerr << "Open file failed." << endl;
        return false;
    }

    in.read((char*)(&data), sizeof(int) * 2); // a, b
    size_t size;
    in.read((char*)(&size), sizeof(size_t)); // points.size()
    data.points.resize(size);
    in.read((char*)(data.points.data()), size * sizeof(Point2f)); // points.data

    in.close();
    return true;
}

int main() {
    auto print = [](const Data& data) {
        cout << data.a << " " << data.b << " { ";
        for (const auto& p : data.points) {
            cout << "(" << p.x << ", " << p.y << ") ";
        }
        cout << "}" << endl;
    };

    Data d = { 1, 2, { { 1.1, 1.1 }, { 2.2, 2.2 } } };
    print(d);

    string filename = "data.bin";
    serialize(filename, d);
    
    Data new_d = {};
    print(new_d);
    
    deserialize(filename, new_d);
    print(new_d);
    
	return 0;
}

进阶版

本节提供嵌套vector的序列化和反序列化方式,同时考虑内存对齐情况。

在这个示例中,结构体内存对齐到8,其中的 int 变量会被额外填充4个字节,因此计算变量长度(字节数)时必须考虑这部分内存。

#include <iostream>
#include <fstream>
#include <vector>
using namespace std;

#pragma pack(8)
struct Point2f {
    float x, y;
};

struct Data {
    int a; // 内存对齐到8,int会被额外填充4个字节,此变量所占内存为8字节
    long int b;
    vector<Point2f> points;
};
#pragma unpack(pop)

bool serialize(const string& filename, const vector<Data>& vec) {
    ofstream out;
    out.open(filename, ios::binary);
    auto size = vec.size();
    out.write((const char*)(&size), sizeof(size));
    for (const auto& item : vec) {
        // out.write((const char*)(&item), sizeof(int) * 2);
        // 由于内存对齐到8,item.a的实际长度为8字节,sizeof(int)结果为4,盲目使用(如上)会出现偏移量计算错误
        // 为了规避内存对齐带来的风向,推荐使用以下写法:
        // 1. 逐变量序列化,且sizeof参数为实际变量;
        // 2. 如果存在指针,需要手动计算数据长度(字节数);
        // 3. 如果存在模板类型,则通过valie_type获取类型,并使用decltype帮助计算长度。
        out.write((const char*)(&item.a), sizeof(item.a));
        out.write((const char*)(&item.b), sizeof(item.b));
        size = item.points.size();
        out.write((const char*)(&size), sizeof(size));
        out.write((const char*)(item.points.data()), size * sizeof(decltype(item.points)::value_type));
    }
    out.close();
    return true;
}

bool deserialize(const string& filename, vector<Data>& vec) {
    ifstream in;
    in.open(filename, ios::binary);
    if (!in.is_open()) {
        cerr << "Open file failed." << endl;
        return false;
    }

	// Tips: 处理未知类型的变量时,可以使用decltype类型推断来让编译器自己判断类型,避免出错
    decltype(Data::points.size()) size;
    in.read((char*)(&size), sizeof(size));
    vec.resize(size);
    for (auto& item : vec) {
        in.read((char*)(&item.a), sizeof(item.a));
        in.read((char*)(&item.b), sizeof(item.b));
        in.read((char*)(&size), sizeof(size));
        cout << size << endl;
        item.points.resize(size);
        in.read((char*)(item.points.data()), size * sizeof(decltype(item.points)::value_type));
    }
    in.close();
    return true;
}

int main() {
    auto print = [](const vector<Data>& data) {
        for (const auto& item : data) {
            cout << item.a << ", " << item.b << ", { ";
            for (const auto& p : item.points) {
                cout << "(" << p.x << "," << p.y << ") ";
            }
            cout << "}" << endl;
        }
    };

    vector<Data> d = {
        { 1, 1, { { 1.0, 1.0 }, { 1.1, 1.1 } } },
        { 2, 2, { { 2.0, 2.0 }, { 2.1, 2.1 } } },
        { 3, 3, { { 3.0, 3.0 }, { 3.1, 3.1 }, { 3.2, 3.2 } } }
    };
    print(d);

    string filename = "data.bin";
    serialize(filename, d);

    d.clear();
    deserialize(filename, d);
    print(d);

    return 0;
}

3. 总结

将结构体序列化为二进制字节文件时,最容易产生错误的环节是计算变量的字节数,重点注意事项:

  • 内存对齐规则
  • 成员变量在内存中是否连续
  • 系统架构是否一致(不同系统架构下基本数据类型的字节数可能不同)

因此,在设计序列化算法之前,请务必对结构体成员变量的内存布局有清晰的认识。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值