首页 > 代码库 > 【大话QT之七】QT序列化操作

【大话QT之七】QT序列化操作

应用需求:

        在网盘开发过程中有这样一个需求,即对文件版本进行控制,即记录文件版本的更替信息,这里说的更替信息仅仅是记录不同时刻的文件变化,即文件的增、删、改、重命名等操作。在每个待监控的目录下都会保存一份文件,记录文件变化的增量信息,每次低版本到高版本升级的时候就可以通过消元合并操作快速地进行。关于文件版本控制的具体实现方案会在开发完善后列出,这里仅仅指出它的保存方式,即将文件操作的实例对象序列化后保存在文件中。

序列化的实现:

        这里我们采用QDataStream来实现序列化,QT针对不同的实例化对象有不同的要求。这里主要分两类,即:QT中原生的数据类型,例如:QString、QMap、QHash等,这对这种原生数据类型的序列化,我们不需要做其它额外的操作,直接就可以序列化到文件中。还有一类特殊的就是我们自己定义的数据结构或类,这种方式利用QDataStream不能直接实现序列化,我们必须重载<<和>>操作符,只有重载完之后才可以按我们的要求实现序列化。下面就举例来说明一下,我们自定义的数据结构或类应该如何实现序列化:

自定义的类:LHTFileVersionItem,该类用来记录某一个操作,它的定义为:

#ifndef LHT_FILEVERSIONITEM_H
#define LHT_FILEVERSIONITEM_H

#include <QDataStream>

struct FileVersionItem
{
    QString               m_sFileAbsolutePath ;

    QString               m_sFileOrgName ;
    QString               m_sFileNowName ;

    int               m_sFileType ;

    QString               m_sFileMoveFromAbsolutePath ;
    QString               m_sFileMoveToAbsolutePath ;
};

class LHTFileVersionItem
{
public:
    LHTFileVersionItem();
    ~LHTFileVersionItem();

    void setVersion(int version);
    void setOp(int op);

    int  GetVersion();
    int  GetOp();
    FileVersionItem* GetFileVersionPointer();

    friend QDataStream &operator<<(QDataStream & , const LHTFileVersionItem &);
    friend QDataStream &operator>>(QDataStream & , LHTFileVersionItem &);

private:
    int                   m_iVersion;

    //!-1:delete   1:crate           2:change
    //!3:rename    4:move
    int                   m_iOp;
    FileVersionItem       *m_hFileVersionPointer ;
};

#endif // LHT_FILEVERSIONITEM_H
其中,friend QDataStream &operator << 和 friend QDataStream &operator >>就是声明的对操作符的重载。注意:这里我们使用了friend来声明为友元函数,这里对friend关键字做些介绍:

        何谓友元?为什么要使用友元?我们知道,采用类的机制后实现了数据的隐藏和封装,类的数据成员一般定义为私有成员,成员函数一般定义为公有的,依此提供类与外界间的访问接口。但是,有时需要定义一些函数,注意:这些函数并不是类的一部分(因此在cpp文件中实现该函数时,函数前不需要使用,类名::函数名的方式),但又需要频繁地访问类的私有数据成员,这是可以将这些函数定义为友元函数。除了友元函数外还有友元类,两者统称为友元。友元的作用是提高了程序的运行效率(即:减少了类型检查和安全性检查等,这些操作都需要时间开销),但是它同时也破坏了类的封装性和隐蔽性,使得非成员函数可以访问类的私有成员。

         其实,这里理解友元关注friend就可以了,friend代指“朋友”、“关系友好”的意思,只有两者(函数与类友好形成友元函数;类与类友好形成友元类)关系友好,我才允许它访问我的私有成员。

自定义的类:LHTFileVersionItem的实现:

#include "lht_fileversionitem.h"

LHTFileVersionItem::LHTFileVersionItem()
{
    m_hFileVersionPointer = new FileVersionItem();
}

LHTFileVersionItem::~LHTFileVersionItem()
{
}

void LHTFileVersionItem::setVersion(int version)
{
    this->m_iVersion = version ;
}

void LHTFileVersionItem::setOp(int op)
{
    this->m_iOp = op ;
}

int LHTFileVersionItem::GetVersion()
{
    return this->m_iVersion ;
}

int LHTFileVersionItem::GetOp()
{
    return this->m_iOp ;
}

FileVersionItem* LHTFileVersionItem::GetFileVersionPointer()
{
    return this->m_hFileVersionPointer;
}
//! 重载操作符<<的实现
QDataStream &operator<<(QDataStream &output , const LHTFileVersionItem & item)
{
    output << item.m_iVersion << item.m_iOp << item.m_hFileVersionPointer->m_sFileAbsolutePath <<               item.m_hFileVersionPointer->m_sFileMoveFromAbsolutePath << item.m_hFileVersionPointer->m_sFileMoveToAbsolutePath <<               item.m_hFileVersionPointer->m_sFileNowName << item.m_hFileVersionPointer->m_sFileOrgName ;
    return output ;
}
//! 重载操作符>>的实现
QDataStream &operator>>(QDataStream & input, LHTFileVersionItem & item)
{
    input >> item.m_iVersion >> item.m_iOp >> item.m_hFileVersionPointer->m_sFileAbsolutePath >>              item.m_hFileVersionPointer->m_sFileMoveFromAbsolutePath >> item.m_hFileVersionPointer->m_sFileMoveToAbsolutePath >>              item.m_hFileVersionPointer->m_sFileNowName >> item.m_hFileVersionPointer->m_sFileOrgName ;
    return input ;
}
继承自Object后会出现的问题:

        如果我们自定义的类继承自QObject,在使用时可能会出现这样的编译错误:error C2248 ‘QObject::QObject‘ : cannot access private member declared in class ‘QObject‘如下所示:

为什么加了继承自QObject就会出现这种问题呢,肯定是QObject的问题,查看源码中在private中有这样一句:

private:
    Q_DISABLE_COPY(QObject)
    Q_PRIVATE_SLOT(d_func(), void _q_reregisterTimers(void *))/*   Some classes do not permit copies to be made of an object. These   classes contains a private copy constructor and assignment   operator to disable copying (the compiler gives an error message).*/#define Q_DISABLE_COPY(Class)     Class(const Class &);     Class &operator=(const Class &);
从上面的注释和实现可以看出继承自QObject后它不允许对象的赋值操作,即=,我查找我所有调用的函数里面没有看到直接赋值的操作啊,那为什么会出现这样的问题呢?原因在于在函数调用时形参的传递也会被认为是赋值操作。因此,出现这样问题,直接将QObject的继承去掉就可以,我们没有使用到QObject独特的特定。

局部变量使用对性能的影响以及进程的堆和栈:

        由于在代码中我使用了QMulitHash<QString , LHFilteVersionItem> tmp;这一局部变量来保存某一目录下的文件,由于在写测试代码期间,我利用循环模拟了50万的数据序列化后保存在文件中,在运行期间我发现读取函数耗费很长的时间,而函数里面最耗时的读取操作也只花费了很短的时间,但是函数一直无法立即退出,在等待了大约30s后才能退出,相关代码如下:

void LHTWORKFLOW::ReadAllDataFromFile(QMultiHash<QString, LHTFILEITEM> &m_hFileItemInfo)
{
    if (NULL == m_fFileInfoHandle)
    {
        OpenFile(m_sFileItemInfoAbsolutePath , 0);
    }

    m_fFileInfoHandle->seek(0);

    QDataStream input(m_fFileInfoHandle);

	QMultiHash<QString, LHTFILEITEM> final;
    while (!input.atEnd())
    {
        QMultiHash<QString, LHTFILEITEM> tmp ;
        input >> tmp ;
        final += tmp ;
    }

	m_hFileItemInfo = final ;
    CloseFile(m_fFileInfoHandle);
}
经过仔细分析和思考,发现问题就出在局部变量final上,由于是局部变量,因此当函数执行完毕后局部变量就要销毁,由于是QMultiHash类型的变量,我们知道Hash相比数组来说它的一大好处是数据地址不连续,元素在内存控件中占用的内存地址是不连续的,而数据量又大,因此在销毁的过程中应该是逐步遍历去释放内存指针去了。如果是数组这种连续的数据结构的话,释放会很快,只需要把该块内存的标志设为无用它就又可以被系统回收利用了。这种情况还是我第一次碰到,感到很有意思,对我以后的代码编写也有一定的知道意义,于是我就查阅了进行相关堆和栈的相关内容。

        以下内容的参考链接:http://blog.csdn.net/hairetz/article/details/4141043 ,为了加深理解我这里再列出一点吧。

        1> 预备知识—程序的内存分配

              一个由C++编译的程序占用的内存分为一下几个部分:

              1) 栈区(stack)— 由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

              2) 堆区(heap)— 一般由程序员分配释放,若程序员不释放,程序结束时可能又操作系统回收。

              3) 全局区(静态区)(static) — 全局变量和静态变量的存储是放在一起的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,程序结束和由系统释放。

              4) 文字常量区— 常量字符串就是放在这里的。程序结束后由系统释放。

              5) 程序代码区— 存放函数体的二进制代码。

        2> 堆和栈的对比

              1) 申请方式

                  栈(stack)由系统自动分配。例如,声明在函数中的一个局部变量 int b ; 系统自动在栈中为b开辟空间。

                  堆(heap)需要程序员自己申请,并指明大小,在c中malloc函数如:p1 = (char *)malloc(10) ; 在C++中用new运算符如:p2 = new char[10];

                  但注意:p1、p2本身是在栈中的,只是通过malloc和new分配的空间是在堆中的。

        3> 申请后的系统响应

              栈(stack):只要栈的剩余空间大于所申请的空间,系统将为程序提供内存,否则将报异常提示栈溢出。

              堆(heap):首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆节点,然后将该节点从空闲节点链表中删除,并将该节点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样代码中的delete语句才能正确的释放本内存空间。另外,由于找到堆节点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

         4> 申请大小的限制

               栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶和栈的最大容量是系统预先规定好的,在windows下,栈的大小是2M,如果申请的空间超过栈的剩余空间时,将提示溢出,因此,能从栈获得的空间较小。

               堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统使用链表连存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

         5> 申请效率的比较

               栈:由系统自动分配,速度较快。但程序员是无法控制的。

               堆:是由new分配的内存,最好的方式是用VirtualAlloc分配虚拟内存,它既不是在堆也不是在栈,而是直接在进程的地址空间中保留一块内存,虽然用起来最不方便,但是速度快也最灵活。

          6> 堆和栈中的存储内容

                栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右向左入栈的,然后是函数中的局部变量。注意:静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指定,程序由该点继续运行。

                堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。

总结:

        通过这块内容的分析,对影响程序执行性能的几个方面有了更清除的了解,对进程的堆栈也有了更深入的了解,感觉自己开始慢慢关注那些实质性的东西,这点我感觉是很好的,加油,对每个不懂的问题都要认真总结 。