找回密码
 立即注册
收起左侧

KS04-16 类的二进制格式序列化-读取

0
回复
3777
查看
[复制链接]
累计签到:41 天
连续签到:1 天
来源: 原创 2019-7-17 21:04:57 显示全部楼层 |阅读模式

马上注册,查看详细内容!注册请先查看:注册须知

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
本帖最后由 baizy77 于 2019-8-19 17:47 编辑

本帖最后由 baizy77 于 2019-7-16 20:43 编辑


版权声明
---------------------------------------------------------------------------------------------------------------------
该文章原创于Qter开源社区(www.qter.org
作者: 女儿叫老白
转载请注明出处!
---------------------------------------------------------------------------------------------------------------------
引言
------------------------------------------------------------------------------------
上一节我们介绍了将类保存到二进制格式文件的方法,本节我们继续介绍从二进制文件中将对象读取出来的方法。
正文
------------------------------------------------------------------------------------
   将对象从文件中读取出来的过程与将对象保存到文件的过程类似。类似的,我们要为类增加反序列化接口。首先来看一下CCountry类的反序列化接口:
  1. /**
  2. * @brief 用来把类对象进行二进制方式反序列化的函数。
  3. * @param[in] fileName 文件名。
  4. * @return ESerializeCode枚举值。
  5. */
  6. ESerializeCode deSerializeBinary(const QString& fileName, QString* pError);
  7. /**
  8. * @brief 用来把类对象进行二进制方式序列化的函数。
  9. *        打开的文件与保存时采用相同的ByteOrder(本接口内部已经调用QDataStream::setByteOrder)。
  10. * @param[in] ds 文件流对象。
  11. * @param[in|out] pError 错误信息。
  12. * @return ESerializeCode枚举值。
  13. */
  14. ESerializeCode deSerializeBinary(QDataStream& ds, QString* pError);
复制代码
同序列化接口一样,反序列化接口也设计了两个。第一个用文件名作为参数,第二个用流对象作为参数。我们先来看文件名做参数的接口的实现:
  1. ESerializeCode CCountry::deSerializeBinary(const QString& strFileName, QString* pError) {
  2.     clear();
  3.         Q_UNUSED(pError);
  4.         if (strFileName.isEmpty())         {
  5.                 return ESERIALIZECODE_FILENOTFOND;
  6.         }

  7.         QFile file(strFileName);
  8.         if (!file.open(QFile::ReadOnly))        {
  9.                 return ESERIALIZECODE_FILENOTFOND;
  10.         }

  11.         QDataStream ds(&file);
  12.         ds.setByteOrder(QDataStream::LittleEndian);
  13.         QString strError;
  14.         ESerializeCode ret = deSerializeBinary(ds, &strError);
  15.         file.close();

  16.         return ret;
  17. }
复制代码
该接口比较简单,在该接口内部也调用了使用流对象做参数的接口。但是请大家注意,接口的开头调用了一个clear()。这个函数做什么用呢?我们来看一下实现:
  1. void CCountry::clear(void)
  2. {
  3.         QList<CProvince*>::iterator iteLst = m_lstProvinces.begin();
  4.         while (iteLst != m_lstProvinces.end()) {
  5.                 if (NULL != *iteLst) {
  6.                         delete *iteLst;
  7.                 }
  8.                 iteLst++;
  9.         }
  10.         m_lstProvinces.clear();
  11. }
复制代码
   原来它是为了释放内存用的。这个接口也可以在~CCountry()中调用。同样的,我们为CProvince、CCity都添加了这个接口。

我们来看流对象做参数的接口:
  1. ESerializeCode CCountry::deSerializeBinary(QDataStream& ds, QString* pError) {
  2.         clear();
  3. ds.setByteOrder(QDataStream::LittleEndian);
  4.         ESerializeCode retcode = ESERIALIZECODE_OK;
  5.         ds >> m_strName;
  6.         ds >> m_strContinent;
  7.         quint16 nCount = 0; // 需要明确指定数据类型,否则跨平台时可能出问题。比如int在各个平台上可能长度不一样。
  8.         ds >> nCount;
  9.         quint16 idx = 0;
  10.         CProvince* pProvince = NULL;

  11.         for (idx = 0; idx < nCount; idx++) {
  12.                 pProvince = new CProvince();
  13.                 pProvince->deSerializeBinary(ds, pError);
  14.                 addProvince(pProvince);
  15.         }
  16.         return retcode;
  17. }
复制代码
当读取时,我们先读取到CProvince列表的尺寸,这时,就需要和保存时用的数据类型保持一致,都使用了quint16:
  1. quint16 nCount = 0; // 需要明确指定数据类型,否则跨平台时可能出问题。比如int在各个平台上可能长度不一样。
  2. ds >> nCount;
复制代码
    然后,我们使用nCount作为循环周期进行遍历读取,将CProvince对象构造出来并调用其反序列化接口deSerializeBinary()。从文件中读取该对象后,我们调用addProvince()接口将它添加到CCountry。

    我们看一下CProvince的反序列化接口:
  1. ESerializeCode CProvince::deSerializeBinary(QDataStream& ds, QString* pError) {
  2. clear();
  3.         ds.setByteOrder(QDataStream::LittleEndian);
  4.         ESerializeCode retcode = ESERIALIZECODE_OK;
  5.         ds >> m_strName;
  6.         quint16 nCount = 0; // 需要明确指定数据类型,否则跨平台时可能出问题。比如int在各个平台上可能长度不一样。
  7.         ds >> nCount;
  8.         quint16 idx = 0;
  9.         CCity* pCity = NULL;

  10.         for (idx = 0; idx < nCount; idx++) {
  11.                 pCity = new CCity();
  12.                 pCity->deSerializeBinary(ds, pError);
  13.                 addCity(pCity);
  14.         }
  15.         return retcode;
  16. }
复制代码
    该接口同CCountry的反序列化接口过程类似,同样它内部构造了CCity对象并遍历调用其反序列化接口。在CCity的反序列化接口中,我们为其增加了城市名片成员对象。我先来看一下更新后的序列化接口:
  1. ESerializeCode  CCity::serializeBinary(QDataStream& ds, QString* pError) const {
  2.         ds.setByteOrder(QDataStream::LittleEndian);
  3.         ds << m_strName;
  4.         quint8 byValue = ((NULL != m_pCard) ? true : false);
  5.         ds << byValue;
  6.         if (NULL != m_pCard) {
  7.                 m_pCard->serializeBinary(ds, pError);
  8.         }
  9.         return ESERIALIZECODE_OK;
  10. }
复制代码
请看上述代码清单的第5行,为了区分是否设置了城市名片,我们用一个quint8类型的变量byValue来保存该结果。如果有城市名片,byValue=1,否则byValue=0。我们不能直接用bool类型来定义该变量,因为bool类型也不跨平台,在不同的平台上bool类型所占的空间可能不一样。所以我们采用了固定类型、固定长度的的quint8来保存bool值。大家也可以借鉴这样的设计。

现在来看一下反序列化接口:
  1. ESerializeCode CCity::deSerializeBinary(QDataStream& ds, QString* pError) {
  2.         Q_UNUSED(pError);
  3.     clear();
  4.         ds.setByteOrder(QDataStream::LittleEndian);
  5.         ESerializeCode retcode = ESERIALIZECODE_OK;
  6.         ds >> m_strName;

  7.         quint8 byValue = 0;
  8.         ds >> byValue;

  9.         if (byValue) {
  10.                 m_pCard = new CCard();
  11.                 m_pCard->deSerializeBinary(ds, pError);
  12.         }
  13.         return retcode;
  14. }
复制代码
    该接口中,也是同构quint8类型的变量byValue来判断是否设置了城市名片。如果设置了城市名片,则创建城市名片对象m_pCard并执行其反序列化接口从而从文件中读取城市名片信息。

    我们来看一下新增的城市名片类CCard的序列化接口和反序列化接口。这两个接口在CCity类的序列化接口和反序列化接口中被调用。
  1. /**
  2.         * @brief 用来把类对象进行二进制方式序列化的函数。本接口内部已经调用QDataStream::setByteOrder(QDataStream::LittleEndian)。
  3.         * @param[in] ds 文件流对象。
  4.         * @param[in|out] pError 错误信息。
  5.         * @return ESerializeCode枚举值。
  6.         */
  7.         ESerializeCode serializeBinary(QDataStream& ds, QString* /*pError*/) const {
  8.                 ds.setByteOrder(QDataStream::LittleEndian);
  9.                 ds << m_str;
  10.                 return ESERIALIZECODE_OK;
  11. }
  12. /**
  13.         * @brief 用来把类对象进行二进制方式序列化的函数。
  14.         *        打开的文件与保存时采用相同的ByteOrder。本接口内部已经调用QDataStream::setByteOrder(QDataStream::LittleEndian)。
  15.         * @param[in] ds 文件流对象。
  16.         * @param[in|out] pError 错误信息。
  17.         * @return ESerializeCode枚举值。
  18.         */
  19.         ESerializeCode deSerializeBinary(QDataStream& ds, QString* /*pError*/) {
  20.                 ds.setByteOrder(QDataStream::LittleEndian);
  21.                 ds >> m_str;
  22.                 return ESERIALIZECODE_OK;
  23. }
复制代码
CCard类的序列化、反序列化接口比较简单,我们不再详述。
需要注意2个问题:第一,就是在每个类的序列化、反序列化接口中都要设置字节序,目的是当任何一个类对象被独立序列化到流的时候,可以保证字节序的正确性;第二,就是clear()接口必须被调用,目的是当从流中恢复类对象是先清空类的数据,防止类对象带有脏数据。
    到现在为止,我们把类对象的二进制序列化进行了比较详细的介绍。通过这两个章节,我们掌握了类对象序列化到二进制文件的基本方法。但是,使用二进制序列化有一个不可回避的问题,就是兼容性问题。就是新程序怎么打开旧文件的问题,也就是向前兼容。下一章节我们将介绍怎么实现二进制格式文件的向前兼容。
结语
------------------------------------------------------------------------------------

  使用二进制格式进行序列化可以使文件短小精悍而且处理速度相当快,因此在软件研发中得到了非常广泛的应用。希望大家可以通过练习掌握这种技术并将它应用到日常工作中。

上一节:KS04-15类的二进制格式序列化-保存下一节:KS04-17   类的XML格式序列化

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

公告
可以关注我们的微信公众号yafeilinux_friends获取最新动态,或者加入QQ会员群进行交流:190741849、186601429(已满) 我知道了