开始之前
大多数新用户不熟悉 C++ 中的不同数据类型,并且经常在他们的代码中使用纯 int、char 等。要成功进行序列化,需要彻底考虑他们的数据types。因此,如果您有一个 int 躺在某个地方,这些是您的第一步。
了解您的数据
如果您有 struct 或 class 的某些数据为:
struct cat {
int weight = 0; // In kg (pounds)
int length = 0; // In cm (or feet)
std::string voice = "meow.mp3";
cat() {}
cat(int weight, int length): weight(weight), length(length) {}
}
你的猫真的可以重约 255 公斤(1 字节整数的最大大小)吗?它可以长到 255 厘米(2.5 米)吗?你的猫的voice会随着cat的每个对象而改变吗?
不改变的对象应该被声明为static,并且你应该限制你的对象大小以最适合你的需要。因此,在这些示例中,上述问题的答案是否。
所以我们的猫 struct 现在看起来像这样:
struct cat {
uint8_t weight = 0; // Non negative 8 bit (1 byte) integer (or unsigned char)
uint8_t length = 0; // Same for length
static std::string voice;
cat() {}
cat(uint8_t w, uint8_t l):weight(w), length(l) {}
};
static cat::voice = std::string("meow.mp3");
文件是逐字节写入的(通常作为字符集),您的数据可能会有所不同,因此您需要假定或限制您的数据可以处理的最大值。
但并非每个项目(或结构)都是相同的,所以让我们谈谈您的代码数据和二进制结构化数据的差异。在考虑序列化时,您需要以这种方式思考“这个结构需要唯一的最少数据是多少?”。
对于我们的猫对象,它可以表示除此之外的任何东西:
- 老虎:最大 390 公斤和 340 厘米
- 狮子:最大 315 公斤和 365 厘米
任何其他的都是合格的。因此,您可以根据大小和重量影响您的"meow.mp3",然后使猫与众不同的最重要数据是它的length 和weight。这些是我们需要保存到文件中的数据。
限制您的数据
世界上最大的动物园拥有 5000 只动物和 700 个物种,这意味着动物园中的每个物种平均每个物种包含大约 10 个种群。这意味着对于我们的 cat 物种,我们最多可以存储 1 字节的猫,并且不用担心它会超过它。
因此可以安全地假设我们的zoo 项目应该为每个物种保存多达 200 个元素。这给我们留下了两个不同字节大小的数据,因此我们的结构的序列化数据最多为两个字节。
序列化方法
构建我们的cat 块
对于初学者来说,this 是一个很好的开始方式。它可以帮助您在正确的基础上处理自定义序列化。现在剩下的就是定义结构化的二进制格式。为此,我们需要一种方法来识别我们的两个字节是属于 cat 还是其他结构的一部分,这可以通过相同类型的集合(每两个字节为 cats)或标识符来完成。
-
如果我们有一个包含所有猫的文件(或文件的一部分)。我们只需要文件的起始偏移量和cat字节的大小,然后从起始偏移量读取每两个字节读取一次,以获取所有猫。
-
Identifier 是一种我们可以根据起始字符来识别对象是猫还是其他东西的方法。这通常由 TLV(类型长度值)格式完成,其中类型是 Cats,长度是两个字节,值是这两个字节。
如您所见,第一个选项包含更少的字节,因此更紧凑,但使用第二个选项,我们可以在文件中存储多个动物并创建一个动物园。如何构建二进制文件很大程度上取决于您的项目。目前,由于“单个文件”选项是最合乎逻辑的,我将实施第二个。
关于“标识符”方法的最重要的一点是首先让它对我们来说是合乎逻辑的,然后再让它对我们的机器来说是合乎逻辑的。我来自一个从左到右阅读是常态的世界。所以顺理成章地,我想读的关于猫的第一件事是它的type,然后是length,然后是value。
char type = 'C'; // C shorten for Cat, 0x43
uint8_t length = 2; // It holds 2 bytes, 0x02
uint8_t c_length = '?'; // cats length
uint8_t c_weight = '?'; // cats weight
并将其表示为chunk(block);
+00 4B 43-02-LL-WW ('C\x02LW')
这意味着:
-
+00: 偏移量,0表示文件的开始
-
4B:我们数据块的大小,4字节。
-
43-02-LL-WW:猫的实际值
-
43:字符“C”的十六进制表示
-
02:这种类型长度的十六进制表示(2)
-
LL: 这只猫的长度为 1 字节值
-
WW: 这只猫的权重1字节值
但是由于我从左到右读取数据更容易,这意味着我的数据应该写成小端,而大多数独立计算机都是大端。
它们的重要性和重要性
这里的主要问题是我们机器的endianness,由于我们的struct/class 和字节序,我们需要一个基本类型。我们编写它的方式定义了一个小端操作系统,但操作系统可以是各种endianness,您可以了解如何找到您的机器有here。
对于使用bit 字段的用户,我强烈建议您使用它们。但对于不熟悉的用户:
#include <iostream> // Just for std::ostream, std::cout, and std::endl
bool is_big() {
union {
uint16_t w;
uint8_t p[2];
} p;
p.w = 0x0001;
return p.p[0] == 0x1;
}
union chunk {
uint32_t space;
uint8_t parts[4];
};
chunk make_chunk(uint32_t VAL) {
union chunk ret;
ret.space = VAL;
return ret;
}
std::ostream& operator<<(std::ostream& os, union chunk &c) {
if(is_big()) {
return os << c.parts[3] << c.parts[2] << c.parts[1] << c.parts[0];
}else {
return os << c.parts[0] << c.parts[1] << c.parts[2] << c.parts[3];
}
}
void read_as_binary(union chunk &t, uint32_t VAL) {
t.space = VAL;
if(is_big()) {
t.space = (t.parts[3] << 24) | (t.parts[2] << 16) | (t.parts[1] << 8) | t.parts[0];
}
}
void write_as_binary(union chunk t, uint32_t &VAL) {
if(is_big()) {
t.space = (t.parts[3] << 24) | (t.parts[2] << 16) | (t.parts[1] << 8) | t.parts[0];
}
VAL = t.space;
}
所以现在我们有了我们的块,它将按照我们一眼就能认出的顺序打印出字符。现在我们需要一组从uint32_t 到cat 的转换功能,因为我们的块大小是4 字节或uint32_t。
struct cat {
uint8_t weight = 0; // Non negative 8 bit (1 byte) integer (or unsigned char)
uint8_t length = 0; // The same for length
static std::string voice;
cat() {}
cat(uint8_t w, uint8_t l): weight(w), length(l) {}
cat(union chunk cat_chunk) {
if((cat_chunk.space & 0x43020000) == 0x43020000) {
this->length = cat_chunk.space & 0xff; // To circumvent the endianness bit shifts are best solution for that
this->weight = (cat_chunk.space >> 8) & 0xff;
}
// Some error handling
this->weight = 0;
this->length = 0;
}
operator uint32_t() {
return 0x4302000 | (this->weight << 8) | this->length;
}
};
static cat::voice = std::string("meow.mp3");
动物园文件结构
所以现在我们的cat 对象准备好从chunk 到cat 来回转换。现在我们需要用页眉、页脚、数据和校验和*构建整个文件。假设我们正在构建一个应用程序,用于在动物园设施之间跟踪显示它们有多少动物。我们动物园的数据是他们有什么动物和有多少,我们动物园的页脚可以省略(或者它可以表示文件创建时间的时间戳),并且在标题中我们保存了有关如何读取文件、版本控制和检查损坏的说明。
有关我如何构建这些文件的更多信息,您可以找到来源here 和this shameless plug。
// File structure: all little endian
------------
HEADER:
+00 4B 89-5A-4F-4F ('\221ZOO') Our magic number for the zoo file
+04 4B XX-XX-XX-XX ('????') Whole file checksum
+08 4B 47-0D-1A-0A ('\r\n\032\n') // CRLF <-> LF conversion and END OF FILE 032
+12 4B YY-YY-00-ZZ ('??\0?') Versioning and usage
+16 4B AA-BB-BB-BB ('X???') Start offset + data length
------------
DATA:
Animals: // For each animal type (block identifier)
+20+?? 4B ??-XX-XX-LL ('????') : ? animal type identifier, X start offset from header, Y animals in struct objects
+24+??+4 4B XX-XX-XX-XX ('????') : Checksum for animal type
对于校验和,您可以使用普通的(手动添加每个字节)或CRC-32。选择权在您手中,这取决于您的文件和数据的大小。所以现在我们有了文件的数据。当然,我必须警告你:
只有一个需要序列化的结构或类意味着通常不需要这种类型的序列化。您可以将整个对象转换为所需大小的整数,然后转换为二进制字符序列,然后将某个大小的字符序列读入整数并返回到对象。序列化的真正价值在于我们可以存储多个数据并在二进制混乱中找到自己的方式。
但由于动物园拥有的数据比我们拥有的动物多,因此数据块的大小可能会有所不同。我们需要创建一个interface 或abstract class 来处理文件。
#include <fstream> // File input output ...
#include <vector> // Collection for writing data
#include <sys/types.h> // Gets the types for struct stat
#include <sys/stat.h> // Struct stat
#include <string> // String manipulations
struct handle {
// Members
protected: // Inherited in private
std::string extn = "o";
bool acces = false;
struct stat buffer;
std::string filename = "";
std::vector<chunk> data;
public: // Inherited in public
std::string name = "genesis";
std::string path = "";
// Methods
protected:
void remake_name() {
this->filename = this->path;
if(this->filename != "") {
this->filename.append("//");
}
this->filename.append(this->name);
this->filename.append(".");
this->filename.append(this->extn);
}
void recheck() {
this->acces = (
stat(
this->filename.c_str(),
&this->buffer
) == 0);
}
// To be overwritten later on [override]
virtual bool check_header() { return true;}
virtual bool check_footer() { return true;}
virtual bool load_header() { return true;}
virtual bool load_footer() { return true;}
public:
handle()
: acces(false),
name("genesis"),
extn("o"),
filename(""),
path(""),
data(0) {}
void operator()(const char *name, const char *ext, const char *path) {
this->path = std::string(path);
this->name = std::string(name);
this->extn = std::string(ext);
this->remake_name();
this->recheck();
}
void set_prefix(const char *prefix) {
std::string prn(prefix);
prn.append(this->name);
this->name = prn;
this->remake_name();
}
void set_suffix(const char *suffix) {
this->name.append(suffix);
this->remake_name();
}
int write() {
this->remake_name();
this->recheck();
if(!this->load_header()) {return 0;}
if(!this->load_footer()) {return 0;}
std::fstream file(this->filename.c_str(), std::ios::out | std::ios::binary);
uint32_t temp = 0;
for(int i = 0; i < this->data.size(); i++) {
write_as_binary(this->data[i], temp);
file.write((char *)(&temp), sizeof(temp));
}
if(!this->check_header()) { file.close();return 0; }
if(!this->check_footer()) { file.close();return 0; }
file.close();
return 1;
}
int read() {
this->remake_name();
this->recheck();
if(!this->acces) {return 0;}
std::fstream file(this->filename.c_str(), std::ios::in | std::ios::binary);
uint32_t temp = 0;
chunk ctemp;
size_t fsize = this->buffer.st_size/4;
for(int i = 0; i < fsize; i++) {
file.read((char*)(&temp), sizeof(temp));
read_as_binary(ctemp, temp);
this->data.push_back(ctemp);
}
if(!this->check_header()) {
file.close();
this->data.clear();
return 0;
}
if(!this->check_footer()) {
file.close();
this->data.clear();
return 0;
}
return 1;
}
// Friends
friend std::ostream& operator<<(std::ostream& os, const handle& hand);
friend handle& operator<<(handle& hand, chunk& c);
friend handle& operator>>(handle& hand, chunk& c);
friend struct zoo_file;
};
std::ostream& operator<<(std::ostream& os, const handle& hand) {
for(int i = 0; i < hand.data.size(); i++) {
os << "\t" << hand.data[i] << "\n";
}
return os;
}
handle& operator<<(handle& hand, chunk& c) {
hand.data.push_back(c);
return hand;
}
handle& operator>>(handle& hand, chunk& c) {
c = hand.data[ hand.data.size() - 1 ];
hand.data.pop_back();
return hand;
}
我们可以从中初始化我们的zoo 对象,然后再初始化我们需要的对象。文件 handle 只是一个文件模板,其中包含一个数据块 (handle.data) 和 headers 和/在稍后实现 footers。
由于标题描述了整个文件,因此检查和加载可以增加您特定案例所需的功能。如果您有两个不同的对象,则需要添加到文件中,而不是更改页眉/页脚,一种类型的数据 insert 在数据的开头,另一种类型 push_back 在数据的末尾通过@987654382 @operator<</operator>>.
对于相互之间没有关系的多个对象,您可以在继承中添加更多私有成员,用于存储各个段的当前位置,同时保持文件读写的整洁和组织。
struct zoo_file: public handle {
zoo_file() {this->extn = "zoo";}
void operator()(const char *name, const char *path) {
this->path = std::string(path);
this->name = std::string(name);
this->remake_name();
this->recheck();
}
protected:
virtual bool check_header() {
chunk temp = this->data[0];
uint32_t checksums = 0;
// Magic number
if(chunk.space != 0x895A4F4F) {
this->data.clear();
return false;
}else {
this->data.erase(this->data.begin());
}
// Checksum
temp = this->data[0];
checksums = temp.space;
this->data.erase(this->data.begin());
// Valid load number
temp = this->data[0];
if(chunk.space != 0x470D1A0A) {
this->data.clear();
return false;
}else {
this->data.erase(this->data.begin());
}
// Version + flag
temp = this->data[0];
if((chunk.space & 0x01000000) != 0x01000000) { // If not version 1.0
this->data.clear();
return false;
}else {
this->data.erase(this->data.begin());
}
temp = this->data[0];
int opt_size = (temp.space >> 24);
if(opt_size != 20) {
this->data.clear();
return false;
}
opt_size = temp.space & 0xffffff;
return (opt_size == this->data.size());
}
virtual bool load_header() {
chunk magic, checksum, vload, ver_flag, off_data;
magic = 0x895A4F4F;
checksum = 0;
vload = 0x470D1A0A;
ver_flag = 0x01000001; // 1.0, usage 1 (normal)
off_data = (20 << 24) | ((this->data.size()-1)-4);
for(int i = 0; i < this->data.size(); i++) {
checksum.space += this->data[i].parts[0];
checksum.space += this->data[i].parts[1];
checksum.space += this->data[i].parts[2];
checksum.space += this->data[i].parts[3];
}
this->data.insert(this->data.begin(), off_data);
this->data.insert(this->data.begin(), ver_flag);
this->data.insert(this->data.begin(), vload);
this->data.insert(this->data.begin(), checksum);
this->data.insert(this->data.begin(), magic);
return true;
}
friend zoo_file& operator<<(zoo_file& zf, cat sc);
friend zoo_file& operator>>(zoo_file& zf, cat sc);
friend zoo_file& operator<<(zoo_file& zf, elephant se);
friend zoo_file& operator>>(zoo_file& zf, elephant se);
};
zoo_file& operator<<(zoo_file& zf, cat &sc) {
union chunk temp;
temp = (uint32_t)sc;
zf.data.push_back(temp);
return zf;
}
zoo_file& operator>>(zoo_file& zf, cat &sc) {
size_t pos = zf.data.size() - 1;
union chunk temp;
while (1) {
if((zf[pos].space & 0x4302000) != 0x4302000) {
pos --;
}else {
temp = zf[pos];
break;
}
if(pos == 0) {break;}
}
zf.data.erase(zf.data.begin() + pos);
sc = (uint32_t)temp;
return zf;
}
// same for elephants, koyotes, giraffes .... whatever you need
请不要只是复制代码。句柄对象用作模板,因此如何构建数据块取决于您自己。如果你有不同的结构并且只是复制代码当然是行不通的。
现在我们可以拥有只有猫的动物园了。构建文件很简单:
// All necessary includes
// Writing the zoo file
zoo_file my_zoo;
// Push back to the std::vector some cats in
my_zoo("superb_zoo");
my_zoo.write();
// Reading the zoo file
zoo_file my_zoo;
my_zoo("superb_zoo");
my_zoo.read();