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. 总结
将结构体序列化为二进制字节文件时,最容易产生错误的环节是计算变量的字节数,重点注意事项:
- 内存对齐规则
- 成员变量在内存中是否连续
- 系统架构是否一致(不同系统架构下基本数据类型的字节数可能不同)
因此,在设计序列化算法之前,请务必对结构体成员变量的内存布局有清晰的认识。