baizy77 发表于 2019-2-10 13:28:32

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

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

版权声明---------------------------------------------------------------------------------------------------------------------该文章原创于Qter开源社区(www.qter.org)作者: 女儿叫老白转载请注明出处!---------------------------------------------------------------------------------------------------------------------课程目录: 【独家连载】《Qt入门与提高-GUI产品开发》目录
网页版课程源码 提取码: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 fileName 文件名。
* @param pError 错误信息。
* @return ESerializeCode枚举值。
*/
ESerializeCode serializeBinary(const QString& fileName,QString* pError) const;
/**
* @brief 用来把类对象进行二进制方式序列化的函数。本接口内部已经调用QDataStream::setByteOrder(QDataStream::LittleEndian)。
* @param ds 文件流对象。
* @param 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(constQString& strFileName, QString* pError) const {
    if (0 ==strFileName.length()) {
      if (NULL!= pError) {
    pError->append(QString::fromLocal8Bit("\n文件名为空"));
}

      returnESERIALIZECODE_FILENOTFOND;
    }
    QFilefile(strFileName);
    if(!file.open(QFile::WriteOnly | QFile::Truncate)) {
    returnESERIALIZECODE_FILENOTFOND;
    }

    QDataStreamds(&file);

    ds.setByteOrder(QDataStream::LittleEndian);

    ESerializeCoderet = 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*>::ConstIteratoriteLst = m_lstProvinces.constBegin(); // 因为本函数为const,所以需要调用const类型的接口

    ESerializeCoderet = ESERIALIZECODE_OK;

    while (iteLst!= m_lstProvinces.end()) {

      ESerializeCoderetcode = (*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的接口类似:ESerializeCodeCProvince::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的序列化接口就更简单了:ESerializeCodeCCity::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)。

上一节:KS04-14   配置文件-INI格式上一节:KS04-16   类的二进制格式序列化-读取
页: [1]
查看完整版本: KS04-15 类的二进制格式序列化-保存