C++与序列化
序列化概述
什么是序列化
程序员在编写应用程序的时候往往需要将程序的某些数据存储在内存中,然后将其写入某个文件或是将它传输到网络中的另一台计算机上以实现通讯。这个将程序数据转化成能被存储并传输的格式的过程被称为“序列化”(Serialization),而它的逆过程则可被称为“反序列化”(Deserialization)。
简单来说,序列化就是将对象实例的状态转换为可保持或传输的格式的过程。与序列化相对的是反序列化,它根据流重构对象。这两个过程结合起来,可以轻松地存储和传输数据。例如,可以序列化一个对象,然后使用HTTP通过Internet在客户端和服务器之间传输该对象。
总结
- 序列化:将对象变成字节流的形式传出去。
- 反序列化:从字节流恢复成原来的对象。
序列化的好处
简单来说,对象序列化通常用于两个目的:
- 将对象存储于硬盘上,便于以后反序列化使用
- 在网络上传送对象的字节序列
对网络传输而言,显而易见,序列化对象带来了便捷性和灵活性。另一方面,序列化也节省了开发时间。比如:有一个数据结构存储了大量数据,而里面存储的数据又是经过很多其它数据通过非常复杂的算法长时间计算生成的,因此生成该数据结构所用数据的时间要很久。生成该数据结构后又要用作其它的计算。那么在调试阶段,每次运行程序,都需要花费大量时间生成这个数据,无疑代价是非常大的。如果生成数据结构的算法不会变或不常变,那么就可以通过序列化技术生成数据结构数据存储到磁盘上,下次重新运行程序时只需要从磁盘上读取该对象数据即可,而不需要重新计算、生成这些数据,这样大大节省了我们的开发时间。
C++对象序列化方案概述
Google Protocol Buffers(protobuf)
Google Protocol Buffers (GPB)是Google内部使用的数据编码方式,旨在用来代替XML进行数据交换。可用于数据序列化与反序列化。主要特性有:
- 高效
- 语言中立(Cpp, Java, Python)
- 可扩展
- protobuf支持的数据类型不是很丰富,不支持二维数组(指针)、STL容器序列化
Protocol Buffers文档
Boost.Serialization
Boost.Serialization可以创建或重建程序中的等效结构,并保存为二进制数据、文本数据、XML或者有用户自定义的其他文件。该库具有以下吸引人的特性:
- 代码可移植(实现仅依赖于ANSI C++)。
- 深度指针保存与恢复。
- 可以序列化STL容器和其他常用模版库。
- 数据可移植。
- 非入侵性。
Boost Serialization Tutorial
MFC Serialization
Windows平台下可使用MFC中的序列化方法。MFC对CObject 类中的序列化提供内置支持。因此,所有从CObject派生的类都可利用 CObject 的序列化协议。
.Net Framework
.NET的运行时环境用来支持用户定义类型的流化的机制。它在此过程中,先将对象的公共字段和私有字段以及类的名称(包括类所在的程序集)转换为字节流,然后再把字节流写入数据流。在随后对对象进行反序列化时,将创建出与原对象完全相同的副本。
总结
这几种序列化方案各有优缺点,各有自己的适用场景。其中MFC和.Net框架的方法适用范围很窄,只适用于Windows下,且.Net框架方法还需要.Net的运行环境。
Google Protocol Buffers效率较高,但是数据对象必须预先定义,并使用protoc编译,适合要求效率,允许自定义类型的内部场合使用。
Boost.Serialization 使用灵活简单,而且支持标准C++容器。
Google Protocol Buffers详解
protobuf简介
protobuf是google提供的一个开源序列化框架,具有跨平台、跨语言、可扩展特性,类似于XML,JSON这样的数据表示语言,其最大的特点是基于二进制,具有更小的传输体积、更高的编码、解码能力,特别适合于数据存储、网络数据传输等对存储体积、实时性要求高的领域。以 .proto为后缀,有自己的编译器protoc, protoc2 和 protoc3。
优点:
- 语言无关,平台无关
Protobuf支持Java, C++, Python等多种语言,支持多个平台。 - 高效
比XML更小(3~10倍),更快(20 ~ 100倍),更为简单。 - 扩展性,兼容性好
你可以更新数据结构,而不影响和破坏原有的旧程序。
缺点:消息结构可读性不高,序列化后的字节序列为二进制序列不能简单的分析有效性.
Protocol Buffers文档
protobuf使用
1)创建 .proto 文件,定义数据结构
// 在 xxx.proto 文件中定义 Example1 message
package example;
message Example1 {
optional string stringVal = 1;
optional bytes bytesVal = 2;
message EmbeddedMessage {
int32 int32Val = 1;
string stringVal = 2;
}
optional EmbeddedMessage embeddedExample1 = 3;
repeated int32 repeatedInt32Val = 4;
repeated string repeatedStringVal = 5;
}
使用message Example1 {…}定义一个名字为Example1的消息,在其中定义了 message 具有的字段:
message xxx {
// 字段规则:required -> 字段只能也必须出现 1 次
// 字段规则:optional -> 字段可出现 0 次或1次
// 字段规则:repeated -> 字段可出现任意多次(包括 0)
// 类型:int32、int64、sint32、sint64、string、32-bit ....
// 字段编号:0 ~ 536870911(除去 19000 到 19999 之间的数字)
字段规则 类型 名称 = 字段编号;
}
/* 比如optional string stringVal = 1;含义为:
类型 string,名为 stringVal 的 optional 可选字段,
字段编号为 1,此字段可出现 0 或 1 次
*/
2)编译 .proto文件生成读写接口
当需要把这些数据进行存储或传输时,就需要将这些结构数据进行序列化、反序列化以及读写。ProtoBuf 为我们提供了相应的接口代码。如何提供?答案就是通过 protoc 这个编译器。
可通过如下命令生成相应的接口代码:
// $SRC_DIR: .proto 所在的源目录
// --cpp_out: 生成 c++ 代码
// $DST_DIR: 生成代码的目标目录
// xxx.proto: 要针对哪个 proto 文件生成接口代码
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/xxx.proto
protoc.exe example.proto --cpp_out=./
上面命令会在当前目录下生成example.pb.cc和example.pb.h两个文件,其中命名空间example下定义了Example1类,该类继承自public ::google::protobuf::Message。
最终生成的代码将提供类似如下的接口:
//序列化和解析接口
bool SerializeToString(string* output) const;
bool ParseFromString(const string& data);
bool SerializeToOstreamostream* output) const;
bool ParseFromIstream(istream* data);
//protoc生成接口
void Clear_int32val();
void set_int32val(::google::protobuf::int32 value);
::google::protobuf::int32 int32val() const;
void Clear_stringval();
void set_stringval(const char* value);
const ::std::string& stringval() const;
3)调用接口实现序列化、反序列化以及读写
#include <iostream>
#include <fstream>
#include <string>
#include "example.pb.h"
using namespace example;
int main() {
Example1 example1;
example1.set_stringval("hello,world");
example1.set_bytesval("are you ok?");
Example1_EmbeddedMessage *embeddedExample2 = new Example1_EmbeddedMessage();
embeddedExample2->set_int32val(1);
embeddedExample2->set_stringval("embeddedInfo");
example1.set_allocated_embeddedexample1(embeddedExample2);
example1.add_repeatedint32val(2);
example1.add_repeatedint32val(3);
example1.add_repeatedstringval("repeated1");
example1.add_repeatedstringval("repeated2");
std::string filename = "single_length_delimited_all_example1_val_result";
std::fstream output(filename, std::ios::out | std::ios::trunc | std::ios::binary);
if (!example1.SerializeToOstream(&output)) {
std::cerr << "Failed to write example1." << std::endl;
exit(-1);
}
return 0;
}
protobuf编解码
编码结构
对于序列化后字节流,需要回答的一个重要问题是“从哪里到哪里是哪个数据成员”。
field
在protobuf中,message中每一个field的格式为:
required/optional/repeated FieldType FieldName = FieldNumber(a unique number in current message)
在序列化时,一个field对应一个key-value对,整个二进制文件就是一连串紧密排列的key-value对,这就是一个数据成员。key也称为tag:
准确地说,一个field是Tag - Length - Value。Tag 作为该字段的唯一标识,Length 代表 Value 数据域的长度,最后的 Value 并是数据本身。但是Length实际上是可选的,Tag中包含Value使用的编码方式信息,在Varint编码的方式中,不需要使用Length字段,Value的边界可以由另一种方式表示。
tag
Tag由field_number和wire_type两个部分组成:
- field_number: message 定义字段时指定的字段编号;
- wire_type: ProtoBuf 编码类型,根据这个类型选择不同的 Value 编码方案。
tag= (field_number << 3) | wire_type
key的最低3个bit为wire type,3 bit 的 wire_type 最多可以表达 8 种编码类型,目前 ProtoBuf 已经定义了 6 种:
其中的 Start group 和 End group 两种类型已被遗弃。
根据wire type的取值不同(采用的编码不同),确认接下来value部分的长度(字节数)的方法也不同:
-
wire type = 0、1、5,编码为 key + 数据,只有一个数据,可能占数个字节,数据在编码时自带终止标记;
-
wire type = 2,编码为 key + length + 数据,length指示了数据长度,可能有多个数据,顺序排在length后。
另外,虽然 wire_type 代表编码类型,但是Varint这个编码类型里针对sint32、sint64又会有一些特别编码(ZigTag 编码)处理,相当于 Varint 这个编码类型里又存在两种不同编码。
一个具体的编码结构实例如下:
正整数Varints 编码
varint是一种可变长编码,使用1个或多个字节对整数进行编码,可编码任意大的整数,小整数占用的字节少,大整数占用的字节多,如果小整数更频繁出现,则通过varint可实现压缩存储。
varint中每个字节的最高位bit称之为most significant bit (MSB),如果该bit为0意味着这个字节为表示当前整数的最后一个字节,如果为1则表示后面还有至少1个字节,可见,varint的终止位置其实是自解释的。
Varints 的本质实际上是每个字节都牺牲一个 bit 位(msb),来表示是否已经结束(是否还需要读取下一个字节),msb 实际上就起到了 Length 的作用,正因为有了 msb(Length),所以我们可以摆脱原来那种无论数字大小都必须分配四个字节的窘境。通过 Varints 我们可以让小的数字用更少的字节表示。从而提高了空间利用和效率。每个字节都拿出一个 bit 做 msb,而原先这个 bit 是可直接用来表示 Value 的,现在每个字节都少了一个 bit 位即只有 7 位能真正用来 表达 Value。那就意味这 4 个字节能表达的最大数字为 2^28,而不 再是 2^32 了。 这意味着当数字大于 2^28 时,采用 Varints 编码将导致分配 5 个字节, 而原先明明只需要 4 个字节,此时 Varints 编码的效率不仅不是提高 反而是下降。 但这并不影响 Varints 在实际应用时的高效,因为在大多数情况下, 小于 2^ 28 的数字比大于 2^28 的数字出现的更为频繁。
int32 val = 1; // int32 的字段的值 val = 1; 编码的结果如下
原码:0000 ... 0000 0001 // 1 的原码表示
补码:0000 ... 0000 0001 // 1 的补码表示
Varints 编码:0#000 0001(0x01) // 1 的 Varints 编码
//其中第一个字节的 msb = 0
//表示接下来没有字节了
//用一个字节完成了四字节变量的编码
在Protobuf中,tag和length都是使用varint编码的。length和tag中的field_number都是正整数int32。这里提一下tag,它的低3位bit为wire type,如果只用1个字节表示的话,最高位bit为0,则留给field_number只有4个bit位,1到15,如果field_number大于等于16,就需要用2个字节,所以对于频繁使用的field其field_number应设置为1到15。
负整数的Varints 编码—ZigZag 编码
对于小的正整数,上述编码方式可以节约空间。但负数而言,则没有了这个优势:
int32 val = -1
原码:1000 ... 0001 // 注意这里是 8 个字节
补码:1111 ... 1111 // 注意这里是 8 个字节
//Varints 编码:对补码取 7 bit 一组,低位放在前面。
//上述补码 8 个字节共 64 bit,可分 9 组且这 9 组均为 1,
//这 9 组的 msb 均为 1(因为还有最后一组)
//最后剩下一个 bit 的 1,用 0 补齐作为最后一组放在最后,
//最后得到 Varints 编码
Varints 编码:1#1111111 ... 0#000 0001 (FF FF FF FF FF FF FF FF FF 01)
因为负数必须在最高位(符号位)置 1,这一点意味着无论如何,负数都必须占用所有字节,所以它的补码总是占满 8 个字节。对负数没法像正数那样去掉多余的高位(都是 0)。再加上 msb,最终 Varints 编码的结果将固定在 10 个字节。
对int32而言,ProtoBuf 基于兼容性的考虑(比如开发者将 int64 的字段改成 int32 后应当不影响旧程序),将 int32 扩展成 int64 的八个字节。
为了解决Varints 编码对负数编码效率低的问题,ProtoBuf 为我们提供了 sint32、sint64 两种类型,当使用这两种类型定义字段时,ProtoBuf 将使用 ZigZag 编码。ZigZag 编码将有符号整数映射到无符号整数,然后再使用 Varints 编码:
sint32 n被编码为 (n << 1) ^ (n >> 31)对应的varint,sint64 n被编码为 (n << 1) ^ (n >> 63)对应的varint,这样,绝对值较小的整数只需要较少的字节就可以表示。
bool值的Varints 编码
对于boolVal = ture ,当作正整数1处理。
对boolVal = ture 时其编码结果为空。这里是 ProtoBuf 为了提高效率做的一个小技巧:规定一个默认值机制,当读出来的字段为空的时候就设置字段的值为默认值。而 bool 类型的默认值为 false。
Length-delimited相关类型
主要有3类:string、EmbeddedMessage以及packed repeated fields。它们的编码方式统一为 tag + length + 数据,只是数据部分有所差异。
string的编码为 key + length + 字符。
EmbeddedMessage直接将嵌套message部分的编码接在length后即可,如下所示:
message Test1 {
optional int32 a = 1;
}
message Test3 {
optional Test1 c = 3;
}
// cpp file
// set a = 150
// message Test3 binary file, in hex
// 1a 03 08 96 01
//其中,1a为c的key,03为c的长度,接下来的08 96 01为a的key+value。
packed repeated fields,指的是proto2中声明了[packed=true]的repeated varint、32bit or 64bit数据,proto3中repeated默认packed,如下所示:
// in proto2
message Test4 {
repeated int32 d = 4 [packed=true];
}
// in proto3
message Test4 {
repeated int32 d = 4;
}
// 3, 270, 86942压缩存储如下,in hex
22 // key (field number 4, wire type 2), 0x22 = 34 = (4 << 3) + 2
06 // payload size (6 bytes), length
03 // first element (varint 3)
8E 02 // second element (varint 270)
9E A7 05 // third element (varint 86942)
原先的 repeated 字段的编码结构为 Tag-Length-Value-Tag-Length-Value-Tag-Length-Value…,因为这些 Tag 都是相同的(同一字段),因此可以将这些字段的 Value 打包,即将编码结构变为 Tag-Length-Value-Value-Value…
protobuf序列化源码分析
在 .proto 文件中定义的一个 message 在最终生成的C++代码中,构造成一个message 消息类,且这些 message 消息类继承于 ::google::protobuf::Message,而 ::google::protobuf::Message 继承于一个更为轻量的 MessageLite 类,序列化函数 SerializeToString 就是定义在基类 MessageLite 中:
调用SerializeToString
当某个 Message 调用 SerializeToString 时,经过一层层调用最终会调用底层的关键编码函数 WriteVarint32ToArray 或 WriteVarint64ToArray:
inline uint8* CodedOutputStream::WriteVarint32ToArray(uint32 value, uint8* target) {
// 0x80 -> 1000 0000
// 大于 1000 0000 意味这进行 Varints 编码时至少需要两个字节
// 如果 value < 0x80,则只需要一个字节,编码结果和原值一样,则没有循环直接返回
// 如果至少需要两个字节
while (value >= 0x80) {
// 如果还有后续字节,则 value | 0x80 将 value 的最后字节的最高 bit 位设置为 1,并取后七位
*target = static_cast<uint8>(value | 0x80);
// 处理完七位,后移,继续处理下一个七位
value >>= 7;
// 指针加一,(数组后移一位)
++target;
}
// 跳出循环,则表示已无后续字节,但还有最后一个字节
// 把最后一个字节放入数组
*target = static_cast<uint8>(value);
// 结束地址指向数组最后一个元素的末尾
return target + 1;
}
// Varint64 同理
inline uint8* CodedOutputStream::WriteVarint64ToArray(uint64 value,
uint8* target) {
while (value >= 0x80) {
*target = static_cast<uint8>(value | 0x80);
value >>= 7;
++target;
}
*target = static_cast<uint8>(value);
return target + 1;
}
// WriteTagToArray 函数将 Tag 部分写入
// WriteInt32NoTagToArray 函数将 Value 部分写入
// WriteTagToArray 和 WriteInt32NoTagToArray 底层
// 均调用 coded_stream.h 中的 WriteVarint32ToArray
//因为 ProtoBuf 中的 Tag 均采用 Varint 编码
// int32 的 Value 部分也采用 Varint 编码
inline uint8* WireFormatLite::WriteInt32ToArray(int field_number, int32 value,
uint8* target) {
target = WriteTagToArray(field_number, WIRETYPE_VARINT, target);
return WriteInt32NoTagToArray(value, target);
}
inline uint32 WireFormatLite::ZigZagEncode32(int32 n) {
// 右移为算数右移
// 左移时需要先将 n 转成 uint32 类型,防止溢出
// 当 n 为正数时 result = 2 * n
// 当 n 为负数时 result = - (2 * n + 1)
return (static_cast<uint32>(n) << 1) ^ static_cast<uint32>(n >> 31);
}
Boost.Serialization详解
Boost.Serialization的两种模式
Boost序列化可以分为两种模式:侵入式(intrusive)和非侵入式 (non-intrusive)
- 侵入式(intrusive)
侵入式序列化时,需要在class里面加入序列化的代码,序列化的步骤大致如下:
- 先引用 boost 头文件
- 在类的声明中, 编写序列化函数,该函数的格式如下:
template<class Archive>
void serialize(Archive & ar, const unsigned int version)
//version是版本号
{
ar& m_str;
}
- 类的实例化和赋值
- 定义一个序列化的对象和数据流的写入
boost::archive::text_oarchive text_oa(text_sstream);//文本方式
boost::archive::binary_oarchive binary_oa(binary_sstream);//二进制方式
binary_oa << info;//将对象info的序列化数据以二进制存储形式写入内存
- 非侵入式(non-intrusive)
如果class是早已存在的,且我们不想再改变class里面的代码时,这个时候,我们可以使用非侵入式的序列化。非侵入式序列化时,序列化函数需要访问数据成员,这就要求将class的数据成员暴露出来,即public,而不是private。其序列化的步骤和上面的侵入式序列化步骤一致。
Boost.Serialization的使用
txt文本格式序列化实例
- 使用<<和>>运算符完成序列化和反序列化
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <iostream>
#include <fstream>
void save()
{
std::ofstream file("archive.txt");
boost::archive::text_oarchive oa(file);
std::string s = "Hello World!\n";
oa << s;
}
void load()
{
std::ifstream file("archive.txt");
boost::archive::text_iarchive ia(file);
std::string s;
ia >> s;
std::cout << s << std::endl;
}
int main()
{
save();
load();
}
- 使用&运算符完成序列化和反序列化
#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
#include <iostream>
#include <fstream>
void save()
{
std::ofstream file("archive.txt");
boost::archive::text_oarchive oa(file);
std::string s = "Hello World!\n";
oa & s; // has same effect as oa << s;
}
void load()
{
std::ifstream file("archive.txt");
boost::archive::text_iarchive ia(file);
std::string s;
ia & s;
std::cout << s << std::endl;
}
XML格式序列化实例
步骤上与txt文本不同的地方在于,需要将数据打包到一个名为 BOOST_SERIALIZATION_NVP 的宏中。
#include <boost/archive/xml_iarchive.hpp>
#include <boost/archive/xml_oarchive.hpp>
#include <iostream>
#include <fstream>
void save()
{
std::ofstream file("archive.xml");
boost::archive::xml_oarchive oa(file);
std::string s = "Hello World!\n";
oa & BOOST_SERIALIZATION_NVP(s);
}
void load()
{
std::ifstream file("archive.xml");
boost::archive::xml_iarchive ia(file);
std::string s;
ia & BOOST_SERIALIZATION_NVP(s);
std::cout << s << std::endl;
}
XML文档中存储内容如下:
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!DOCTYPE boost_serialization>
<boost_serialization signature="serialization::archive" version="9">
<s>Hello World!</s>
</boost_serialization>
数组的序列化
过程与字符串类一样;但在恢复过程中,需要指定预期的数组大小:
#include <boost/archive/xml_oarchive.hpp>
#include <boost/archive/xml_iarchive.hpp>
#include <iostream>
#include <fstream>
#include <algorithm>
#include <iterator>
void save()
{
std::ofstream file("archive.xml");
boost::archive::xml_oarchive oa(file);
int array1[] = {34, 78, 22, 1, 910};
oa & BOOST_SERIALIZATION_NVP(array1);
}
void load()
{
std::ifstream file("archive.xml");
boost::archive::xml_iarchive ia(file);
int restored[5]; // Need to specify expected array size
ia >> BOOST_SERIALIZATION_NVP(restored);
std::ostream_iterator<int> oi(std::cout, " ");
std::copy(a, a+5, oi);
}
int main()
{
save();
load();
}
XML文档中存储内容如下:
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!DOCTYPE boost_serialization>
<boost_serialization signature="serialization::archive" version="9">
<array1>
<count>5</count>
<item>34</item>
<item>78</item>
<item>22</item>
<item>1</item>
<item>910</item>
</array1>
</boost_serialization>
**STL集合的序列化 **
对于列表和向量,在将信息加载回来的过程中,不需要提供任何大小和范围信息:
#include <boost/archive/xml_oarchive.hpp>
#include <boost/archive/xml_iarchive.hpp>
#include <boost/serialization/list.hpp>
#include <boost/serialization/vector.hpp>
#include <iostream>
#include <fstream>
#include <algorithm>
#include <iterator>
void save()
{
std::ofstream file("archive.xml");
boost::archive::xml_oarchive oa(file);
float array[] = {34.2, 78.1, 22.221, 1.0, -910.88};
std::list<float> L1(array, array+5);
std::vector<float> V1(array, array+5);
oa & BOOST_SERIALIZATION_NVP(L1);
oa & BOOST_SERIALIZATION_NVP(V1);
}
void load()
{
std::ifstream file("archive.xml");
boost::archive::xml_iarchive ia(file);
std::list<float> L2;
ia >> BOOST_SERIALIZATION_NVP(L2); // No size/range needed
std::vector<float> V2;
ia >> BOOST_SERIALIZATION_NVP(V2); // No size/range needed
std::ostream_iterator<float> oi(std::cout, " ");
std::copy(L2.begin(), L2.end(), oi);
std::copy(V2.begin(), V2.end(), oi);
}
XML文档中存储内容如下:
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!DOCTYPE boost_serialization>
<boost_serialization signature="serialization::archive" version="9">
<L1>
<count>5</count>
<item_version>0</item_version>
<item>34.200001</item>
<item>78.099998</item>
<item>22.221001</item>
<item>1</item>
<item>-910.88</item>
</L1>
<V1>
<count>5</count>
<item_version>0</item_version>
<item>34.200001</item>
<item>78.099998</item>
<item>22.221001</item>
<item>1</item>
<item>-910.88</item>
</V1>
</boost_serialization>
自定义类型序列化
typedef struct date {
unsigned int m_day;
unsigned int m_month;
unsigned int m_year;
} date;
- 侵入式版本:在类定义中定义一个名为 serialize 的方法:
template<class Archive>
void serialize(Archive& archive, const unsigned int version)
{
archive & BOOST_SERIALIZATION_NVP(m_day);
archive & BOOST_SERIALIZATION_NVP(m_month);
archive & BOOST_SERIALIZATION_NVP(m_year);
}
完整代码如下:
#include <boost/archive/xml_oarchive.hpp>
#include <boost/archive/xml_iarchive.hpp>
#include <iostream>
#include <fstream>
typedef struct date {
unsigned int m_day;
unsigned int m_month;
unsigned int m_year;
date( int d, int m, int y) : m_day(d), m_month(m), m_year(y)
{}
date() : m_day(1), m_month(1), m_year(2000)
{}
friend std::ostream& operator << (std::ostream& out, date& d)
{
out << "day:" << d.m_day
<< " month:" << d.m_month
<< " year:" << d.m_year;
return out;
}
template<class Archive>
void serialize(Archive& archive, const unsigned int version)
{
archive & BOOST_SERIALIZATION_NVP(m_day);
archive & BOOST_SERIALIZATION_NVP(m_month);
archive & BOOST_SERIALIZATION_NVP(m_year);
}
} date;
void save()
{
std::ofstream file("archive.xml");
boost::archive::xml_oarchive oa(file);
date d(15, 8, 1947);
oa & BOOST_SERIALIZATION_NVP(d);
}
void load()
{
std::ifstream file("archive.xml");
boost::archive::xml_iarchive ia(file);
date dr;
ia >> BOOST_SERIALIZATION_NVP(dr);
std::cout << dr;
}
- 非侵入式版本:
namespace boost {
namespace serialization {
template<class Archive>
void serialize(Archive& archive, date& d, const unsigned int version)
{
archive & BOOST_SERIALIZATION_NVP(d.m_day);
archive & BOOST_SERIALIZATION_NVP(d.m_month);
archive & BOOST_SERIALIZATION_NVP(d.m_year);
}
} // namespace serialization
} // namespace boost
XML文档中存储内容如下:
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!DOCTYPE boost_serialization>
<boost_serialization signature="serialization::archive" version="9">
<d class_id="0" tracking_level="0" version="0">
<d.m_day>15</d.m_day>
<d.m_month>8</d.m_month>
<d.m_year>1947</d.m_year>
</d>
</boost_serialization>
序列化版本控制
#include "boost/archive/binary_oarchive.hpp"
#include "boost/archive/binary_iarchive.hpp"
#include <iostream>
#include <sstream>
#include <fstream>
#define MAN 1
#define WOMAN 2
#define UNKNOW 3
class CPerson
{
public:
CPerson()
{
;
}
CPerson(size_t nBirthYear, std::string strName, size_t nGender)
: m_nBirthYear(nBirthYear), m_strName(strName), m_nGender(nGender)
{
;
}
~CPerson()
{
;
}
size_t GetBirYear() const
{
return m_nBirthYear;
}
std::string GetName() const
{
return m_strName;
}
size_t m_GetGender() const
{
return m_nGender;
}
private:
//要想序列化用户定义的类型 必须定义seriallze函数
//serialzation不应该被显示调用 因为它仅在序列化或者反序列化的时候调用
//应该声明为private的友元函数(不然boost::serialization访问不到)
friend class boost::serialization::access;
//serialize()提供了&操作符
//使用&操作符就不需要再serialize中区分是序列化还是反序列化了
template <typename Archive>
void serialize(Archive &ar, CPerson &p, const unsigned int version);
private:
size_t m_nBirthYear;
size_t m_nGender;
std::string m_strName;
};
template<typename Archive>
void CPerson::serialize(Archive & ar, CPerson &p, const unsigned int version)
{
ar & p.m_nBirthYear;
ar & p.m_strName;
/*版本1之后存储了以下变量 低版本则使用默认值*/
if (version >= 1)
{
ar & p.m_nGender;
}
else
{
p.m_nGender = UNKNOW;
}
}
BOOST_CLASS_VERSION(CPerson, 1);
void Save()
{
std::ofstream File("Data.bin");
boost::archive::binary_oarchive oa(File);
std::cout << "请输入名字" << std::endl;
std::string strName;
std::cin >> strName;
std::cout << "请输入出生年份" << std::endl;
size_t nBirthYear = 0;
std::cin >> nBirthYear;
std::cout << "请输入性别 1.男性 2.女性 3.未知" << std::endl;
size_t nGender = 0;
std::cin >> nGender;
CPerson p(nBirthYear, strName, nGender);
oa << p;
File.close();
}
void Load()
{
std::ifstream File("Data.bin");
boost::archive::binary_iarchive ia(File);
CPerson p;
ia >> p;
std::cout << "姓名:"<< p.GetName()
<< " 出生年份:" << p.GetBirYear()
<< " 性别代码:" <<p.m_GetGender()
<< std::endl;
File.close();
}
int main()
{
int nTemp = 0;
std::cout << "1.输入并存储 2.读取并显示" << std::endl;
std::cin >> nTemp;
switch (nTemp)
{
case 1:
Save();
break;
case 2:
Load();
break;
default:
break;
}
system("pause");
return 0;
}
版本控制的意义在于:随着class的参数的扩充,版本控制可以兼容旧版本的数据。version的值由宏BOOST_CLASS_VERSION指定,需要两个参数:一个是类名,一个是版本号。版本号默认为0,版本号只支持整型1个字节(0 - 255)。设置版本号之后,序列化会存储版本号,这样就能控制读取变量了。
protobuf 与 Boost.Serialization 的比较
-
protobuf:
轻量级的,支持的数据类型有限,且数据对象必须预先定义,使用 protoc 编译,但其效率较高,适合要求效率,允许自定义类型的内部场合使用。 -
Boost.Serialization
Boost 库非常庞大,功能丰富,Boost.Serialization序列化只是其中的一个小分支,但就算只使用序列化,也需要安装整个Boost库,其支持的序列化功能强大,既支持二维数组(指针),也支持STL容器,序列化使用灵活简。
Qt中的序列化
Qt中提供了二进制格式的序列化。对QT中原生的数据类型,例如:QString、QMap、QHash等,不需要做其它额外的操作,利用QDataStream直接就可以序列化到文件中。
//序列化
QFile file("file.dat"); //创建一个文档
file.open(QIODevice::WriteOnly);//打开并只写
QDataStream out(&file); //序列化文档
out << QString("the answer is");//将字符串写入文档中,实际在该文档中为二进制
out << (qint32)42;
//反序列化
QFile file("file.dat");
file.open(QIODevice::ReadOnly);
QDataStream in(&file);
QString str;
qint32 a;
in >> str >> a;
对于自定义的数据结构或类,利用QDataStream不能直接实现序列化,我们必须重载<<和>>操作符:
class QSampleData : public QObject
{
public:
QChunnelData();
virtual ~QChunnelData();
QChunnelData& operator=(const QChunnelData &other);
friend QDataStream& operator>>(QDataStream&, QChunnelData&);
friend QDataStream& operator<<(QDataStream&, QChunnelData&);
//定义的数据成员
int m_nType;
QString m_strName;
};
CSampleData::CSampleData()
{
m_nType = 0;
m_strName = "";
}
CSampleData::~CSampleData()
{
}
CSampleData::operator =(const CSampleData& other)
{
m_nType = other.m_nType;
m_strName = other.m_strName;
return *this;
}
QDataStream& operator>>(QDataStream& in, CSampleData& data)
{
in >> data.m_nType >> data.m_strName;
return in;
}
QDataStream& operator<<(QDataStream& out, CSampleData& data)
{
out << data.m_nType << data.m_strName;
return out;
}