在没有序列化库的情况下,在 c++ 中从结构体读取和写入文件的最简单方法是什么?



我正在编写一个程序,该程序定期存储和读取以下形式的结构。

struct Node {
    int leftChild = 0;
    int rightChild = 0;
    std::string value;
    int count = 1;
    int balanceFactor = 0;
};

如何读取节点并将其写入文件?我想使用带有seekg和seekp的fstream类手动进行序列化,但根据文档,我不确定它是如何工作的,我很难找到合适的示例。

[edit]指定我不想使用序列化库。

这个问题被称为序列化。使用串行化库,例如Google的协议缓冲区或平面缓冲区。

要序列化对象,您需要坚持对象将其成员写入流并从流中读取成员的概念。此外,成员对象应该将自己写入流(以及读取)。

我使用三个成员函数和一个缓冲区实现了一个方案:

void load_from_buffer(uint8_t * & buffer_pointer);  
void store_to_buffer(uint8_t * & buffer_pointer) const;  
unsigned int size_on_stream() const;  

size_on_stream将首先被调用,以确定对象的缓冲区大小(或它在缓冲区中占据的空间)。

load_from_buffer函数使用给定的指针从缓冲区加载对象的成员。该函数还适当地增加指针。

store_to_buffer函数使用给定的指针将对象的成员存储到缓冲区中。该函数还适当地增加指针。

这可以通过使用模板和模板专业化应用于POD类型。

这些函数还允许您将输出打包到缓冲区中,并从打包格式加载。

对缓冲区进行I/O的原因是,您可以使用更高效的流方法,如writeread

编辑1:将节点写入流
编写或序列化节点(如链表或树节点)的问题是指针无法转换为文件。无法保证操作系统每次都会将程序放在相同的内存位置或给您相同的内存区域。

您有两个选项:1)只存储数据。2) 将指针转换为文件偏移量。选项2)非常复杂,因为它可能需要重新定位文件指针,因为文件偏移量可能不提前知道。

此外,还要注意字符串等可变长度记录。不能直接将字符串对象写入文件。除非使用固定的字符串宽度,否则字符串大小将发生变化。您需要在字符串前面加上字符串长度(首选),或者使用某种终止字符,如"\0"。首选字符串长度优先,因为您不必搜索字符串的末尾;你可以使用块读来阅读文本。

如果用char缓冲区替换std::字符串,则可以使用fwrite和fread将结构作为固定大小的信息块写入磁盘和从磁盘读取结构。在一个应该可以正常工作的程序内。

最大的错误是编译器会在字段之间插入填充以保持数据对齐。这使得代码的可移植性降低,就好像一个模块是用不同的对齐要求编译的一样——结构实际上可以是不同的大小,这就把你的固定大小假设抛在了门外。

我倾向于使用某种过时的序列化库。

另一种方法是重载运算符<lt;和运算符>>,以便它知道如何保存/加载自己。这将减少知道在哪里读/写节点的问题。理论上,您的左右子字段可以是节点实际所在的查找地址,而新字段可以包含当前节点的查找位置。

在实现自己的序列化方法时,您必须做出的第一个决定是希望磁盘上的数据是二进制格式还是文本格式。

我发现实现保存为二进制格式的功能更容易。实现这一点所需的功能数量很少。您需要实现可以编写基本类型、编译时已知大小的数组、动态数组和字符串的函数。其他一切都可以建立在这些之上。

以下内容与我最近在生产代码中所做的内容非常接近。

#include <cstring>
#include <fstream>
#include <cstddef>
#include <stdexcept>
// Class to write to a stream
struct Writer
{
   std::ostream& out_;
   Writer(std::ostream& out) : out_(out) {}
   // Write the fundamental types
   template <typename T>
      void write(T number)
      {
         out_.write(reinterpret_cast<char const*>(&number), sizeof(number));
         if (!out_ )
         {
            throw std::runtime_error("Unable to write a number");
         }
      }
   // Write arrays whose size is known at compile time
   template <typename T, uint64_t N>
      void write(T (&array)[N])
      {
         for(uint64_t i = 0; i < N; ++i )
         {
            write(array[i]);
         }
      }
   // Write dynamic arrays
   template <typename T>
      void write(T array[], uint64_t size)
      {
         write(size);
         for(uint64_t i = 0; i < size; ++i )
         {
            write(array[i]);
         }
      }
   // Write strings
   void write(std::string const& str)
   {
      write(str.c_str(), str.size());
   }
   void write(char const* str)
   {
      write(str, std::strlen(str));
   }
};

// Class to read from a stream
struct Reader
{
   std::ifstream& in_;
   Reader(std::ifstream& in) : in_(in) {}
   template <typename T>
      void read(T& number)
      {
         in_.read(reinterpret_cast<char*>(&number), sizeof(number));
         if (!in_ )
         {
            throw std::runtime_error("Unable to read a number.");
         }
      }
   template <typename T, uint64_t N>
      void read(T (&array)[N])
      {
         for(uint64_t i = 0; i < N; ++i )
         {
            read(array[i]);
         }
      }
   template <typename T>
      void read(T*& array)
      {
         uint64_t size;
         read(size);
         array = new T[size];
         for(uint64_t i = 0; i < size; ++i )
         {
            read(array[i]);
         }
      }
   void read(std::string& str)
   {
      char* s;
      read(s);
      str = s;
      delete [] s;
   }
};

// Test the code.
#include <iostream>
void writeData(std::string const& file)
{
   std::ofstream out(file);
   Writer w(out);
   w.write(10);
   w.write(20.f);
   w.write(200.456);
   w.write("Test String");
}
void readData(std::string const& file)
{
   std::ifstream in(file);
   Reader r(in);
   int i;
   r.read(i);
   std::cout << "i: " << i << std::endl;
   float f;
   r.read(f);
   std::cout << "f: " << f << std::endl;
   double d;
   r.read(d);
   std::cout << "d: " << d << std::endl;
   std::string s;
   r.read(s);
   std::cout << "s: " << s << std::endl;
}
void testWriteAndRead(std::string const& file)
{
   writeData(file);
   readData(file);
}
int main()
{
   testWriteAndRead("test.bin");
   return 0;
}

输出:

i: 10
f: 20
d: 200.456
s: Test String

写入和读取Node的能力非常容易实现。

void write(Writer& w, Node const& n)
{
    w.write(n.leftChild);
    w.write(n.rightChild);
    w.write(n.value);
    w.write(n.count);
    w.write(n.balanceFactor);
}
void read(Reader& r, Node& n)
{
    r.read(n.leftChild);
    r.read(n.rightChild);
    r.read(n.value);
    r.read(n.count);
    r.read(n.balanceFactor);
}

您所指的进程称为序列化。我推荐麦片http://uscilab.github.io/cereal/

它支持jsonxmlbinary序列化,并且非常易于使用(有很好的示例)。

(不幸的是,它不支持我最喜欢的格式yaml

最新更新