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

KS04-15 类的二进制格式序列化-保存

0
回复
873
查看
[复制链接]
累计签到:41 天
连续签到:1 天
来源: 原创 2019-2-10 13:28:32 显示全部楼层 |阅读模式

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

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

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

版权声明
---------------------------------------------------------------------------------------------------------------------
该文章原创于Qter开源社区(www.qter.org
作者: 女儿叫老白
转载请注明出处!
---------------------------------------------------------------------------------------------------------------------

网页版课程源码 提取码:1uy7

引言
----------------------------------------------------------------------------------------------------------------------
在前面的章节中,我们通过XML、INI格式的配置文件介绍了文本文件的操作方法。在项目、产品开发中,我们经常会将内存对象或数据保存到文件中供他人读取。虽然文本方式的文件可读性比较强,但是存取性能相对要差一些,而二进制格式的文件虽然可读性不好,但是存取速度还是比文本格式的文件要快得多。今天我们来介绍一下如何把一个类对象以二进制格式序列化到文件(保存到文件)。
正文
----------------------------------------------------------------------------------------------------------------------
   我们先给出一套不含序列化接口的类CCountry、CProvince、CCity。这几个类是聚合关系。CCountry含有n个CProvince,而CProvince含有n个CCity。
    我们现在要做的就是给它们添加序列化接口,以便能将这些对象以二进制格式保存到文件中。
    首先我们做了一项准备工作:把前面章节的公共类库改造为basedll。这个库就作为日后其他章节的基础库,可以被后续章节的项目调用。具体改造方法我们不再赘述,大家只要知道这个dll的头文件存放在公共include路径下的base/basedll/baseapi.h。
    既然要序列化到文件,我们就要先从容器的最外层开始:也就是CCountry。因此我们为CCountry增加了序列化接口:

  1. /**  
  2. * @brief 用来把类对象进行二进制方式序列化的函数。本接口内部已经调用QDataStream::setByteOrder(QDataStream::LittleEndian)。
  3. * @param[in] fileName 文件名。  
  4. * @param[in|out] pError 错误信息。  
  5. * @return ESerializeCode枚举值。  
  6. */  
  7. ESerializeCode serializeBinary(const QString& fileName,  QString* pError) const;  
  8. /**
  9. * @brief 用来把类对象进行二进制方式序列化的函数。本接口内部已经调用QDataStream::setByteOrder(QDataStream::LittleEndian)。  
  10. * @param[in] ds 文件流对象。  
  11. * @param[in|out] pError 错误信息。  
  12. * @return ESerializeCode枚举值。
  13.   */  
  14. ESerializeCode serializeBinary(QDataStream& ds, QString* pError)  const;
复制代码
    从上述代码可以看出,我们提供了两个序列化接口。为什么要提供两个呢?通过观察可以看出,这两个接口的参数是不同的,第一个提供文件名,第二个提供QDataStream的流对象。实际上,在开发过程中,我们的对象进行序列化时,不一定都序列化到文件,有时候需要序列化到流中。对象序列化到流中之后,可以保存到文件,也可以保存到数据库或者其他介质中。这样的设计就会带来很多灵活性。
    这两个接口都提供了ESerializeCode类型的返回值,这个ESerializeCode用来区分序列化时不同的错误码。
  1. enum ESerializeCode {
  2.     ESERIALIZECODE_OK  = 0,              /// 正常
  3.     ESERIALIZECODE_FILENOTFOND,         /// 文件不存在
  4.     ESERIALIZECODE_ELEMENT_NOTFOUND,    /// doc元素不存在
  5.     ESERIALIZECODE_SETCONTENT_ERROR,    /// QDomDocument::setContent()调用失败
  6.     ESERIALIZECODE_GRAPHVERSION_NOTRECOGNIZE,   /// 图形版本不识别
  7.     ESERIALIZECODE_OTHERERROR,                  /// 其他错误
  8. };
复制代码
    目前已定义的错误码如上所示。
而且通过接口注释可以知道,这两个接口都在内部调用了字节序[注1]的设置接口:
QDataStream::setByteOrder(QDataStream::LittleEndian)
    这样一来,调用者就无需关注字节序问题了。后续的接口中,我们也采用了这样的设计。
我们来看一下CCountry类的这两个接口实现。
  1. ESerializeCode CCountry::serializeBinary(const  QString& strFileName, QString* pError) const {
  2.     if (0 ==  strFileName.length()) {
  3.         if (NULL  != pError) {
  4.     pError->append(QString::fromLocal8Bit("\n文件名为空"));
  5. }
  6.   
  7.         return  ESERIALIZECODE_FILENOTFOND;
  8.     }
  9.     QFile  file(strFileName);
  10.     if  (!file.open(QFile::WriteOnly | QFile::Truncate)) {
  11.     return  ESERIALIZECODE_FILENOTFOND;
  12.     }

  13.     QDataStream  ds(&file);
  14.   
  15.     ds.setByteOrder(QDataStream::LittleEndian);
  16.   
  17.     ESerializeCode  ret = serializeBinary(ds, pError);
  18.   
  19.     file.close();
  20.     return ret;
  21. }
复制代码
    从该实现可以看出,接口的开头进行入口参数的有效性判断。然后创建一个QFile对象,并且以只写和清空方式打开文件。
然后创建一个QDataStream对象并且将其与QFile对象关联。setByteOrder用来设置该流对象的字节序,我们在这里设置为小端字节序。后面直接调用了CCountry类的另一个序列化接口:
    ESerializeCode serializeBinary(QDataStream&ds, QString* pError) const;
这样我们可以很好的实现代码复用。
  1. ESerializeCode   CCountry::serializeBinary(QDataStream& ds, QString* pError) const  {
  2.   
  3. ds.setByteOrder(QDataStream::LittleEndian);
  4.   
  5.     ds <<  m_strName;
  6.   
  7.     ds << m_strContinent;
  8.   
  9.     quint16 nCount  = m_lstProvinces.size(); // 需要明确指定数据类型,否则跨平台时可能出问题。比如int在各个平台上可能长度不一样。
  10.   
  11.     ds <<  nCount;
  12.   
  13.     QList<CProvince*>::ConstIterator  iteLst = m_lstProvinces.constBegin(); // 因为本函数为const,所以需要调用const类型的接口
  14.   
  15.     ESerializeCode  ret = ESERIALIZECODE_OK;
  16.   
  17.     while (iteLst  != m_lstProvinces.end()) {
  18.   
  19.         ESerializeCode  retcode = (*iteLst)->serializeBinary(ds, pError);
  20.   
  21.         if  (ESERIALIZECODE_OK != retcode) {
  22.   
  23.             ret =  retcode;
  24.   
  25.         }
  26.   
  27.         iteLst++;
  28.   
  29.     }
  30.   
  31.     return ret;
  32.   
  33. }
复制代码
    首先请注意该接口内部第一行代码,我们又重新对流对象设置了字节序。这并非多余,因为该接口有可能被其他代码直接调用,而调用者可能忘记设置字节序。
在上述接口实现代码中,我们对各个成员遍历进行序列化。请注意,仅需要保存必须的成员变量。如果有些变量只需要在内存中存在无需保存到文件,那么这些变量可以不参与序列化。
在对m_lstProvinces这个列表序列化时,我们需要先把其个数进行保存,以便在反序列化时先得到个数用来做循环的次数(反序列化的内容会在后续章节介绍)。请注意观察保存m_lstProvinces的尺寸的代码,我们使用了quint16定义变量,这样做的目的是确保我们保存的数据与将来读到的数据尺寸一致,因为我们可能在不同的平台上进行读取操作。比如,我们在windows保存文件,然后传送到unix机器读取该文件,如果这两个平台上对于size()接口的返回值的数据类型定义不同,而我们直接用ds <<m_lstProvinces.size()进行序列化那就麻烦了。然后我们对m_lstProvinces遍历,对其每个成员(CProvince类)调用序列化接口。
接着我们来看看CProvince类的序列化接口,它同CCountry的接口类似:
  1. ESerializeCode  CProvince::serializeBinary(QDataStream&  ds, QString* pError) const {
  2.   
  3.     ds.setByteOrder(QDataStream::LittleEndian);
  4.   
  5. ds << m_strName;
  6.   
  7.     quint16 nCount = m_lstCities.size(); // 需要明确指定数据类型,否则跨平台时可能出问题。比如int在各个平台上可能长度不一样。
  8.   
  9.     ds << nCount;
  10.   
  11.     QList<CCity*>::ConstIterator iteLst =  m_lstCities.constBegin(); // 因为本函数为const,所以需要调用const类型的接口
  12.   
  13.     while (iteLst != m_lstCities.end()) {
  14.   
  15.         (*iteLst)->serializeBinary(ds, pError);
  16.   
  17.         iteLst++;
  18.   
  19.     }
  20.   
  21.   
  22.     return ESERIALIZECODE_OK;
  23.   
  24. }
复制代码
    CProvince对m_lstCities遍历并调用CCity的序列化接口。
CCity的序列化接口就更简单了:
  1. ESerializeCode  CCity::serializeBinary(QDataStream& ds,  QString* pError) const {
  2.   
  3.     Q_UNUSED(pError);
  4.   
  5.      ds.setByteOrder(QDataStream::LittleEndian);
  6.   
  7.     ds << m_strName;
  8.   
  9.     quint8 byValue = ((NULL != m_pCard) ? true : false);
  10.   
  11.     ds << byValue;
  12.   
  13. if (NULL !=  m_pCard){
  14.   
  15.     m_pCard->serializeBianry(ds,  pError);
  16. }
  17.   
  18.     return ESERIALIZECODE_OK;
  19.   
  20. }
复制代码
    到此为止,我们为大家简单介绍了类的序列化的实现方式。我们来看一下使用它们的示例:
  1. /**
  2. * @brief 初始化数据并持久化.
  3. * @return void
  4. */
  5. void example01(void) {
  6.     CProvince* pProvince = NULL;
  7.   
  8.     CCity* pCity = NULL;
  9.   
  10.     CCountry* pCountry = new CCountry(QString::fromLocal8Bit("中国"));
  11.   
  12.     if (NULL == pCountry) {
  13.          return;
  14.     }
  15.   
  16.     // add province
  17.      {
  18.          pProvince = new CProvince();
  19.         pCountry->addProvince(pProvince);
  20.         pProvince->setCountry(pCountry);
  21.         pProvince->setName(QString::fromLocal8Bit("山东"));
  22.         // add city
  23.         pCity = new CCity();
  24.         pCity->setName(QString::fromLocal8Bit("济南"));
  25.         pCity->setProvince(pProvince);
  26.         pProvince->addCity(pCity);
  27.    
  28.         // add city
  29.         pCity = new CCity();
  30.         pCity->setName(QString::fromLocal8Bit("青岛"));
  31.         pCity->setProvince(pProvince);
  32.          pProvince->addCity(pCity);
  33.      }
  34.      // add province
  35.      {
  36.         pProvince = new CProvince();
  37.          pCountry->addProvince(pProvince);
  38.          pProvince->setCountry(pCountry);
  39.         pProvince->setName(QString::fromLocal8Bit("河北"));
复制代码
    在该示例程序中,我们构造了CCountry对象并向其添加了CProvince对象,也向CProvince对象添加了CCity对象。然后我们将CCountry对象打印输出并序列化到文件。在最后退出程序前,我们将对象显示析构。也请大家看一下CCountry等类的析构函数,我们在这些析构函数中对内存进行释放。
结语
----------------------------------------------------------------------------------------------------------------------
在本节中,我们介绍了通过对一个类进行序列化的设计方案及实现,序列化操作可以对应到文件,也可以对应到数据库、网络等等介质。所以序列化仅仅是指数据流向,这从QDataStream的类名就可以看出来。本节我们将类保存到文件,下一节我们将类从文件中重新构造出来,欢迎关注。

注解
----------------------------------------------------------------------------------------------------------------------
注1:字节序指的是在不同的硬件平台上,变量在内存、磁盘或者网络上的组织形式可能不同。比如一个int32类型的变量一共占有4个字节的内存,在X86平台上在内存中是按照低字节在前、高字节在后的顺序存放,这叫做小端(Little endian),反之则叫大端(Big endian)。


回复

使用道具 举报

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