我很难理解序列化是什么和做什么。
让我简化一下我的问题。我的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
中保持不变,对吧?
问题
那么序列化有什么意义呢?我需要它吗?如果是,我应该如何使用它?
我一直认为任何类型的文件.txt/.csv/.exe/...
都是内存中01
的位,这意味着它们自然具有二进制表示,所以我们不能简单地通过这些文件发送这些文件套接字直接?
代码示例非常受欢迎。
答案 0 :(得分:6)
但无论如何数据a在save.bin中都是持久的,对吗?
没有!您的结构包含std::string
。确切的实现(以及使用强制转换为char*
获得的二进制数据不是由标准定义的,但实际的字符串数据总是会在类框架之外的某个地方重新分配,堆分配,所以你不能这很容易保存这些数据。通过正确完成序列化,字符串数据被写入到类的其余部分也会结束的位置,因此您可以从文件中读取它。这就是您需要序列化的内容。
怎么做:你必须以某种方式编码字符串,最简单的方法是首先写它的长度,然后是字符串本身。在读回文件时,首先读回长度,然后将该字节数读入新的字符串对象。
我一直认为任何类型的文件,.txt / .csv / .exe / ...,都是内存中的01位
是的,但问题是它没有普遍定义哪个位代表数据结构的哪个部分。特别是,有little-endian and big-endian architectures,它们存储“反过来”的位。如果你天真地读出一个用不匹配的架构编写的文件,你显然会变得垃圾。
答案 1 :(得分:5)
只写下二进制内存中的图像是一种序列化形式,对于琐碎的情况,它可以工作。但是一般来说,你需要解决一些只是倾倒内存而不考虑的问题:
如果数据包含任何指针当然你不能只是稍后转储一个加载,因为一旦程序终止并重新启动,指针所指向的内存地址就没有任何意义。许多对象都有“隐藏”指针......例如,没有办法将std::vector
转储到内存中并在以后正确地重新加载... sizeof
上的std::vector
显然不包括包含元素的大小,因此任何包含std::vector
的结构都不能被转储和重新加载。 std::string
和所有其他std
容器也是如此。
C和C ++结构和类没有根据它们在内存中占用的字节来定义,而不是可移植的。这意味着不同的编译器,不同的编译器版本甚至相同的版本但具有不同的编译选项可能会生成内存中结构布局不同的代码。
如果你需要序列化只是在同一个程序中保存和重新加载数据而且数据不应该长寿,那么确实可以使用内存转储。只是想想通过转储结构来保存数百万个文档,现在新的编译器版本(你强迫强制使用,因为它是新操作系统版本唯一支持的版本)具有不同的布局和你不能再加载这些文件了。
除了同系统可移植性问题之外,还要注意即使只有一个整数,也可以在不同的系统上具有不同的内存中表示。它可能更大或更小;它可能有不同的字节顺序。仅使用内存转储意味着另一个系统无法加载保存的内容。甚至不是一个整数。
如果你保存的数据寿命很长,那么你很可能会随着程序的发展改变结构,例如你将添加新的字段,你将删除未使用的字段,你将改变一般结构(例如,将矢量更改为链接列表。)
如果您的格式只是当前数据结构的内存图像,那么以后很难能够将color
字段添加到polygon
对象,并且程序可以加载旧文档假设默认颜色值为先前版本中使用的颜色。
即使编写转换程序也很困难,因为您将拥有能够加载旧文档的旧代码和能够保存新文档的新代码,但您不能只是“合并”这两个并获得一个加载旧程序并保存新程序的程序(即两个程序的源代码都有一个polygon
结构但是有不同的字段,现在是什么?)。
答案 2 :(得分:3)
您的字符串无法正确保存。如果您有不同的计算机,它们的整数表示可能会有所不同,例如,不同的编程语言对字符串的表示形式不同。
但是当你有指向成员的指针时,你将保存指针地址而不是指向成员,这意味着你无法再从文件中获取该数据。如果您的结构需要改变怎么办?所有使用您数据的软件都需要更改。
是的,您可以通过套接字发送文件,但是您需要某种协议才能确保知道文件的名称以及何时到达文件的末尾。
答案 3 :(得分:3)
你正在玩游戏。在非常困难的模式。你达到了最后一个级别。你很高兴。 2天的不间断比赛正在取得成效。情节即将结束。你会发现邪恶的策划者的动机,你如何成为英雄,并将收集在最后一扇门后面等待的受欢迎的史诗神器。 你来到这里而不必重启一次。
在幕后,有一个游戏对象,看起来像这样:
class GameState
{
int level;
}
级别为25
。
到目前为止,你真的很喜欢这款游戏,但是如果最后一个老板杀了你,你不想重新开始游戏。因此,直观地说,按Ctrl+S
。但是等等,你得到一个错误:
Sorry, saving is disabled.
什么?所以我必须重新开始以防我死?怎么会这样。
击鼓声
开发人员,虽然很棒(他们设法让你连续两天迷上,对吧?)没有实现序列化。
重新启动游戏时,会进行内存清理。那个非常重要的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
。
答案 4 :(得分:3)
序列化做了很多事情。它支持持久性(能够 离开程序,然后回到它并获得相同的数据), 进程和机器之间的通信。它基本上意味着 将内部数据转换为字节序列,并且有用, 你还必须支持反序列化:转换序列 字节返回数据。
当你这样做时,重要的是要在内部意识到这一点
程序,数据不仅仅是一个字节序列。它有格式和
结构:double
的表示方式与一台机器不同
例如,到下一个;和更复杂的对象,如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' );
}
}
答案 5 :(得分:1)
除了大edian或little endian之外,还存在如何使用该编译器为该程序的给定结构打包数据的问题。如果要保存整个结构,则不能使用任何指针,您必须将其替换为足以满足您需求的字符缓冲区。如果其他机器将是相同的体系结构,那么如果使用#pragma pack(1),结构的字段之间将没有任何间隙,您可以确保数据看起来像是序列化的,但没有字符串的大小前缀。如果您确定将读取数据的其他程序具有完全相同的相同设置,则可以跳过#pragma pack(1)。除此之外,数据不会匹配。
如果首先序列化到内存,则可以加快序列化过程。这通常可以通过缓冲类和大多数类型的一个模板化函数来完成。
template<typename T>
buffer& operator<<(T data)
{
*(T*)buf = data;
buf += sizeof(T);
}
显然,对于字符串和更大的数据类型,您需要专门的。您可以将memcpy用于大型结构并将指针传递给数据。对于字符串,您需要为前面提到的长度添加前缀。
但是,对于严重的序列化需求,还有很多需要考虑的问题。