7

我很难理解序列化是什么以及做什么。

让我简化我的问题。我的 c/c++ 程序中有一个struct info,我可以将此struct数据存储到文件中save.bin或通过套接字将其发送到另一台计算机。

struct info {
    std::string name;
    int age;
};

void write_to_file()
{
    info a = {"Steve", 10};
    ofstream ofs("save.bin", ofstream::binary);
    ofs.write((char *) &a, sizeof(a));   // am I doing it right?
    ofs.close();
}

void write_to_sock()
{
    // I don't know about socket api, but I assume write **a** to socket is similar to file, isn't it?
}

write_to_file将简单地将struct info对象保存a到磁盘,使这些数据持久化,对吗?将其写入套接字几乎相同,对吧?

在上面的代码中,我认为我没有使用数据序列化,但无论如何数据a都是持久的save.bin,对吧?

问题

  1. 那么序列化的意义何在?我这里需要吗?如果是,我应该如何使用它?

  2. 我一直认为任何类型的文件.txt/.csv/.exe/...,,,都是内存中的位01,这意味着它们自然具有二进制表示,所以我们不能简单地通过套接字直接发送这些文件吗?

代码示例受到高度赞赏。

4

6 回答 6

6

但是数据 a 无论如何都会在 save.bin 中持久化,对吗?

不!您的结构包含一个std::string. 确切的实现(以及您通过强制转换获得的二进制数据char*不是由标准定义的,但实际的字符串数据将始终在类框架之外的某个地方辞职,堆分配,因此您无法轻松保存该数据. 正确完成序列化后,字符串数据将写入类的其余部分也结束的位置,因此您将能够从文件中读取它。这就是您需要序列化的目的。

怎么做:你必须以某种方式对字符串进行编码,最简单的方法是先写它的长度,然后写字符串本身。在读回文件时,首先读回长度,然后将该字节数读入一个新的字符串对象。

我一直认为任何类型的文件,.txt/.csv/.exe/...,都是内存中的 01 位

是的,但问题是没有普遍定义哪个位代表数据结构的哪个部分。特别是,有little-endian 和 big-endian 架构,它们以“相反的方式”存储位。如果你天真地读出一个用不匹配架构编写的文件,你显然会得到垃圾。

于 2012-08-17T07:16:21.967 回答
5

仅仅写下内存中的二进制图像是一种序列化形式,对于琐碎的情况,它可以工作。但是,一般来说,您需要解决一些仅转储内存未考虑的问题:

1.指针

如果数据包含任何指针,当然你不能只是稍后转储负载,因为一旦程序终止并重新启动,指针指向的内存地址将毫无意义。许多对象都有“隐藏”指针...例如,无法将 an 转储std::vector到内存中并稍后正确重新加载... sizeofonstd::vector显然不包括所包含元素的大小,因此任何包含 an 的结构std::vector都不能仅转储并重新加载。对于std::string所有其他std容器也是如此。

2. 便携性

C 和 C++ 结构和类不是根据它们在内存中占用的字节来定义的,即不可移植。这意味着不同的编译器、不同的编译器版本甚至相同版本但具有不同编译选项的代码可能会生成内存中结构布局不同的代码。

如果您需要序列化以仅在同一程序中保存和重新加载数据并且该数据不应该存在很长时间,那么确实可以使用内存转储。但是,只需考虑通过转储结构保存数百万个文档,现在新的编译器版本(您被迫使用,因为它是新操作系统版本唯一支持的)具有不同的布局,您无法再加载这些文档.

除了相同系统的可移植性问题之外,还要注意即使是单个整数也可以在不同系统上具有不同的内存表示。它可能更大或更小;它可能有不同的字节顺序。仅使用内存转储意味着保存的内容不能被另一个系统加载。甚至没有一个整数。

3. 版本控制

如果您保存的数据有很长的生命周期,那么您很可能会随着程序的发展而更改结构,例如您将添加新字段,您将删除未使用的字段,您将更改一般结构(例如更改向量到链表)。

如果您的格式只是当前数据结构的内存图像,那么以后很难将例如color字段添加到polygon对象并让程序可以加载旧文档,假设默认颜色值是使用的颜色以前的版本。

即使编写转换程序也会很困难,因为您将拥有能够加载旧文档的旧代码和能够保存新文档的新代码,但是您不能只是“合并”两者并获得一个加载旧代码并保存新文档的程序(即两者程序源代码将具有polygon结构但具有不同的字段,现在呢?)。

于 2012-08-17T07:25:09.963 回答
3

您的字符串将无法正确保存。如果您有不同的机器,它们对整数的表示可能会有所不同,例如,不同的编程语言不会对字符串有相同的表示。

但是当您有指向成员的指针时,您将保存指针地址而不是指向的成员,这意味着您无法再次从文件中获取该数据。如果你的结构需要改变怎么办?所有使用您数据的软件都需要更改。

是的,您可以通过套接字发送文件,但您需要某种协议以确保您知道文件的名称以及何时到达文件末尾。

于 2012-08-17T07:17:53.743 回答
3

你在玩游戏。在非常困难的模式下。你到达最后一层。你很高兴。2 天的不间断游戏正在获得回报。剧情很快就要结束了。你会发现邪恶策划者的动机,你是如何成为英雄的,并将收集在最后一扇门后面等待的广受欢迎的史诗神器。而且您无需重新启动一次即可到达这里。

在幕后,有一个游戏对象,如下所示:

class GameState
{
   int level;
}

而且水平是25

到目前为止,你真的很喜欢这个游戏,但你不想从头开始,以防最后一个老板杀了你。因此,直观地,您按下Ctrl+S。但是等等,你会得到一个错误:

Sorry, saving is disabled.

什么?所以我必须重新开始,以防我死?怎么会这样。

击鼓

开发人员虽然很出色(他们设法让您连续 2 天着迷,对吧?)并没有实现serialization

当您重新启动游戏时,会进行内存清理。那个最重要的GameState对象,你花了 2 天时间将level成员增加到 的那个25,被销毁了。

你怎么能解决这个问题?关闭游戏时,操作系统会回收内存。你能把它存放在哪里?在外部服务器上?(套接字)在磁盘上?(写入文件)

好吧,为什么不呢。

class GameState
{
    int level;
    void save(const std::string& fileName)
    { /* write level to file */ }
    void load(const std::string& fileName)
    { /* read game state from file */ }
};

当您按下Ctrl+s时,GameState对象被保存到一个文件中。

而且,奇迹般地,当您加载游戏时,GameState会从该文件中读取对象。您不再需要花费 2 天时间才能回到上一个老板。你已经在那里了。

真实答案:

从技术上讲,编写序列化功能非常困难。我建议你使用第三方。Google 协议缓冲区提供了跨平台甚至跨语言的序列化。许多其他的存在。

1.那么序列化有什么意义呢?我这里需要吗?如果是,我应该如何使用它?

如上所述,它在运行之间或进程之间(可能在不同的机器上)存储状态。是否需要取决于您是否需要存储状态并稍后重新加载。

2.我一直认为任何一种文件,.txt/.csv/.exe/...,在内存中都是01的位,这意味着它们自然具有二进制表示,所以我们不能简单地通过套接字发送这些文件直接地?

他们是。但是您不想在.exe玩新游戏时修改。

于 2012-08-17T07:19:27.913 回答
3

序列化做了很多事情。它支持持久性(能够离开程序,然后返回程序并获得相同的数据),以及进程和机器之间的通信。它基本上意味着将您的内部数据转换为字节序列,并且为了有用,您还必须支持反序列化:将字节序列转换回数据。

当你这样做时,重要的是要意识到在程序内部,数据不仅仅是一个字节序列。它具有格式和结构:例如,adouble的表示方式因一台机器与另一台机器不同;和更复杂的对象,例如std::string,甚至不在连续内存中。因此,当您序列化时,您要做的第一件事就是定义每种类型如何表示为一个字节序列。如果您正在与另一个程序通信,则两个程序都必须同意这种串行格式;如果只是为了让您自己重新读取数据,您可以使用任何您想要的格式(但我建议使用预定义的标准格式,如 XDR,如果只是为了简化文档)。

你不能做的只是在内存中转储对象的图像。复杂的对象std::string中会有指针,而这些指针在另一个进程中将毫无意义。甚至简单类型的表示double也可能会随着时间而改变。(从 32 位迁移到 64 位导致long大多数系统上的大小发生变化。)您必须定义一种格式,然后从您拥有的数据中逐字节生成它。例如,要编写 XDR,您可能会使用如下内容:

typedef std::vector<char> Buffer;

void
writeUInt( Buffer& dest, unsigned value )
{
    dest.push_back( (value >> 24) & 0xFF );
    dest.push_back( (value >> 16) & 0xFF );
    dest.push_back( (value >>  8) & 0xFF );
    dest.push_back( (value      ) & 0xFF );
}

void
writeInt( Buffer& dest, int value )
{
    writeUInt( dest, static_cast<unsigned>( value ) );
}

void
writeString( Buffer& dest, std::string const& value)
{
    assert( value.size() <= 0xFFFFFFFF );
    writeInt( dest, value.size() )
    std::copy( value.begin(), value.end(), std::back_inserter( dest ) );
    while ( dest.size() % 4 != 0 ) {
        dest.push_back( '\0' );
    }
}
于 2012-08-17T10:02:32.617 回答
1

除了大 edian 或 little endian 之外,还有一个问题是如何使用该编译器为该程序的给定结构打包数据。如果要保存整个结构,则不能使用任何指针,则必须将其替换为足够大的字符缓冲区以满足您的需要。如果另一台机器将采用相同的架构,那么如果您使用#pragma pack(1),您的结构字段之间将不会有任何间隙,并且您可以确保数据看起来好像是序列化的,但没有字符串的大小前缀。如果您确定将读取数据的其他程序对于相同的结构具有完全相同的设置,则可以跳过#pragma pack(1)。除此之外,数据将不匹配。

如果先序列化到内存,可以加快序列化过程。这通常可以通过缓冲区类和大多数类型的模板函数来完成。

template<typename T>
buffer& operator<<(T data)
{
    *(T*)buf = data;
    buf += sizeof(T);
}

显然,您需要专门的字符串和更大的数据类型。您可以将 memcpy 用于大型结构并传入指向数据的指针。对于字符串,您需要为前面提到的长度添加前缀。

但是,对于严重的序列化需求,还有更多需要考虑。

于 2012-08-17T09:24:13.783 回答