如何使用自定义对象的序列化在 c++ 中编写自定义二进制文件处理程序



我有一些结构,我想序列化和反序列化,以便能够将它们从一个程序传递到另一个程序(作为保存文件),并被其他程序操作(进行微小的更改......

我已经通读了:

  • 描述 isocpp 序列化说明的文档
  • 显示如何阅读块的 SO 问题
  • SO问题如何读取和写入二进制文件
  • 对不同的文件处理程序进行基准测试,提高速度和依赖性
  • 序列化"介绍">

但是我没有找到任何地方如何将这一步从具有一些classstruct传递到序列化结构,然后您可以读取,写入,操作......无论是单数(每个文件 1 个结构)还是顺序(每个文件多个结构类型的多个列表)。

如何用自定义对象的序列化在c ++中编写自定义二进制文件处理程序?

在我们开始之前

大多数新用户不熟悉C++中的不同数据类型,并且经常在代码中使用普通intchar等。要成功进行序列化,需要彻底考虑它们的数据类型。因此,如果你有int躺在某个地方,这些是你的第一步。

  • 了解您的数据
    • 变量应保持的最大值是多少?
    • 会是负面的吗?
  • 限制您的数据
    • 从上面实施决策
    • 限制文件可以容纳的对象数量。

了解您的数据

如果您有structclass包含某些数据:

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");

文件是逐字节写入的(通常作为字符集),您的数据可能会有所不同,因此您需要假定限制数据可以处理的最大值。

但并非每个项目(或结构)都是一样的,所以让我们谈谈代码数据和二进制结构化数据的差异。在考虑序列化时,您需要以这种方式考虑"此结构需要唯一的最小数据是多少?

对于我们的 cat 对象,它可以表示以下任何内容:

  • 老虎:最大 390 公斤和 340 厘米
  • 狮子:最大315公斤和365厘米

其他任何东西都符合条件。因此,您可以根据大小和体重影响您的"meow.mp3",然后使猫与众不同的最重要的数据是它的lengthweight。这些是我们需要保存到文件中的数据。

限制您的数据

世界上最大的动物园拥有 5000 只动物和 700 个物种,这意味着动物园中的每个物种平均每个物种包含大约 10 个种群。这意味着根据我们的cat种类,我们最多可以存储 1 字节的猫,并且不必担心它会超过它。

因此,可以安全地假设我们的zoo项目应该为每个物种容纳多达200个元素。这给我们留下了两个不同的字节大小的数据,因此我们的结构的序列化数据最多为两个字节。

>序列化方法

构建我们的cat

对于初学者来说,这是开始的好方法。它可以帮助您在正确的基础上进行自定义序列化。现在剩下的就是定义一个结构化的二进制格式。为此,我们需要一种方法来识别我们的两个字节是 cat 还是其他结构的一部分,这可以通过相同类型的集合(每两个字节是cats)或通过标识符来完成。

  1. 如果我们有一个文件(或文件的一部分)来容纳所有猫。我们只需要文件的起始偏移量和cat字节的大小,然后从开始偏移量读取每两个字节读取一次,以获得所有 cats。

  2. 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

并将其表示为块(块);

+00        4B        43-02-LL-WW        ('Cx02LW')

这意味着:

  • +00:偏移量从开始,0表示它是文件的开始
  • 4B:我们数据块的大小,4 字节。
  • 43-02-LL-WW:猫的实际价值
    • 43:字符"C"的十六进制表示
    • 02:此类型长度的十六进制表示 (2)
    • LL:这只猫的长度为1个字节值
    • WW:这只猫的权重为1字节值

但是由于我更容易从左到右读取数据,这意味着我的数据应该写为小端序,并且大多数独立计算机都是大端序。

它们的吸引力和重要性

这里的主要问题是我们机器的字节序,由于我们的struct/class和字节序,我们需要一个基本类型。我们编写它的方式定义了一个小端序操作系统,但操作系统可以是各种字节序,你可以在这里找到如何找到你的机器。

对于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_tcat的转换功能,因为我们的块大小是 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对象,可以从chunkcat来回铸造。现在我们需要用页眉、页脚、数据和校验和*构建一个完整的文件。假设我们正在构建一个应用程序,用于跟踪动物园设施之间,显示他们有多少动物。我们动物园的数据是他们拥有的动物和数量,我们动物园的页脚可以省略(或者它可以表示创建文件时的时间戳),并在标题中保存有关如何读取文件的说明,版本控制和检查损坏。

有关我如何构建这些文件的更多信息,您可以在此处找到资源和这个无耻的插件。

// 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   ('rn32n') // CRLF <-> LF conversion and END OF FILE 032
+12    4B    YY-YY-00-ZZ   ('???')     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等。选择权在您手中,这取决于您的文件和数据的大小。所以现在我们有文件的数据。当然,我必须警告你

只有一个需要序列化的结构或类意味着通常不需要这种类型的序列化。您可以只将整个对象强制转换为所需大小的整数,然后转换为二进制字符序列,然后将该大小的字符序列读入整数并读回对象。序列化的真正价值在于我们可以存储多个数据,并在二进制混乱中找到自己的方式。

但是,由于动物园可以拥有比我们拥有的动物更多的数据,因此其大小可能会有所不同。我们需要为文件处理制定interfaceabstract 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)和页眉和/以后实现的页脚

由于标头描述整个文件,因此检查和加载可以具有特定案例所需的附加功能。如果您有两个不同的对象,则需要添加到文件中,而不是更改页眉/页脚,一种类型的数据insert在数据的开头,另一种类型的数据push_back通过overloadedoperator<</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();

最新更新