MFC的连续存储 永久保存(串行化)两篇

MFC的连续存储 永久保存(串行化)(转载)

 

先用一句话来说明永久保存的重要:弄懂它以后,你就越来越像个程序员了!

如果我们的程序不需要永久保存,那几乎可以肯定是一个小玩儿。那怕我们的记事本、画图等小程序,也需要保存才有真正的意义。

对于MFC的很多地方我不甚满意,总觉得它喜欢拿一组低能而神秘的宏来故弄玄虚,但对于它的连续存储(serialize)机制,却是我十分钟爱的地方。在此,可让大家感受到面向对象的幸福。

MFC 的连续存储(serialize)机制俗称串行化。“在你的程序中尽管有着各种各样的数据,serialize机制会象流水一样按顺序存储到单一的文件 中,而又能按顺序地取出,变成各种不同的对象数据。”不知我在说上面这一句话的时候,大家有什么反应,可能很多朋友直觉是一件很简单的事情,只是说了一个    “爽”字就没有下文了。

 

要实现象流水一样存储其实是一个    很大的难题。试想,在我们的程序里有各式各样的对象数据。如画图程序中,里面设计了点类,矩形类,圆形类等等,它们的绘图方式及对数据的处理各不相同,用 它们实现了成百上千的对象之后,如何存储起来?不想由可,一想头都大了:我们要在程序中设计函数store(),在我们单击“文件/保存”时能把各对象往 里存储。那么这个store()函数要神通广大,它能清楚地知道我们设计的是什么样的类,产生什么样的对象。大家可能并不觉得这是一件很困难的事情,程序    有能力知道我们的类的样子,对象也不过是一块初始化了存储区域罢了。就把一大堆对象“转换”成磁盘文件就行了。

 

即使上面的存储能成立,但当我们单击“文件/打开”时,程序当然不能预测用户想打开哪个文件,并且当打开文件的时候,要根据你那一大堆垃圾数据new出数百个对象,还原为你原来存储时的样子,你又该怎么做呢?

试 想,要是我们有一个能容纳各种不同对象的容器,这样,用户用我们的应用程序打开一个磁盘文件时,就可以把文件的内容读进我们程序的容器中。把磁盘文件读进    内存,然后识别它“是什么对象”是一件很难的事情。首先,保存过程不像电影的胶片,把景物直接映射进去,然后,看一下胶片就知道那是什么内容。可能有朋友 说它象录像磁带,拿着录像带我们看不出里面变化的磁场信号,但经过录像机就能把它还原出来。

其实不是这样的,比如保存一个矩形,程序并不是把矩形本身按点阵存储到磁盘中,因为我们绘制矩形的整个过程只不过是调用一个GDI函数罢了。它保存只是坐标值、线宽和某些标记等。程序面对“00 FF”这样的东西,当然不知道它是一个圆或是一个字符!

 

拿 刚才录像带的例子,我们之所以能最后放映出来,前提我们知道这对象是“录像带”,即确定了它是什么类对象。如果我们事先只知道它“里面保存有东西,但不知    道它是什么类型的东西”,这就导致我们无法把它读出来。拿录像带到录音机去放,对录音机来说,那完全是垃圾数据。即是说,要了解永久保存,要对动态创建有 深刻的认识。

现在大家可以知道困难的根源了吧。我们在写程序的时候,会不断创造新的类,构造新的对象。这些对象,当然是旧的类对象(如MyDocument)从未见过的。那么,我们如何才能使文档对象可以保存自己新对象呢,又能动态创建自己新的类对象呢?

许 多朋友在这个时候想起了CObject这个类,也想到了虚函数的概念。于是以为自己“大致了解”串行化的概念。他们设想:“我们设计的MyClass(我 们想用于串行化的对象)全部从CObject类派生,CObject类对象当然是MyDocument能认识的。”这样就实现了一个目的:本来 MyDocument不能识别我们创建的MyClass对象,但它能识别CObject类对象。由于MyClass从CObject类派生,我产的新类对 象“是一个CObject”,所以MyDocument能把我们的新对象当作CObiect对象读出。或者根据书本上所说的:打开或保存文件的时候, MyDocument会调用Serialize(),MyDocument的Serialize()函会呼叫我们创建类的Serialize函数[即是在    MyDocument Serialize()中调用:m_pObject->Serialize(),注意:在此m_pObject是CObject类指针,它可以指向 我们设计的类对象]。最终结果是MyDocument的读出和保存变成了我们创建的类对象的读出和保存,这种认识是不明朗的。

 

由 于MyClass从CObject派生,所以CObject类型指针能指向MyClass对象,并且能够让MyClass对象执行某些函数(特指重载的 CObject虚函数),但前提必须在MyClass对象实例化了,即在内存中占领了一块存储区域之后。不过,我们的问题恰恰就是在应用程序随便打开一个 文件,面对的是它不认识的MyClass类,当然实例化不了对象。

 

幸 好我们在上一节课中懂得了动态创建。即想要从CObject派生的MyClass成为可以动态创建的对象只要用到 DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC宏就可以了(注意:最终可以Serialize的对象仅仅用到了    DECLARE_SERIAL/IMPLEMENT_SERIAL宏,这是因为DECLARE_SERIAL/IMPLEMENT_SERIAL包含了 DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC宏)。

从解决上面的问题中,我们可以分步理解了:

1、 Serialize的目的:让MyDocument对象在执行打开/保存操作时,能读出(构造)和保存它不认的MyClass类对象。

2、 MyDocument对象在执行打开/保存操作时会调用它本身的Serialize()函数。但不要指望它会自动保存和读出我们的MyClass类对象。这个问题很容易解决,就直接在MyDocument::    Serialize(){

// 在此函数调用MyClass类的Serialize()就行了!即

MyObject. Serialize();

}

3、 我们希望MyClass对象为可以动态创建的对象,所以要求在MyClass类中加上DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC宏。

但目前的Serialize机制还很抽象。我们仅仅知道了表面上的东西,实际又是如何的呢?下面作一个简单深刻的详解。

先看一下我们文档类的Serialize()

void CMyDoc::Serialize(CArchive& ar)

{

if (ar.IsStoring())

{

// TOD add storing code here

}

else

{

// TOD add loading code here

}

}

目前这个子数什么也没做(没有数据的读出和写入),CMyDoc类正等待着我们去改写这个函数。现在假设CMyDoc有一个MFC可识别的成员变量m_MyVar,那么函数就可改写成如下形式:

void CMyDoc::Serialize(CArchive& ar)

{

if (ar.IsStoring()) //读写判断

{

ar<

}

else

{

ar>>m_MyVar; //

}

}

许 多网友问:自己写的类(即MFC未包含的类)为什么不行?我们在CMyDoc里包含自写类的头文件MyClass.h,这样CMyDoc就认识 MyClass类对象了。这是一般常识性的错误,MyDoc类认识MyClass类对象与否并没有用,关键是CArchive类,即对象ar不认识 MyClass(当然你梦想重写CArchive类当别论)。“>>”、“<<”都是CArchive重载的操作符。上面 ar>>m_MyVar说白即是在执行一个以ar和m_MyVar 为参数的函数,类似于function(ar,m_MyVar)罢了。我们当然不能传递一个它不认识的参数类型,也因此不会执行function(ar, m_MyObject)了。

[注: 这里我们可以用指针。让MyClass从Cobject派生,一切又起了质的变化,假设我们定义了:MyClass    *pMyClass = new MyClass;因为MyClass从CObject派生,根据虚函数原理,pMyClass也是一个CObject*,即pMyClass指针是 CArchive类可认识的。所以执行上述function(ar,    pMyClass),即ar << pMyClass是没有太多的问题(在保证了MyClass对象可以动态创建的前提下)。]

回 过头来,如果想让MyClass类对象能Serialize,就得让MyClass从CObject派生,Serialize()函数在CObject里 为虚,MyClass从CObject派生之后就可以根据自己的要求去改写它,象上面改写CMyDoc::Serialize()方法一样。这样 MyClass就得到了属于MyClass自己特有的Serialize()函数。

现在,程序就可以这样写:

……

#include “MyClass.h

……

void CMyDoc::Serialize(CArchive& ar)

{

//在此调用MyClass重写过的Serialize()

m_MyObject. Serialize(ar); // m_MyObject为MyClass实例

}

至 此,串行化工作就算完成了,一即简单直观:从CObject派生自己的类,重写Serialize()。在此过程中,我刻意安排:在没有用到 DECLARE_SERIAL/IMPLEMENT_SERIAL宏,也没有用到CArray等模板类的前提下就完成了串行化的工作。我看过某些书,总是    一开始就讲DECLARE_SERIAL/IMPLEMENT_SERIAL宏或马上用CArray模板,让读者觉得串行化就是这两个东西,导致许多朋友 因此找不着北。

大家看到了,没有DECLARE_SERIAL/IMPLEMENT_SERIAL宏和CArray等数据结构模板也依然可以完成串行化工作。

现在可以腾出时间讲一下大家觉得十分抽象的CArchive。我们先看以下程序(注:以下程序包含动态创建等,请包含DECLARE_SERIAL/IMPLEMENT_SERIAL宏)

void MyClass::Serialize(CArchive& ar)

{

if (ar.IsStoring()) //读写判断

{

ar<< m_pMyVar; //问题:ar如何把m_pMyVar所指的对象变量保存到磁盘?

}

else

{

pMyClass = new MyClass; //准备存储空间

ar>> m_pMyVar;

}

}

要回答上面的问题,即“ar<< FONT>

“ar<<<”的重载动作。AR和XXX都是该重载函数中的一参数而已。函数大致如下:

CArchive& operator<<( CArchive& ar, const CObject* pOb)

{

…………

//以下为CRuntimeClass链表中找到、识别pOb资料。

CRuntimeClass* pClassRef = pOb->GetRuntimeClass();

//保存pClassRef即类信息(略)

((CObject*)pOb)->Serialize();//保存MyClass数据

…………

}

从上面可以看出,因为Serialize()为虚函数,即“ar<>XXX”,虽然不是“ar<

至 此,永久保存算是写完了。在此过程中,我一直努力用最少的代码,详尽的解释来说明问题。以前我为本课题写过一个版本,并在几个论坛上发表过,但不知怎么在    网上遗失(可能被删除)。所以这篇文章是我重写的版本。记得第一个版本中,我是对DECLARE_SERIAL/IMPLEMENT_SERIAL和可串    行化的数组及链表对象说了许多。这个版本中我对DECLARE_SERIAL/IMPLEMENT_SERIAL其中奥秘几乎一句不提,目的是让大家能找    到中心,有更简洁的永久保存的概念,我觉得这种感觉很好!

 

 

 

 

 

 

/*********************************************************************************************************/

 

  程序中用到了许多数据类型,特别是结构体和类,自定义的数据类型等。  
  你的程序处理的这种数据就是文档。  
  拿普通编辑器来说,肯定有一个cstring的数据用来存放用户编辑的文字,并且这串文字还有许多特性也需要记录,比如颜色,字体等,那没与此相关的就还有许多变量。  
  序列化的意思就是,如何将这些文档数据存盘,以及如何读取出来显示。那能有两种办法可以做到,一、利用cfile将每个数据依次写入磁盘,读取时又将文件中的数据依次读到变量,这种方式很繁琐,你必须依次做。  
  二、利用cfile和carchive配合,进行读写文件,并统一写为一个函数,例如:  
  mystruct     m_mydata1;  
  int               m_mydata2:  
  cstring       m_mydata3;  
  colorref     m_mydata4;   //等等  
   
  上面假设是一种编辑器要处理的数据,是cmydoc的成员。  
   
  void   cmydoc::serialize(carchive&   ar)  
  {  
        if(   ar.isstoring()   )   //如果是存盘  
        {  
                ar.write(   &m_mydata1,   sizeof(mystruct)   );  
                ar<                 ar<                 ar<                 //上面后三行也可以写为ar<                 //因为<<操作符又将返回一个carchive对象。  
        }  
        else   //如果是读取  
        {  
                ar.read(   &m_mydata1,   sizeof(mystruct)   );  
                ar>>m_mydata2;  
                ar>>m_mydata3;  
                ar>>m_mydata4;  
                //上面后三行也可以写为ar>>m_mydata2>>m_mydata3>>m_mydata4;  
                //因为>>操作符又将返回一个carchive对象。  
        }  
  }  
   
  当需要存盘时:  
  void   cmydoc::savefiletodisk()  
  {  
          cfile   file;  
          file.open(   "yourfile.mdt",   cfile::modewrite|cfile::modecreate   );  
          carchive   ar(   &file,carchive::store   );  
          serialize(   ar   );  
          ar.close();  
          file.close();  
  }  
   
  当需要读取文件时:  
  void   cmydoc::readfilefromdisk()  
  {  
          cfile   file;  
          file.open(   "yourfile.mdt",   cfile::moderead   );  
          carchive   ar(   &file,carchive::load   );  
          serialize(   ar   );  
          ar.close();  
          file.close();  
  }  
   
  这就是我们自己实现的序列化,也就是序列化的原理。  
  应该注意的是,carchive采用>>和<<来读写文件,其操作的数据如果是类,这个类必须从cobject派生,并声明为 declare_serial,如果不从cobject派生,则需要实现>>和<<操作符,比如cstring不从 cobject派生,但它实现了>>和<<操作符。对于结构体,你需要调用carchive::read/write来进行,对 于简单数据类型可以直接使用>>和<<操作符。  
  另外,基于文档/视结构的mfc程序,其框架实现了上述序列化的savefiletodisk和readfilefromdisk(隐藏了),只提供了一个serialize()函数,你只需要在其中加入要序列化的变量。  
   
  以上是序列化的全过程。

转载于:https://www.cnblogs.com/yanglin1228/archive/2012/11/17/4812051.html

你可能感兴趣的:(MFC的连续存储 永久保存(串行化)两篇)