C++序列化
C++序列化
一、序列化概念:
序列化指的是将一个内存对象转化成一串字节数据(存储在一个字节数组中),可用于保存到本地文件或网络传输。
反序列化就是将字节数据还原成内存对象。
对序列化的理解
在编写应用程序的时候往往需要将程序的某些数据存储在内存中,然后将其写入某个文件或是将它传输到网络中的另一台计算机上以实现通讯。这个将 程序数据转化成能被存储并传输的格式的过程被称为“序列化”(Serialization),而它的逆过程则可被称为“反序列化” (Deserialization)。
简单来说,序列化就是将对象实例的状态转换为可保持或传输的格式的过程。与序列化相对的是反序列化,它根据流重构对象。这两个过程结合起来,可以轻 松地存储和传输数据。例如,可以序列化一个对象,然后使用 HTTP 通过 Internet 在客户端和服务器之间传输该对象。
序列化:将对象变成字节流的形式传出去。
反序列化:从字节流恢复成原来的对象。
简单来说,对象序列化通常用于两个目的:
1)将对象存储于硬盘上 ,便于以后反序列化使用。
2)在网络上传送对象的字节序列。
二、为什么要序列化?
通过序列化,可以将对象的状态保持在存储媒体中,在以后能够重新创建精确的副本。我们经常需要将对象的字段值保存到磁盘中,并在以后检索此数据。尽管不使用序列化也能完成这项工作,但这种方法通常很繁琐而且容易出错,并且在需要跟踪对象的层次结构时,会变得越来越复杂。可以想象一下编写包含大量对象的大型业务应用程序的情形,程序员不得不为每一个对象编写代码,以便将字段和属性保存至磁盘以及从磁盘还原这些字段和属性。序列化提供了轻松实现这个目标的快捷方法。
三、序列化的优势
在系统化的序列化方法出现之前,程序员如果想要将自定义的一个类的对象持久化地保存下来,并进行传输,可以采用以下这些方法:
由程序员自己实现保存对象数据的功能,针对每一个对象编写代码,将其数据存储下来。
将对象强制转换为char*或者void*类型的数据,然后进行数据的传输。
下面将从通用性、便捷性、灵活性和可移植性的角度来比较序列化相对于上述两种方法的优势。
通用性
如果由程序员自己实现保存对象数据的功能,那么对于每一个类的对象,程序员都要编写不同的代码,工作量很大,通用性不高。而序列化提供了一套流程化的方法,对于每一种类,都是大体一致的流程,提高了代码的通用性。
如果将对象强制转换为char*或void*类型的数据进行传输,那么必须预先得知该对象的大小以提前分配数组的空间。但是,如果该对象中存在可变长的数据结构,就无法准确地得知对象数据的大小了,只能预先估计一下。如果估计小了,可能会造成空间溢出,程序崩溃的后果;如果估计大了,又会造成空间的浪费。但是,如果使用序列化的方法,就能很好地解决可变长数据结构的问题。
便捷性
如果由程序员自己实现保存对象数据的功能,那么对于类中不同的数据结构,程序员都要编写相应的保存代码,简单的数据结构还好说,如果是具有多种层次的数据结构,代码的编写将越来越复杂,这样繁琐且容易出错。序列化提供了针对简单数据类型,以及字符串类型、STL容器、指针等种种数据类型的持久化的方法,只需简单地调用即可,具有很大的便捷性。
灵活性
序列化提供了若干种将对象数据持久化的格式,比如以简单文本格式保存、以XML格式保存、以SOAP格式保存、以二进制格式保存等等。还提供了多种保存持久化之后的对象的方式,比如保存到字符串、保存到文件等等,具有很大的灵活性。
可移植性
使用将对象强制转换为char*类型进行传输的方法,需要注意CPU字节序的问题。如果起始机器与目的机器的CPU字节序不同,就会造成目的机器读到的数据无法恢复成原来对象的问题。虽然可以通过将本地字节序转化为网络字节序进行传输,传到目的机器之后再将网络字节序转为本地字节序的方法解决这个问题,但是这就增加了程序员考虑问题的复杂性。序列化屏蔽了字节序的差异,使得被持久化对象的传输更具有可移植性。
此外,使用序列化还可以很好地跨平台。
四、C++对象序列化的四种方法
将C++对象进行序列化的方法一般有四种,下面分别介绍:
3.1 Google Protocol Buffers(protobuf)
Google Protocol Buffers (GPB)是Google内部使用的数据编码方式,旨在用来代替XML进行数据交换。可用于数据序列化与反序列化。主要特性有:
- 高效
- 语言中立(Cpp, Java, Python)
- 可扩展
3.2 Boost.Serialization
Boost.Serialization可以创建或重建程序中的等效结构,并保存为二进制数据、文本数据、XML或者有用户自定义的其他文件。该库具有以下吸引人的特性:
- 代码可移植(实现仅依赖于ANSI C++)。
- 深度指针保存与恢复。
- 可以序列化STL容器和其他常用模版库。
- 数据可移植。
- 非入侵性。
3.3 MFC Serialization
Windows平台下可使用MFC中的序列化方法。MFC 对 CObject 类中的序列化提供内置支持。因此,所有从 CObject 派生的类都可利用 CObject 的序列化协议。
3.4 .Net Framework
.NET的运行时环境用来支持用户定义类型的流化的机制。它在此过程中,先将对象的公共字段和私有字段以及类的名称(包括类所在的程序集)转换为字节流,然后再把字节流写入数据流。在随后对对象进行反序列化时,将创建出与原对象完全相同的副本。
3.5 简单总结
这几种序列化方案各有优缺点,各有自己的适用场景。其中MFC和.Net框架的方法适用范围很窄,只适用于Windows下,且.Net框架方法还需要.Net的运行环境。从序列化时间、反序列化时间和产生数据文件大小这几个方面比较了前三种序列化方案,得出结论如下(仅供参考):
- Google Protocol Buffers效率较高,但是数据对象必须预先定义,并使用protoc编译,适合要求效率,允许自定义类型的内部场合使用。
- Boost.Serialization 使用灵活简单,而且支持标准C++容器。
- 相比而言,MFC的效率较低,但是结合MSVS平台使用最为方便。
为了考虑平台的移植性、适用性和高效性,推荐大家使用Google的protobuf和Boost的序列化方案,下面介绍我使用这两种方案的心得及注意事项。
4. 最常用的两种序列化方案使用心得
关于这两种方案的具体使用和示例没什么好写的,因为优秀的参考资料很多,请看后面给出的相关参考资料,这里只给出我使用时的一些心得,方便大家在选择序列化方案时有个正确的参考,避免选择错误,浪费时间。
4.1 Google Protocol Buffers
protobuf相对而言效率应该是最高的,不管是安装效率还是使用效率,protobuf都很高效,而且protobuf不仅用于C++序列化,还可用于Java和Python的序列化,使用范围很广。但在使用过程中要注意两个问题:
(1)protobuf支持的数据类型不是很丰富
protobuf属于轻量级的,因此不能支持太多的数据类型,下面是protobuf支持的基本类型列表,一般都能满足需求,不过在选择方案之前,还是先看看是否都能支持,以免前功尽弃。同样该表也值得收藏,作为我们在定义类型时做参考。
|
.proto type |
c++ |
notes |
|---|---|---|
|
double |
double |
|
|
float |
float |
|
|
int32 |
int32 |
使用可变长编码方式,负数时不够高效,应该使用sint32 |
|
int64 |
int64 |
同上 |
|
uint32 |
uint32 |
使用可变长编码方式 |
|
uint64 |
uint64 |
同上 |
|
sint32 |
int32 |
使用可变长编码方式,有符号的整型值,编码时比通常的int32高效 |
|
sint64 |
sint64 |
同上 |
|
fixed32 |
uint32 |
总是4个字节,如果数值总是比2^28大的话,这个类型会比uint32高效 |
|
fixed64 |
uint64 |
总是8个字节,如果数值总是比2^56大的话,这个类型会比uint64高效 |
|
sfixed32 |
int32 |
总是4个字节 |
|
sfixed64 |
int64 |
总是8个字节 |
|
bool |
bool |
|
|
string |
string |
一个字符串必须是utf-8编码或者7-bit的ascii编码的文本 |
|
bytes |
string |
可能包含任意顺序的字节数据 |
(2)protobuf不支持二维数组(指针),不支持STL容器序列化
这个缺陷挺大,因为稍复杂点的数据结构或类结构里出现二维数组、二维指针和STL容器(set、list、map等)很频繁,但因为protobuf简单的实现机制,只支持一维数组和指针(用repeated修饰符修饰),不能使用repeated repeated来支持二维数组,也不支持STL,因此在选择该方案之前,一定 要确保你的数据结构里没有这些不支持的类型。
(3)protobuf嵌套后会改变类名称
protobuf支持类的嵌套,即在一个自定义类型中可以定义另一个自定义类型,但注意嵌套的自定义类型在经过protobuf处理后生成的类名称并不是你定义的类名称,而是加上了外层的类名称作为前缀,下面举一个简单的例子:
[cpp] view plaincopy
- message DFA {
- required int32 _size = 1;
- message accept_pair {
- required bool is_accept_state = 1;
- required bool is_strict_end = 2;
- optional string app_name = 3;
- }
- repeated accept_pair accept_states = 2;
- }
那么嵌套中的accept_pair 生成后的类不是accept_pair 而是DFA_accept_pair 。如果不想改类名称,将accept_pair 拿到外面与DFA平行定义即可。
4.2 Boost.Serialization
Boost库是个很庞大的库,功能非常丰富,序列化只是其中的一个小分支,但为了使用Boost的序列化方案,你需要安装整个Boost库,所花费的磁盘空间和时间都很多,同样支持的序列化功能也很强大,既支持二维数组(指针),也支持STL容器,更不需要我们用某种特殊的格式重新定义我们的类结构,其非侵入的性质使得我们无须改动已有的类结构即可序列化,这时非常赞的一个性质。但是由于体积庞大,安装复杂,如果只是简单的序列化,没必要使用该方案,只有protobuf不能满足你的需求时,才应该考虑该方案。
(1)安装boost库遇到的一系列问题
安装boost库本事就是一项很费时的工程,如果期间出现了各种错误,更加耗时耗耐心。我们可以从官网下载Boost库的二进制源码进行安装,安装方法请参考网络或后面我给出的参考资料,下面给出安装时的注意事项:
注意1:要用root权限进行安装,否则会在安装过程中报错,提示权限不足。
注意2:boost库的安装依赖一些环境,通常有Python、bzip2和zlib,它们所在的软件包分别为:
Ubuntu下:
zlib1g-dev libbz2-dev libpython2.7-dev (and libpython3.3-dev)
Fedora/Redhat下: zlib-devel libbz2-devel python-devel (and python3-devel)
这也是安装过程中报错的主要来源。
报错1:如果Python库不完整,可能会报“ fatal error: pyconfig.h: No such file or directory compilation terminated.”错误。解决方法如下:
Fedora系统:sudo yum install python-devel
Ubuntu系统:sudo apt-get install python-dev
报错2:报错 “ libs/iostreams/src/bzip2.cpp:20:56: fatal error: bzlib.h: No such file or directory”,解决方案:
Fedora系统:sudo yum install bzip2-devel
Ubuntu系统或Debian系统:sudo apt-get install libbz2-dev
通常对于这些错误,在Ubuntu系统下一般可以通过sudo apt-get install libboost-all-dev全部解决,但不一定行得通。
(2)安装成功后,如果未指定安装位置,那么默认将会安装到/usr/local/lib和/usr/local/include下,那么我们在使用Boost库进行编译时就需要使用-L和-I参数加上具体的lib和include路径,像下面这样:
g++ -o test boost_test.cpp -I$BOOST_INCLUDE -L$BOOST_LIB -lboost_serialization
如果觉得每次都这样很麻烦,那么可以将我们所要用到的lib和include文件加入到环境变量中,像下面这样:
sudo cp /usr/local/lib/libboost_serialization.* /usr/lib
sudo cp -r /usr/local/include/boost /usr/include
然后在编译时直接g++ -o test boost_test.cpp -lboost_serialization即可。
注意:boost下面有两个序列化lib文件:ibboost_serialization.lib 和 libboost_wserialization.lib,那么这两者有什么区别呢?
其实'w' 表示使用的是宽字符,例如 wchar_t。
(3)boost不尽人意的地方
- 基本类型指针很难序列化,例如int *array,官网上是这么说的: “By default, data types designated primitive by Implementation Levelclass serialization trait are never tracked. If it is desired totrack a shared primitive object through a pointer (e.g. a
longused as a reference count), It should be wrappedin a class/struct so that it is an identifiable type.The alternative of changing the implementation level of alongwould affect alllongs serialized in the wholeprogram - probably not what one would intend.” 也就是说如果你想序列化原生类型的指针,需要给其加上struct或class使其变为类类型再序列化,可见有些麻烦,这样的需求往往也很频繁,鉴于序列化机制的实现原理,boost库暂时还不能很好的支持基本类型的指针序列化。 - 不能序列化变长数组(variable-sized array),会报错说变长数组不是模板类类型。
(4)如果需要定义一个对象数组,如定义含有2个元素的class A对象数组,那么必须用A a[2]定义而不能用对象的指针A *a = new A[2]定义,这样序列化a后默认当作一个A对象处理,因此只能存储一个对象的值,后面的不会存储。
(5)所谓boost很人性的非侵入性质也有一定的条件:如果不想改动原来的类,那么原来的类属性必须是public的,这很容易解释,因为你必须要能在别处访问到这些属性并定义其序列化方式,当然这也在其它地方暴露了类的结构,具有一定的劣势。这样的条件往往很难满足,因为我们定义的类属性一般都是private的,如果是这样,且仍想要使用非侵入性质,那么需要在类中添加以下声明来开放访问给 serialization 库:
friend class boost::serialization::access;
这样的方式比让成员public更好。
文章参考:http://www.360doc.com/content/17/0105/21/24811_620357253.shtml

浙公网安备 33010602011771号