【问题标题】:How to write custom binary file handler in c++ with serialisation of custom objects?如何在 C++ 中编写自定义二进制文件处理程序并序列化自定义对象?
【发布时间】:2019-08-27 09:35:43
【问题描述】:

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

我已阅读:

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

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

【问题讨论】:

  • 您可能需要一两年以上的时间来重新实现 boost 可以为您提供反序列化的功能,只是说...
  • 如果您不关心可移植性和向后兼容性,那么您可以将 (POD) 结构转换为 char* 并将它们写入文件,否则使用 boost 序列化、google protobuf 等。
  • 从技术上讲,static_cast<uint8_t*>(&some_data_obj) 是“序列化”的合法方式,产生指向长度为 sizeof(some_data) 的字节数组的指针 - 但它没有解决任务的复杂性,例如字节序,引用/指针作为成员,容器,(以及我从未见过的其他几个)。 Python 世界中的这种方法称为pickling (pythontips.com/2013/08/02/what-is-pickle-in-python)。但这一切都取决于您的需求,如何使用序列化数据? (只在同一台机器上?)
  • @foreknownas_463035818 你没有抓住重点。这不是boost vs anything 问题。这是序列化的问题。请坚持主题。
  • 我刚刚意识到您只链接了 SO Q/As。可能,这也很有趣(非 SO):C++ FAQ: Serialization and Unserialization.

标签: c++ class serialization data-structures struct


【解决方案1】:

开始之前

大多数新用户不熟悉 C++ 中的不同数据类型,并且经常在他们的代码中使用纯 intchar 等。要成功进行序列化,需要彻底考虑他们的数据types。因此,如果您有一个 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");

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

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

对于我们的猫对象,它可以表示除此之外的任何东西:

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

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

限制您的数据

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

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


序列化方法

构建我们的cat

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

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

  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

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

有关我如何构建这些文件的更多信息,您可以找到来源herethis 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。选择权在您手中,这取决于您的文件和数据的大小。所以现在我们有了文件的数据。当然,我必须警告你

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

但由于动物园拥有的数据比我们拥有的动物多,因此数据块的大小可能会有所不同。我们需要创建一个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) 和 headers 和/在稍后实现 footers

由于标题描述了整个文件,因此检查和加载可以增加您特定案例所需的功能。如果您有两个不同的对象,则需要添加到文件中,而不是更改页眉/页脚,一种类型的数据 insert 在数据的开头,另一种类型 push_back 在数据的末尾通过@987654382 @operator&lt;&lt;/operator&gt;&gt;.

对于相互之间没有关系的多个对象,您可以在继承中添加更多私有成员,用于存储各个段的当前位置,同时保持文件读写的整洁和组织。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-21
    • 2016-05-07
    • 2015-09-04
    • 2012-07-14
    • 1970-01-01
    • 2014-03-03
    相关资源
    最近更新 更多