本帖最后由 baizy77 于 2019-8-19 17:44 编辑
版权声明--------------------------------------------------------------------------------------------------------------------- 作者: 女儿叫老白 转载请注明出处! ---------------------------------------------------------------------------------------------------------------------
网页版课程源码 提取码:1uy7
引言 ---------------------------------------------------------------------------------------------------------------------- 在前面的章节中,我们通过XML、INI格式的配置文件介绍了文本文件的操作方法。在项目、产品开发中,我们经常会将内存对象或数据保存到文件中供他人读取。虽然文本方式的文件可读性比较强,但是存取性能相对要差一些,而二进制格式的文件虽然可读性不好,但是存取速度还是比文本格式的文件要快得多。今天我们来介绍一下如何把一个类对象以二进制格式序列化到文件(保存到文件)。 正文 ---------------------------------------------------------------------------------------------------------------------- 我们先给出一套不含序列化接口的类CCountry、CProvince、CCity。这几个类是聚合关系。CCountry含有n个CProvince,而CProvince含有n个CCity。 我们现在要做的就是给它们添加序列化接口,以便能将这些对象以二进制格式保存到文件中。 首先我们做了一项准备工作:把前面章节的公共类库改造为basedll。这个库就作为日后其他章节的基础库,可以被后续章节的项目调用。具体改造方法我们不再赘述,大家只要知道这个dll的头文件存放在公共include路径下的base/basedll/baseapi.h。 既然要序列化到文件,我们就要先从容器的最外层开始:也就是CCountry。因此我们为CCountry增加了序列化接口:
- /**
- * @brief 用来把类对象进行二进制方式序列化的函数。本接口内部已经调用QDataStream::setByteOrder(QDataStream::LittleEndian)。
- * @param[in] fileName 文件名。
- * @param[in|out] pError 错误信息。
- * @return ESerializeCode枚举值。
- */
- ESerializeCode serializeBinary(const QString& fileName, QString* pError) const;
- /**
- * @brief 用来把类对象进行二进制方式序列化的函数。本接口内部已经调用QDataStream::setByteOrder(QDataStream::LittleEndian)。
- * @param[in] ds 文件流对象。
- * @param[in|out] pError 错误信息。
- * @return ESerializeCode枚举值。
- */
- ESerializeCode serializeBinary(QDataStream& ds, QString* pError) const;
复制代码 从上述代码可以看出,我们提供了两个序列化接口。为什么要提供两个呢?通过观察可以看出,这两个接口的参数是不同的,第一个提供文件名,第二个提供QDataStream的流对象。实际上,在开发过程中,我们的对象进行序列化时,不一定都序列化到文件,有时候需要序列化到流中。对象序列化到流中之后,可以保存到文件,也可以保存到数据库或者其他介质中。这样的设计就会带来很多灵活性。 这两个接口都提供了ESerializeCode类型的返回值,这个ESerializeCode用来区分序列化时不同的错误码。 - enum ESerializeCode {
- ESERIALIZECODE_OK = 0, /// 正常
- ESERIALIZECODE_FILENOTFOND, /// 文件不存在
- ESERIALIZECODE_ELEMENT_NOTFOUND, /// doc元素不存在
- ESERIALIZECODE_SETCONTENT_ERROR, /// QDomDocument::setContent()调用失败
- ESERIALIZECODE_GRAPHVERSION_NOTRECOGNIZE, /// 图形版本不识别
- ESERIALIZECODE_OTHERERROR, /// 其他错误
- };
复制代码 目前已定义的错误码如上所示。 而且通过接口注释可以知道,这两个接口都在内部调用了字节序[注1]的设置接口: QDataStream::setByteOrder(QDataStream::LittleEndian) 这样一来,调用者就无需关注字节序问题了。后续的接口中,我们也采用了这样的设计。 我们来看一下CCountry类的这两个接口实现。 - ESerializeCode CCountry::serializeBinary(const QString& strFileName, QString* pError) const {
- if (0 == strFileName.length()) {
- if (NULL != pError) {
- pError->append(QString::fromLocal8Bit("\n文件名为空"));
- }
-
- return ESERIALIZECODE_FILENOTFOND;
- }
- QFile file(strFileName);
- if (!file.open(QFile::WriteOnly | QFile::Truncate)) {
- return ESERIALIZECODE_FILENOTFOND;
- }
- QDataStream ds(&file);
-
- ds.setByteOrder(QDataStream::LittleEndian);
-
- ESerializeCode ret = serializeBinary(ds, pError);
-
- file.close();
- return ret;
- }
复制代码 从该实现可以看出,接口的开头进行入口参数的有效性判断。然后创建一个QFile对象,并且以只写和清空方式打开文件。 然后创建一个QDataStream对象并且将其与QFile对象关联。setByteOrder用来设置该流对象的字节序,我们在这里设置为小端字节序。后面直接调用了CCountry类的另一个序列化接口: ESerializeCode serializeBinary(QDataStream&ds, QString* pError) const; 这样我们可以很好的实现代码复用。 - ESerializeCode CCountry::serializeBinary(QDataStream& ds, QString* pError) const {
-
- ds.setByteOrder(QDataStream::LittleEndian);
-
- ds << m_strName;
-
- ds << m_strContinent;
-
- quint16 nCount = m_lstProvinces.size(); // 需要明确指定数据类型,否则跨平台时可能出问题。比如int在各个平台上可能长度不一样。
-
- ds << nCount;
-
- QList<CProvince*>::ConstIterator iteLst = m_lstProvinces.constBegin(); // 因为本函数为const,所以需要调用const类型的接口
-
- ESerializeCode ret = ESERIALIZECODE_OK;
-
- while (iteLst != m_lstProvinces.end()) {
-
- ESerializeCode retcode = (*iteLst)->serializeBinary(ds, pError);
-
- if (ESERIALIZECODE_OK != retcode) {
-
- ret = retcode;
-
- }
-
- iteLst++;
-
- }
-
- return ret;
-
- }
复制代码 首先请注意该接口内部第一行代码,我们又重新对流对象设置了字节序。这并非多余,因为该接口有可能被其他代码直接调用,而调用者可能忘记设置字节序。 在上述接口实现代码中,我们对各个成员遍历进行序列化。请注意,仅需要保存必须的成员变量。如果有些变量只需要在内存中存在无需保存到文件,那么这些变量可以不参与序列化。 在对m_lstProvinces这个列表序列化时,我们需要先把其个数进行保存,以便在反序列化时先得到个数用来做循环的次数(反序列化的内容会在后续章节介绍)。请注意观察保存m_lstProvinces的尺寸的代码,我们使用了quint16定义变量,这样做的目的是确保我们保存的数据与将来读到的数据尺寸一致,因为我们可能在不同的平台上进行读取操作。比如,我们在windows保存文件,然后传送到unix机器读取该文件,如果这两个平台上对于size()接口的返回值的数据类型定义不同,而我们直接用ds <<m_lstProvinces.size()进行序列化那就麻烦了。然后我们对m_lstProvinces遍历,对其每个成员(CProvince类)调用序列化接口。 接着我们来看看CProvince类的序列化接口,它同CCountry的接口类似: - ESerializeCode CProvince::serializeBinary(QDataStream& ds, QString* pError) const {
-
- ds.setByteOrder(QDataStream::LittleEndian);
-
- ds << m_strName;
-
- quint16 nCount = m_lstCities.size(); // 需要明确指定数据类型,否则跨平台时可能出问题。比如int在各个平台上可能长度不一样。
-
- ds << nCount;
-
- QList<CCity*>::ConstIterator iteLst = m_lstCities.constBegin(); // 因为本函数为const,所以需要调用const类型的接口
-
- while (iteLst != m_lstCities.end()) {
-
- (*iteLst)->serializeBinary(ds, pError);
-
- iteLst++;
-
- }
-
-
- return ESERIALIZECODE_OK;
-
- }
复制代码 CProvince对m_lstCities遍历并调用CCity的序列化接口。 CCity的序列化接口就更简单了: - ESerializeCode CCity::serializeBinary(QDataStream& ds, QString* pError) const {
-
- Q_UNUSED(pError);
-
- ds.setByteOrder(QDataStream::LittleEndian);
-
- ds << m_strName;
-
- quint8 byValue = ((NULL != m_pCard) ? true : false);
-
- ds << byValue;
-
- if (NULL != m_pCard){
-
- m_pCard->serializeBianry(ds, pError);
- }
-
- return ESERIALIZECODE_OK;
-
- }
复制代码 到此为止,我们为大家简单介绍了类的序列化的实现方式。我们来看一下使用它们的示例: - /**
- * @brief 初始化数据并持久化.
- * @return void
- */
- void example01(void) {
- CProvince* pProvince = NULL;
-
- CCity* pCity = NULL;
-
- CCountry* pCountry = new CCountry(QString::fromLocal8Bit("中国"));
-
- if (NULL == pCountry) {
- return;
- }
-
- // add province
- {
- pProvince = new CProvince();
- pCountry->addProvince(pProvince);
- pProvince->setCountry(pCountry);
- pProvince->setName(QString::fromLocal8Bit("山东"));
- // add city
- pCity = new CCity();
- pCity->setName(QString::fromLocal8Bit("济南"));
- pCity->setProvince(pProvince);
- pProvince->addCity(pCity);
-
- // add city
- pCity = new CCity();
- pCity->setName(QString::fromLocal8Bit("青岛"));
- pCity->setProvince(pProvince);
- pProvince->addCity(pCity);
- }
- // add province
- {
- pProvince = new CProvince();
- pCountry->addProvince(pProvince);
- pProvince->setCountry(pCountry);
- pProvince->setName(QString::fromLocal8Bit("河北"));
复制代码 在该示例程序中,我们构造了CCountry对象并向其添加了CProvince对象,也向CProvince对象添加了CCity对象。然后我们将CCountry对象打印输出并序列化到文件。在最后退出程序前,我们将对象显示析构。也请大家看一下CCountry等类的析构函数,我们在这些析构函数中对内存进行释放。 结语 ---------------------------------------------------------------------------------------------------------------------- 在本节中,我们介绍了通过对一个类进行序列化的设计方案及实现,序列化操作可以对应到文件,也可以对应到数据库、网络等等介质。所以序列化仅仅是指数据流向,这从QDataStream的类名就可以看出来。本节我们将类保存到文件,下一节我们将类从文件中重新构造出来,欢迎关注。
注解 ---------------------------------------------------------------------------------------------------------------------- 注1:字节序指的是在不同的硬件平台上,变量在内存、磁盘或者网络上的组织形式可能不同。比如一个int32类型的变量一共占有4个字节的内存,在X86平台上在内存中是按照低字节在前、高字节在后的顺序存放,这叫做小端(Little endian),反之则叫大端(Big endian)。
|