稍微正式一点的应用都会用到对象的序列化/反序列化操作,常见的需求包括:
对象的持久存储。比如把一个对象存储到文件;在需要的时候,再把对象从文件中读出来;
对象的传递。比如要把一个对象通过管道,socket等任何手段传送到对端;
从数据构建对象。对象类型未知,但是我们可以从数据中构建一个对象出来。
我们现在来分析这些需求。看看C++如何有效地序列化/反序列化对象。下文中,凡提到序列化,都包含相应的反序列化。
一个对象的状态由其所有的内部成员状态决定。所以序列化对象其实可以等价于序列化其内部对象,递归地,其内部对象的序列化又可以归于构成这些内部对象的序列化。我们可以发现,到了最后,所有的对象构成都归于基本类型的序列化。
注意,这里的讨论并不适用于任何的资源管理类对象。
首先,我们需要的是最基本的支持,亦即对C++基本类型的序列化和反序列化;同时,由于标准库中的字符串类使用极为广泛,我们同样加入支持;最后,我们也加入对非格式的POD数据的支持,这样,据大多数的序列化需求应该可以得到满足。以下直接给出实现,其后是简单的说明,请仔细揣摩。
说明:
1. 使用独立的名字空间,避免名字冲突;
2. 对于bool型,理论上使用一个bit就可以表达其所有的信息。但是我们实际使用还是一个字节,因为这是内存操作的最小单位。为了有效地利用额外的信息,我们这里加入了对bool的校验,使用两个特殊的值;这两个值可以是任意值,只要不同即可。
3. 实现两个类,分别完成基本对象,字符串及其宽字符版本以及POD数据的序列化和反序列化;
4. 使用成员模板来消除基本对象序列化/反序列话过程中的代码重复;
5. 对于I/O操作失败,我们直接抛出异常。注意,这不是临时方案。而是经过仔细设计的序列化/反序列化过程的错误处理机制;
6. 构造这些对象需要输出或输入流对象,而不仅仅限于文件;
有了这个基础,我们就可以设计通用的序列化/反序列化接口了,非常简单:
那么,这个接口怎么使用呢?假设你有如下类,如果实现对其序列化/反序列化的支持呢?
下面就是实现代码以及用来测试的代码:
注意到了吗?实现一个对象的序列化/反序列化操作需要:
1. 从接口serializable中继承
2. 实现相应的接口。对于序列化,按照顺序依次把成员写入流;对于反序列化,则要严格按照相反的顺序依次读入
3. 如果某个成员不是基本数据类型,有两种选择:a. 实现该成员所属类型的序列化/反序列化,然后直接调用之;b. 直接序列化该成员的子成员。这样做会严重破坏对象的完整性,所以非常不推荐使用。
实现了对象的序列话之后,使用起来就特别容易了。在我们的测试代码中,我们把对象序列化为一个字串,然后反序列化之到另一个对象。我们最后得到两个状态完全一致的对象。如果我们把该字串通过socket传到远方,实际上就是对象的远程传递了。所有流行的远程对象调用都是用类似的基础机制达成其目的。