简体中文版翻译:Zhiguo Zhang
它可以让你把一个对象或组对象存储在磁盘;或通过有线或无线传输到另一台计算机,然后通过反向过程:唤醒原始的对象。基本机制是把对象扁平化(flatten)为一维的字节流,然后把字节流再变成原始的对象。
.如同星际迷航中的运输机,把复杂的东西扁平化为1和0的序列,然后把1和0的序列(可能在另一个地方,另一个时间)重新构造为原有的复杂“东西”。
[ Top | Bottom
| Previous section
| Next section
| Search the FAQ
]
有很多很多的条件限制,实际上,涉及到技术整体连贯性的多个方面。因为我时间有限(翻译:我没有报酬),我将之简化为“使用人类可读的(“文本”)或者非人类可读的(“二进制“)格式”,由或多或少按照技术成熟度排序的5个小技巧组成。
当然,不局限于上述五技术。你可能会想最终混合的几种技术。当然你也可以随时使用比实际需求更复杂(编号更高)的技术。事实上,使用比实际需求更复杂的技术是明智的,如果你认为未来的式样变更需要更大的复杂度。所以,这份名单仅仅是一个很好的起点。
最好准备!有许多东东在这里呢!
下面是同样的信息,但是使用算法格式:
记住:可以随意混合/增加上述列表,如果你能够判断使用更复杂的技术可以让额外成本最小。
还有一件事:对象继承和对象含有指针在逻辑上是不相关的,因此#2比#3-5简单并没有任何理论依据。然而在实践中常常(并不总是)总是这样。所以请不要认为这些分类是金科玉律—在某种程度上他们有些武断,你可能需要混合的这些解决方案以满足你的具体情况。序列化技术领域远非几个问题和解答能解决的。
[ Top | Bottom
| Previous section
| Next section
| Search the FAQ
]
要小心。
这个问题没有“正确”回答,它取决于你的目标。下面是人类可读(“文本”) 与非人类可读的(“二进制”)格式的一些利弊:
你也许会有补充添加其他的优缺点…重要的是要知道一鞋难合众人脚-请作出自己慎重决定。
还有:无论你如何选择,你要在每个文件/流的开头加上“magic”标签和版本号。版本号将显示格式规则。这样,如果你决定对格式做重大改变的时候,将仍然可以读取旧的软件产生的输出。
[ Top | Bottom
| Previous section
| Next section
| Search the FAQ
]
答案取决于使用人类可读(“文本”)还是非格式人类可读(“二进制”)格式的决定:
在本节几乎所有的其他FAQ中都需要上述FAQ中的讨论作为基础。
[ Top | Bottom
| Previous section
| Next section
| Search the FAQ
]
阅读之前,请确保要评估人类可读和非人类可读的格式之间的平衡。这种均衡评估是非常重要的,所以不能因为上一个项目没有这么做而下意识地抵制它—一鞋难合众人脚。
如果你已明确决定使用人类可读(“文本”)格式,你应该记住这些要点:
请记住,这些是在本节中的其他FAQ中需要的一些基础知识。
[ Top | Bottom
| Previous section
| Next section
| Search the FAQ
]
阅读之前,请确保要评估人类可读和非人类可读的格式之间的平衡。这种均衡评估是非常重要的,所以不能因为上一个项目没有这么做而下意识地抵制它—一鞋难合众人脚。
如果你已明确决定使用非人类可读(“文本”)格式,你应该记住这些要点:
请记住,这些是在本节中的其他FAQ中需要的一些基础知识。
[ Top | Bottom
| Previous section
| Next section
| Search the FAQ
]
这是最简单的问题,毫不奇怪它也最容易解决:
[ Top | Bottom
| Previous section
| Next section
| Search the FAQ
]
假设你要序列化“shape”对象,其中shape是一个抽象类,它的派生类有Rectangle, Ellipse, Line, Text, etc等。在shape类中你将声明一个纯虚函数serialize(std::
ostream&) const,并确保每个重写首先会输出类的身份。例如,Ellipse::serialize(std::ostream&) const会输出Ellipse的标识符(可能只是一个简单的字符串,但也可以是下文讨论的几种方案)。
当反序列化对象时,事情有点棘手。通常首先从基类的静态成员函数如Shape::unserialize(std::istream& istr)开始。这是声明返回一个 Shape *或可能是像shape::Ptr的智能指针。它读取类名标识符
,然后使用某些创建模式来创建对象。例如,你可能有类名到对象的映射表,然后使用虚拟构造函数用法来创建对象。
这里有一个具体的例子:在基类shape内添加一个纯虚函数create(std::istream&) const,并定义只有一行的重写函数,来生成一个适当的派生类对象。例如,Ellipse::create(std::istream& istr) const将是{
return new Ellipse(istr); }。 添加一个static
映射表通常采用静态初始化。例如,如果文件Ellipse.cpp包含了派生类Ellipse的代码,它还将包含一个静态的对象,其构造函数将类添加到映射表:theMap["Ellipse"] = new Ellipse()。
说明和注意事项:
[ Top | Bottom
| Previous section
| Next section
| Search the FAQ
]
开始之前,你必须明白,“树”并不意味着对象存储在数据结构的树中。它只是意味着你的对象互相指向对方。没有回路是指,如果不断地从一个对象指针跟随到下一个对象,你永远不会回到一个较早的对象。你的对象不是在树的内部,它们是一棵“树”。如果你不理解这些话,在继续之前你应该阅读行话FAQ。
既无回路也无链接的图非常常见,即使像组合或者修饰样的“递归组合”设计模式。例如,表示XML文档或HTML文档的对象可以表示为一个没有回路和链接的图。
序列化图的关键是忽视节点的身份
,但注重其内容 。算法(通常递归)遍历树并且不断地输出其内容。例如,如果当前节点恰好有一个整数a,指针B,浮点数c和另一个指针d,那么你先输出整数 a, 然后递归遍历b指向的子节点,然后输出浮点数 c,最后递归遍历d指向的子节点 。 (你不必以声明顺序来进行序列化/反序列化,唯一的规则是,序列化和反序列化的顺序要一致。)
反序列化时你需要一个构造函数以std::istream&为参数。上述对象的构造函数将读入一个整数并存储结果到a中,然后分配一个对象存储在指针b中(需要传递std::istream到对象的构造函数中,然它也可以读取流的内容), 读入一个浮点数到c,最后将分配一个对象存储在指针d中 。一定要在对象中使用智能指针,否则,如果任何序列化抛出异常的话(除了第一个指针象之外),将会出现内存泄露。
当创建对象的时候,使用命名构造函数惯用法很方便。这样做的好处是可以强制使用的智能指针。要在类 Foo中实现这一点,需要添加一个如FooPtr
Foo::create(std::istream& istr)
如果一个对象包含可变数目的子对象,例如, 一个std::vector类型的指针,通常的做法是在递归之前输出子对象的数目。当反序列化时,只需要读入子对象的数目 , 然后就可以使用一个循环来创建适当数量的子对象。
如果一个子对象有可能是 NULL指针,一定要在序列化和反序列化时候做处理。这不应该是一个问题, 如果对象使用继承 ;详情见上面的解决方案。
否则,如果第一个序列化的成员有一个已知的范围,使用范围以外的东西来表示一个NULL指针。例如,如果一个序列化对象的第一个成员总是一个数字,那就使用比如N这样的非数字来表示一个 NULL指针。反序列化是偶可以使用std::istream::peek()来检查'n'标签。如果第一个成员没有范围,那就强加一个范围,例如,每个对象之前总是输出'×',然后使用’y’来表示NULL。
如果一个对象内部本身包含另一个对象,而不是包含指向其他对象的指针,这与上述做法没有什么两样:你仍然需要递归子节点,就好像是通过指针一样。
[ Top | Bottom
| Previous section
| Next section
| Search the FAQ
]
和之前一样,“树”并不意味着对象存储在数据结构的树中。它只是意味着你的对象互相指向对方。没有回路是指,如果不断地从一个对象指针跟随到下一个对象,你永远不会回到一个较早的对象。你的对象不是在树的内部,它们是一棵“树”。如果你不理解这些话,在继续之前你应该阅读行话FAQ。
如果图包含叶节点的链接,但这些链接可以很容易地通过一个简单的查找表重建,那么使用此解决方案。举例来说,像(3*(a+b) - 1/a)样的算术表达式的解析树可能会有链接,因为变量名称(如a)出现不止一次。如果你想让图使用完全相同的节点对象来表示该变量的两次出现,那么你可以可能需要这种解决方案。
虽然上述限制,不适合于那些没有任何链接的解决方案。但是它是如此的接近,因此你可以想办法套用那个解决方案。差异是:
警告:这里假设变量a的所有出现应该映射到同一个节点对象;如果情况比这复杂,即如果一部分a映射到一个对象,另一些部分映射到另一个对象,你可能需要使用一个更复杂的解决方案。
[ Top | Bottom
| Previous section
| Next section
| Search the FAQ
]
警告:术语“树”并不意味着对象存储在数据结构的树中。它只是意味着你的对象互相指向对方。没有回路是指,如果不断地从一个对象指针跟随到下一个对象,你永远不会回到一个较早的对象。你的对象不是在树的内部,它们是一棵“树”。如果你不理解这些话,在继续之前你应该阅读行话FAQ。
如果图包含回路,或者包含比平凡链接解决方案中更复杂的链接,那么使用此解决方案。该解决方案处理的两个核心问题:它避免了无限循环,除了节点的内容之外还写入/读取每个节点的身份
。
一个节点的身份在不同的输出流中通常不会一致。例如,如果某个文件使用数字3代表节点 x,一个不同的文件可以使用数字3代表一个不同的节点y 。
有一些巧妙的方法来序列化这种图,但最容易描述的是两阶段(two-pass)算法,该算法使用一个对象的ID映射表 ,例如std::map<Node*,unsigned> oidMap。在第一阶段设置oidMap,也就是说,它构建一个对象指针到对象整数ID的映射。 通过递归图,在每个节点检查该节点是否已经在oidMap,如果没有在oidMap中,就添加节点和一个唯一的整数ID到oidMap,然后递归新的子节点。唯一的整数ID通常取oidMap.size(),如unsigned
n = oidMap.size(); oidMap[nodePtr] = n。(是的,需要使用两条语句。你也必须这样做。 不要缩短为一条语句。莫怪我没有警告你!)
第二阶段遍历oidMap的所有节点,并在在每个节点输出入节点的身份(相关的整数ID)之后输入节点内容。当输出节点内容时候,如果包含指向其他节点的指针,不要遍历这些子对象,只需要输出子节点的身份(相关整数ID)。例如,当节点包含Node* child,只需输出整数oidMap[child]。第二阶段之后,该oidMap可以被丢弃。换句话说,节点*到无符号整数ID的映射通常不会活过任何给定图的序列化结束。
也有一些巧妙的方法来反序列化这种图,但在这里还是使用最容易描述的是两阶段(two-pass)算法。第一阶段设置一个含有正确类型的std::vector<Node*> v对象,但所有这些对象的子指针都是NULL指针。这意味着v[3]将指向对象ID是3的对象,但该对象内部的任何子指针将都是 NULL指针。第二阶段设置对象内部的的子指针,例如, 如果 v [3]有一个子指针叫child,应该指向对象ID是5的对象。在第二阶段的将v[3].child从NULL修改为v [5](显然封装可能禁止直接访问v[3].child,但最终v[3].child需要被改为v[5])。反序列化一个给定流之后, vector v通常被丢弃。换句话说,当序列化或反序列化不同的流时,对象ID(3,5等等)没有任何意义-这些数字只在一个给定流内有意义。
注意:如果你的对象包含多态性指针(polymorphic pointers) ,也就是基类指针可能指向派生类对象,那么使用以前描述的技术。 你还需要阅读的先前的关于处理NULL指针和版本号的一些技术。
注意:当递归遍历图的时候,你应该考虑访问者模式。因为序列化可能只是需要做递归遍历的原因之一,任何递归都需要避免无限循环。
[ Top | Bottom
| Previous section
| Next section
| Search the FAQ
]
除非在特殊情况下,不要在遍历时候改变节点数据。例如,有些人觉得通过简单地增加一个节点类的整型数据成员,他们就可以映射Node*到整数。有时也添加布尔型的haveVisited标志作为Node对象的另外一个数据成员。
但是 ,这将导致大量的多线程和/或性能问题。你的serialize()方法可能不能再为const,所以即使它在逻辑上只是读取节点数据,尽管它在逻辑上可以有多个线程同时读取节点,但是实际的算法却写入数据到节点。如果你理解线程和读/写冲突,你只能寄希望于你的代码更复杂更慢(你必须阻止所有的读取线程,只要任何线程对图进行操作的话)。如果你(或者后继的代码维护者)不理解线程和读/写冲突,这可能引起非常严重和非常不容易觉察的错误。读/写和写/写冲突很不容易觉察,这些错误很难测试到。坏消息!相信我!如果你不相信我,找个你信任的人。但是切莫偷工减料!
现在还有很多更多要说,例如一些特殊情况。但我已经花了太多时间。如果想了解更多信息,花一些钱吧。
[ Top | Bottom
| Previous section
| Next section
| Search the FAQ
]
当你的对象包含指向其他对象的指针,你就有了一种计算机科学家称之为图的东东 。你的对象都存储在一个像树的数据结构里面;他们是一个像树的数据结构。
图的节点又名顶点相当于你的对象 ,图的边相当于你的对象里面的指针 。 该图是一个树的一个特例,称为带根有向图。要序列化的根对象对应于图的根节点 ,指针对应于直接边。
如果对象 x有一个指向对象 Y的指针,我们说x是Y双亲 或Y是X 的 孩子 。
图的路径是指从一个对象开始,顺着指针到另一个对象等等,可以是任意深度。如果存在一个从 x 到 z的路径,我们说,x是z的祖先 和/或z是x 的 子孙。
图的链接是指有两个或两个以上不同的路径可以到达同一对象。例如, 如果 z是x和y的孩子,则图有一个链接,z是一个链接节点 。
图的回路(环)是指存在一个返回对象自己的路径: 如果 x有一个指向自己的指针,或指向Y的指针,Y又指向x,或者为 Y,Y指向 z,Z又指向x等。图是有环的如果有一个或多个回路,否则是无环的 。
一个内部节点是有孩子的节点。叶节点是没有孩子的节点。
正如本节中使用的那样,树是指一个带根的,有向的,无环图。请注意,树的节点也是树。
[ Top | Bottom
| Previous section
| Next section
| Search the FAQ
]
E-mail the author
[ C++ FAQ Lite | Table of contents
| Subject index | About the author
| ©
| Download your own copy ]
Revised
Jan 2, 2009