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

What's the most simple way to read and write data from a struct to and from a file in c++ without serialization library?

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

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

如何将节点读写到文件中?我想将 fstream class 与 seekg 和 seekp 一起使用来手动进行序列化,但我不确定它是如何根据文档工作的,并且正在努力寻找合适的例子。

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

此问题称为 serialization. Use a serializing library like e.g. Google's Protocol Buffers or Flatbuffers

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

的麦片

它支持两者 json, xml二进制序列化并且非常易于使用(有很好的例子)。

(可惜不支持我喜欢的格式yaml)

如果将 std::string 替换为字符缓冲区,则可以使用 fwrite 和 fread write/read 您的结构作为固定大小的信息块进出磁盘。在应该可以正常工作的单个程序中。

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

我会倾向于某种老旧的序列化库。

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

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

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以确定对象的缓冲区大小(或它在缓冲区中占用了多少space)。

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

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

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

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

I/O到buffer的原因是可以使用更高效的block流方法,比如writeread.

编辑 1:将节点写入流
编写或序列化节点(例如链表或树节点)的问题是指针不会转换为文件。不能保证 OS 每次都会将您的程序放在相同的内存位置或为您提供相同的内存区域。

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

另外,请注意像字符串这样的可变长度记录。您不能直接将字符串对象写入文件。除非您使用固定的字符串宽度,否则字符串大小会发生变化。您需要在字符串前添加字符串长度(首选)或使用某种终止字符,例如“\0”。字符串长度优先是首选,因为您不必搜索字符串的结尾;您可以使用块读取来读取文本。

另一种方法是为结构重载运算符<< 和运算符>>,以便它知道如何save/load 本身。这会将问题减少到知道 read/write 节点的位置。理论上,您的左右子字段可以是节点实际所在的搜索地址,而新字段可以保存当前节点的搜索位置。

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

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

这与我最近放入生产代码中的内容非常接近。

#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);
}