玩转C科技.NET

从学会做人开始认识这个世界!http://volnet.github.io

导航

文档序列化(理论+实际)

一、文档的基本特征

文档类文件是从CDocument继承而来的。

The CDocument class provides the basic functionality for user-defined document classes. A document represents the unit of data that the user typically opens with the File Open command and saves with the File Save command.

翻译:文档类提供用户自定义文档类的基本功能。一个文档的打开命令和保存命令是数据单元特征。

CDocument supports standard operations such as creating a document, loading it, and saving it. The framework manipulates documents using the interface defined by CDocument.

翻译:文档类支持标准操作有:创建文档,载入文档,和保存。框架界面使用由文档类定义的界面。

An application can support more than one type of document; for example, an application might support both spreadsheets and text documents. Each type of document has an associated document template; the document template specifies what resources (for example, menu, icon, or accelerator table) are used for that type of document. Each document contains a pointer to its associated CDocTemplate object.

翻译:一个应用程序可以支持多于一种类型的文档;例如,一个应用程序可能同时支持电子表格和普通文本文档。每一种类型的文档有一种关联文档模板;文档模板指定了(例如,菜单,图标,或者加速表格)使用这些类型的文档。每个文档包含一个指针指向关联的模板对象。

Users interact with a document through the CView object(s) associated with it. A view renders an image of the document in a frame window and interprets user input as operations on the document. A document can have multiple views associated with it. When the user opens a window on a document, the framework creates a view and attaches it to the document. The document template specifies what type of view and frame window are used to display each type of document.

翻译:用户将一个文档和一个视类对象想关联起来。一个视类扮演一个嵌入在文档框架内的图像,在文档内解释用户的输入操作。一个文档可以有多个视类与其关联。当用户在一个文档上打开一个窗体,框架创造一个视图,把它放在文档上。文档模板指定适合于用户文档类型的相同类的视图和框架窗口用来显示用户打开的文件。

Documents are part of the framework's standard command routing and consequently receive commands from standard user-interface components (such as the File Save menu item). A document receives commands forwarded by the active view. If the document doesn't handle a given command, it forwards the command to the document template that manages it.

翻译:文档是一个框架标准命令路由,因此从标准用户界面组件(如文件保存菜单项)接受命令。一个文档由其活动视图迅速接受命令。如果该文档没有没有响应给出的命令,那么该命令将交由文档模板来管理。

When a document's data is modified, each of its views must reflect those modifications. CDocument provides the UpdateAllViews member function for you to notify the views of such changes, so the views can repaint themselves as necessary. The framework also prompts the user to save a modified file before closing it.

翻译:当一个文档数据发生改变,每一个它的视图将随即发生改变。文档类提供更新所有视图成员函数来通报每一个视图的改变,因此视图可以在必要的时候被重绘。框架也在关闭前迅速保存改动文件。

To implement documents in a typical application, you must do the following:

l         Derive a class from CDocument for each type of document.

l         Add member variables to store each document's data.

l         Implement member functions for reading and modifying the document's data. The document's views are the most important users of these member functions.

l         Override the CObject::Serialize member function in your document class to write and read the document's data to and from disk.

翻译:在一个典型类型的应用程序中实现一个文档,你必须按以下步骤:

l         为每一种类型的文档从 CDocument 派生一个类

l         增加成员函数来存储每一个文档的数据

l         为读写文档数据实现成员函数。文档视图类是这个成员函数最重要的使用者。

l         在你的文档类重载 CObject::Serialize 成员函数,来将文档数据从磁盘读写。

序列化:

本文解释 Microsoft 基础类库 (MFC) 中提供的序列化机制,该机制使对象可以在程序运行之间保持。

序列化是指将对象写入永久性存储媒体(如磁盘文件)或从其中读取对象的进程。 MFC CObject 类中的序列化提供内置支持。因此,所有从 CObject 派生的类都可利用 CObject 的序列化协议。

序列化的基本思想是对象应能将其当前状态(通常由该对象的成员变量指示)写入永久性存储中。以后,通过从存储中读取对象状态或反序列化对象状态,可以重新创建该对象。序列化处理序列化对象时使用的对象指针和对象循环引用的所有详细资料。关键之处在于对象本身负责读写其自身状态。因此,对于可序列化的类,必须实现基本的序列化操作。正如 序列化 文章组中所显示的那样,很容易将该功能添加到类中。

MFC CArchive 类的对象用作将被序列化的对象和存储媒体之间的中介物。该对象始终与 CFile 对象相关联,它从 CFile 对象获得序列化所需的信息,包括文件名和请求的操作是读取还是写入。执行序列化操作的对象可在不考虑存储媒体本质的情况下使用 CArchive 对象。

CArchive 对象使用重载输出运算符 (<<) 和输入运算符 (>>) 来执行读写操作。有关更多信息,请参见 序列化:序列化对象 文章中的 通过存档存储和加载 CObjects

注意     请不要将 CArchive 类与通用 iostream 类混淆, iostream 类只用于格式化的文本。而 CArchive 类则用于二进制格式的序列化对象。

如果愿意,可以不使用 MFC 序列化而为永久性数据存储创建自己的机制。您将需要在用户的命令处重写启动序列化的类成员函数。请参见 ID_FILE_OPEN ID_FILE_SAVE ID_FILE_SAVE_AS 标准命令的 技术说明 22 中的讨论。

下列文章介绍了序列化所需的两个主要任务:

l         序列化:创建可序列化的类

l         序列化:序列化对象

序列化:序列化与数据库输入 /输出的对比 一文描述了序列化在数据库应用程序中何时是适当的输入 / 输出技术。

通过存档存储和加载 CObjects

通过存档存储及加载 CObject 需要额外注意。在某些情况下,应调用对象的 Serialize 函数,其中, CArchive 对象是 Serialize 调用的参数,与使用 CArchive “<<” “>>” 运算符不同。要牢记的重要事实是: CArchive “>>” 运算符基于由存档先前写到文件的 CRuntimeClass 信息构造内存中的 CObject

因此,是使用 CArchive “<<” “>>” 运算符还是调用 Serialize ,取决于是否需要加载存档基于先前存储的 CRuntimeClass 信息动态地重新构造对象。在下列情况下使用 Serialize 函数:

l         反序列化对象时,预先知道对象的确切的类。

l         反序列化对象时,已为其分配了内存。

警告     如果使用 Serialize 函数加载对象,也必须使用 Serialize 函数存储对象。不要使用 CArchive “<<” 运算符先存储,然后使用 Serialize 函数进行加载;或使用 Serialize 函数存储,然后使用 CArchive “>>” 运算符进行加载。

以下示例阐释了这些情况:

class CMyObject : public CObject

{

// ...Member functions

   public:

   CMyObject() { }

   virtual void Serialize( CArchive& ar ) { }

 

// Implementation

   protected:

   DECLARE_SERIAL( CMyObject )

};

 

 

class COtherObject : public CObject

{

   // ...Member functions

   public:

   COtherObject() { }

   virtual void Serialize( CArchive& ar ) { }

 

// Implementation

protected:

   DECLARE_SERIAL( COtherObject )

};

 

 

class CCompoundObject : public CObject

{

   // ...Member functions

   public:

   CCompoundObject();

   virtual void Serialize( CArchive& ar );

 

// Implementation

protected:

   CMyObject m_myob;    // Embedded object

   COtherObject* m_pOther;    // Object allocated in constructor

   CObject* m_pObDyn;    // Dynamically allocated object

   //..Other member data and implementation

 

   DECLARE_SERIAL( CCompoundObject )

};

 

IMPLEMENT_SERIAL(CMyObject,CObject,1)

IMPLEMENT_SERIAL(COtherObject,CObject,1)

IMPLEMENT_SERIAL(CCompoundObject,CObject,1)

 

 

CCompoundObject::CCompoundObject()

{

   m_pOther = new COtherObject; // Exact type known and object already

            //allocated.

   m_pObDyn = NULL;    // Will be allocated in another member function

            // if needed, could be a derived class object.

}

 

void CCompoundObject::Serialize( CArchive& ar )

{

   CObject::Serialize( ar );    // Always call base class Serialize.

   m_myob.Serialize( ar );    // Call Serialize on embedded member.

   m_pOther->Serialize( ar );    // Call Serialize on objects of known exact type.

 

   // Serialize dynamic members and other raw data

   if ( ar.IsStoring() )

   {

      ar << m_pObDyn;

      // Store other members

   }

   else

   {

      ar >> m_pObDyn; // Polymorphic reconstruction of persistent

            // object

            //load other members

   }

}

总之,如果可序列化的类将嵌入的 CObject 定义为成员,则不应使用该对象的 CArchive “<<” “>>” 运算符,而应调用 Serialize 函数。同时,如果可序列化的类将指向 CObject (或从 CObject 派生的对象)的指针定义为成员,但在自己的构造函数中将其构造为其他对象,则也应调用 Serialize

序列化:创建可序列化的类

ms-help://MS.VSCC.2003/MS.MSDNQTR.2003FEB.2052/vccore/html/_core_serialization.3a_.making_a_serializable_class.htm

使类可序列化需要五个主要步骤。下面列出了这些步骤并在以后章节内进行了解释:

  1. CObject 派生类 (或从 CObject 派生的某个类中派生)。
  2. 重写 Serialize 成员函数
  3. 使用 DECLARE_SERIAL (在类声明中)。
  4. 定义不带参数的构造函数
  5. 为类 在实现文件中使用 IMPLEMENT_SERIAL

如果直接调用 Serialize而不是通过 CArchive“>>”“<<”运算符调用,则序列化不需要最后三个步骤。

CObject 派生类

CObject 类中定义了基本的序列化协议和功能。正如在 CPerson 类的下列声明中所示,通过从 CObject 中(或从 CObject 的派生类中)派生类,可获得对 CObject 的序列化协议及功能的访问权限。

 

重写 Serialize 成员函数

 

CObject 类中定义的 Serialize 成员函数实际上负责对捕获对象的当前状态所必需的数据进行序列化。 Serialize 函数具有 CArchive 参数,该函数使用其来读写对象数据。 CArchive 对象具有成员函数 IsStoring ,该成员函数指示 Serialize 正在存储(即正在写入数据)还是正在加载(即正在读取数据)。用 IsStoring 的结果作为参考,使用输出运算符 (<<) 将对象数据插入到 CArchive 对象中或使用输入运算符 (>>) 提取数据。

假定一个类是从 CObject 派生的并具有两个新成员变量,分别为 CString WORD 类型。下列类声明段显示了新成员变量和重写的 Serialize 成员函数的声明:

class CPerson : public CObject

{

public:

    DECLARE_SERIAL( CPerson )

    // empty constructor is necessary

    CPerson(){};

 

    CString m_name;

    WORD   m_number;

 

    void Serialize( CArchive& archive );

   

    // rest of class declaration

};

 

重写 Serialize 成员函数

  1. 调用 Serialize 的基类版本以确保序列化对象的继承部分。
  2. 插入或提取您的类所特定的成员变量。

输出运算符及输入运算符与存档类交互作用以读写数据。下面的示例显示了如何实现以上声明的 CPerson类的 Serialize

				
void  CPerson::Serialize( CArchive &  archive )
{
    
//  call base class function first
    
//  base class is CObject in this case
    CObject::Serialize( archive );

    
//  now do the stuff for our specific class
     if ( archive.IsStoring() )
        archive 
<<  m_name  <<  m_number;
    
else
        archive 
>>  m_name  >>  m_number;
}

也可使用 CArchive::ReadCArchive::Write成员函数来读写大量未键入的数据。

使用 DECLARE_SERIAL

在支持序列化的类的声明中需要 DECLARE_SERIAL宏,如下所示:

class  CPerson :  public  CObject
{
    DECLARE_SERIAL( CPerson )
    
//  rest of declaration follows
}
;
定义不带参数的构造函数

反序列化对象(从磁盘上加载)后,MFC 重新创建这些对象时,需要一个默认的构造函数。反序列化进程将用重新创建对象所需的值填充所有成员变量。

可将该构造函数声明为公共的、受保护的或私有的。如果使该构造函数成为受保护的或私有的,请确保它将仅由序列化函数使用。该构造函数必须使对象处于这样一种状态:必要时,可允许将其安全删除。

注意     如果忘记在使用 DECLARE_SERIAL IMPLEMENT_SERIAL 宏的类中定义不带参数的构造函数,将在使用 IMPLEMENT_SERIAL 宏的行上得到 没有可用的默认构造函数 编译器警告。

在实现文件中使用 IMPLEMENT_SERIAL

IMPLEMENT_SERIAL 宏用于定义从 CObject中派生可序列化类时所需的各种函数。在类的实现文件 (.CPP) 中使用这个宏。该宏的前两个参数是类名和直接基类的名称。

该宏的第三个参数是架构编号。架构编号实质上是类对象的版本号。架构编号使用大于或等于零的整数。(请不要将该架构编号与数据库术语混淆。)

MFC 序列化代码在将对象读取到内存时检查该架构编号。如果磁盘上对象的架构编号与内存中类的架构编号不匹配,库将引发 CArchiveException,防止程序读取对象的不正确版本。

如果要使 Serialize成员函数能够读取多个版本(即,读取用应用程序的不同版本写入的文件),可将 VERSIONABLE_SCHEMA值作为 IMPLEMENT_SERIAL宏的参数。有关用法信息和示例,请参见 CArchive类的 GetObjectSchema成员函数。

以下示例显示了如何将 IMPLEMENT_SERIAL用于从 CObject派生的 CPerson类。

				
IMPLEMENT_SERIAL( CPerson, CObject, 1 )

正如序列化:序列化对象文章中所讨论的,一旦具有可序列化的类,就可以序列化类的对象。

序列化 :序列化对象

序列化:创建可序列化的类 一文说明如何使类可序列化。一旦具有可序列化的类,就可以通过 CArchive对象将该类的对象序列化到文件和从文件序列化该类的对象。本文将解释:

可使框架创建可序列化文档的存档或自己显式创建 CArchive对象。通过使用 CArchive“<<”“>>”运算符或在某些情况下通过调用 CObject派生类的 Serialize 函数,可在文件和可序列化对象间传输数据。

什么是Archive对象?

CArchive 对象提供了一个类型安全缓冲机制,用于将可序列化对象写入 CFile对象或从中读取可序列化对象。通常,CFile对象表示磁盘文件;但是,它也可以是表示剪贴板的内存文件(CSharedFile对象)。

给定的 CArchive对象要么存储数据(即写入数据或将数据序列化),要么加载数据(即读取数据或将数据反序列化),但决不能同时进行。CArchive对象的寿命只限于将对象写入文件或从文件读取对象的一次传递。因此,需要两个连续创建的 CArchive对象将数据序列化到文件,然后从文件反序列化数据。

当存档将对象存储到文件时,存档将 CRuntimeClass名称附加到这些对象。然后,当另一个存档将对象从文件加载到内存时,将基于这些对象的 CRuntimeClass动态地重新构造 CObject派生的对象。通过存储存档将给定对象写入文件时,该对象可能被引用多次。然而,加载存档将仅对该对象重新构造一次。有关存档如何将 CRuntimeClass信息附加到对象以及重新构造对象(考虑可能的多次引用)的详细信息,请参见技术说明 2

将数据序列化到存档时,存档积累数据,直到其缓冲区被填满为止。然后,存档将其缓冲区写入 CArchive对象指向的 CFile对象。同样,当您从存档中读取数据时,存档会将数据从文件读取到它的缓冲区,然后从缓冲区读取到反序列化的对象。这种缓冲减少了物理读取硬盘的次数,从而提高了应用程序的性能。

创建 CArchive 对象有两种方法:

l         通过框架隐式创建 CArchive 对象

l         显式创建 CArchive 对象

通过框架隐式创建 CArchive 对象

最普通且最容易的方法是使框架代表“文件”菜单上的“保存”、“另存为”和“打开”命令为文档创建 CArchive 对象。

以下是应用程序的用户从“文件”菜单上发出“另存为”命令时,框架所执行的操作:

显示“另存为”对话框并从用户获取文件名。

打开用户命名的文件作为 CFile 对象。

创建指向该 CFile 对象的 CArchive 对象。在创建 CArchive 对象时,框架将模式设置为“存储”(即写入或序列化),而不是“加载”(即读取或反序列化)。

调用在 CDocument 派生类中定义的 Serialize 函数,将 CArchive 对象的引用传递给该函数。

然后,文档的 Serialize 函数将数据写入 CArchive 对象(刚作了解释)。从 Serialize 函数返回时,框架先销毁 CArchive 对象,再销毁 CFile 对象。

因此,如果让框架为文档创建 CArchive 对象,您所要做的一切是实现写入存档和从存档中读取的文档的 Serialize 函数。您还必须为文档的 Serialize 函数直接或间接依次序列化的任何 CObject 派生对象实现 Serialize

显式创建 CArchive 对象

除了通过框架将文档序列化之外,在其他场合也可能需要 CArchive 对象。例如,可能要序列化到达或来自剪贴板的数据,由 CSharedFile 对象表示。或者,可能要使用用户界面来保存与框架提供的文件不同的文件。在这种情况下,可以显式创建 CArchive 对象。使用下列过程,用与框架采用的相同方式来执行此操作。

显式创建 CArchive 对象

构造 CFile 对象或从 CFile 导出的对象。

按照下面示例所示,将 CFile 对象传递到 CArchive 的构造函数:

CFile theFile;

theFile.Open(..., CFile::modeWrite);

CArchive archive(&theFile, CArchive::store);

CArchive 构造函数的第二个参数是指定存档将用于向文件中存储数据还是用于从文件中加载数据的枚举值。对象的 Serialize 函数通过调用存档对象的 IsStoring 函数来检查该状态。

当完成向 CArchive 对象存储数据或从该对象中加载数据时,关闭该对象。虽然 CArchive 对象(和 CFile 对象)会自动关闭存档(和文件),好的做法是显式执行,因为这使从错误恢复更为容易。有关错误处理的更多信息,请参见异常:捕捉和删除异常一文。

关闭 CArchive 对象

以下示例阐释了如何关闭 CArchive 对象:

archive.Close();

theFile.Close();

 

使用 CArchive 对象的“ << ”和“ >> ”操作符

CArchive 提供“ << ”和“ >> ”运算符,用于向文件中写入简单的数据类型和 CObjects 以及从文件中读取它们。

通过存档将对象存储在文件中

以下示例显示了如何通过存档将对象存储在文件中:

CArchive ar(&theFile, CArchive::store);

WORD wEmployeeID;

...

ar << wEmployeeID;

从先前存储在文件中的值加载对象

以下示例显示了如何从先前存储在文件中的值加载对象:

CArchive ar(&theFile, CArchive::load);

WORD wEmployeeID;

...

ar >> wEmployeeID;

通常,通过 CObject 派生类的 Serialize 函数中的存档将数据存储到文件中或从文件中加载数据,必须已用 DECLARE_SERIALIZE 宏来声明这些函数。将 CArchive 对象的引用传递到 Serialize 函数。调用 CArchive 对象的 IsLoading 函数以确定是否已调用 Serialize 函数来从文件中加载数据或将数据存储到文件中。

可序列化的 CObject 派生类的 Serialize 函数通常具有以下形式:

void CPerson::Serialize(CArchive& ar)

{

    CObject::Serialize(ar);

    if (ar.IsStoring())

    {

        // TODO:  add storing code here

    }

    else

    {

    // TODO:  add loading code here

    }

}

上面的代码模板与 AppWizard 为该文档(从 CDocument 派生的类)的 Serialize 函数所创建的代码模板完全相同。由于存储代码和加载代码总是并行,该代码模板有助于写的代码更容易复查,如下例中所示:

void CPerson:Serialize(CArchive& ar)

{

    if (ar.IsStoring())

    {

        ar << m_strName;

        ar << m_wAge;

    }

    else

    {

        ar >> m_strName;

        ar >> m_wAge;

    }

}

库将 CArchive 的“ << ”和“ >> ”运算符定义为第一操作数,将下列数据类型和类类型定义为第二操作数:

CObject*

SIZE CSize

float

WORD

CString

POINT CPoint

DWORD

BYTE

RECT CRect

double

LONG

CTime CTimeSpan

int

COleCurrency

COleVariant

COleDateTime

COleDateTimeSpan

 

 

注意    通过存档存储及加载 CObjects 需要额外注意。有关更多信息,请参见通过存档存储和加载 CObjects

CArchive 的“ << ”和“ >> ”运算符总是返回 CArchive 对象的引用,该引用为第一操作数。这使您可以链接运算符,如下所示:

BYTE bSomeByte;

WORD wSomeWord;

DWORD wSomeDoubleWord;

...

ar << bSomeByte << wSomeWord << wSomeDoubleWord;

 

 

通过存档存储及加载 CObject (见前)

下面用一个示例来解释这个问题。

目标:一个画图程序,通过保存打开按钮存取图片。方法:保存图片绘制信息。

按步骤:

l         创建可序列化的类 ->Graph.cpp+Graph.h

l         View 类中添加对控件的响应,实现画图功能,每次鼠标弹起的时候保存绘图信息

l         保存文件(通过 Doc Serialize 来保存数据)

l         打开文件(通过 Doc Serialize 来读取数据,并将其重绘)

View 类中定义 CObArray m_obArray;

下面按这个思路来完成:(在 C Graph 子类中完成重绘的画图功能)

Graph.cpp

#include "StdAfx.h"

#include ".\graph.h"

IMPLEMENT_SERIAL(CGraph, CObject, 1 )

CGraph::CGraph(void)

: m_ptOrigin(0)

, m_ptEnd(0)

, m_nDrawType(0)

{

}

CGraph::CGraph(CPoint m_ptOrigin,CPoint m_ptEnd,UINT m_nDrawType)

{

     this->m_ptOrigin=m_ptOrigin;

     this->m_ptEnd=m_ptEnd;

     this->m_nDrawType=m_nDrawType;

}

CGraph::~CGraph(void)

{

}

void CGraph::Serialize( CArchive& ar )

{

     // 继承基类的CObject

    CObject::Serialize( ar );

    if( ar.IsStoring() )

     {

        ar << m_ptOrigin << m_ptEnd << m_nDrawType ;

     }

     else

     {

         ar >> m_ptOrigin >> m_ptEnd >> m_nDrawType ;

     }

}

void CGraph::Draw(CDC* pDC)

{

     CBrush *pBrush=CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));

     CBrush *pOldBrush=pDC->SelectObject(pBrush);

     switch(m_nDrawType)

     {

     case 1:

         pDC->SetPixel(m_ptEnd,RGB(0,0,0));

         break;

     case 2:

         pDC->MoveTo(m_ptOrigin);

         pDC->LineTo(m_ptEnd);

         break;

     case 3:

         pDC->Rectangle(CRect(m_ptOrigin,m_ptEnd));

         break;

     case 4:

         pDC->Ellipse(CRect(m_ptOrigin,m_ptEnd));

         break;

     }

     pDC->SelectObject(pOldBrush);

}

Graph..h

#pragma once

#include "atltypes.h"

 

class CGraph : publicCObject

{

public :

     DECLARE_SERIAL( CGraph )

     CGraph(void);

     CGraph::CGraph(CPoint m_ptOrigin,CPoint m_ptEnd,UINT m_nDrawType);

     void Serialize( CArchive& ar );

     ~CGraph(void);

     CPoint m_ptOrigin;

     CPoint m_ptEnd;

     UINT m_nDrawType;

     void Draw(CDC* pDC);

};

View 类中添加画图功能

void CGraphicSerialView::OnDot()

{

     // TODO: 在此添加命令处理程序代码

     m_nDrawType=1;

}

 

void CGraphicSerialView::OnLine()

{

     // TODO: 在此添加命令处理程序代码

     m_nDrawType=2;

}

 

void CGraphicSerialView::OnRectangle()

{

     // TODO: 在此添加命令处理程序代码

     m_nDrawType=3;

}

 

void CGraphicSerialView::OnEllipse()

{

     // TODO: 在此添加命令处理程序代码

     m_nDrawType=4;

}

 

void CGraphicSerialView::OnLButtonDown(UINT nFlags, CPoint point)

{

     // TODO: 在此添加消息处理程序代码和/或调用默认值

     m_ptOrigin = point;

     CView::OnLButtonDown(nFlags, point);

}

 

void CGraphicSerialView::OnLButtonUp(UINT nFlags, CPoint point)

{

     // TODO: 在此添加消息处理程序代码和/或调用默认值

     CClientDC dc(this);

     CBrush *pBrush=CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));     // 选择一个透明画刷

     dc.SelectObject(pBrush);

 

     switch(m_nDrawType)

     {

     case 1:

         dc.SetPixel(point,RGB(0,0,0));

         break;

     case 2:

         dc.MoveTo(m_ptOrigin);

         dc.LineTo(point);

         break;

     case 3:

         dc.Rectangle(m_ptOrigin.x,m_ptOrigin.y,point.x,point.y);

         break;

     case 4:

         dc.Ellipse(CRect(m_ptOrigin,point));

         break;

     default:

         break;

     }

     // 将图形信息保存起来

     CGraph *pGraph=new CGraph( m_ptOrigin , point , m_nDrawType );   // 创建一个pGraph指针指向一个存储图形信息的“图形”

     m_obArray.Add(pGraph);// 将一群这样的pGraph组织在一起放进m_obArray

     CView::OnLButtonUp(nFlags, point);

}

但是要保存和读取文件需要用到 Serialize

因此转到 Doc

void CGraphicSerialDoc::Serialize(CArchive& ar)

{

     POSITION pos=GetFirstViewPosition();// 获得第一个视类对象在对象列表中的位置

     CGraphicSerialView *pView=(CGraphicSerialView*)GetNextView(pos);// 找到当前视类对象的指针,由于这是单文档,因此只有一个视类对象

     if (ar.IsStoring())

     {

         // TODO: 在此添加存储代码

         int nCount=pView->m_obArray.GetSize();

         ar<<nCount;   // 为了在读取文件的时候能够知道元素个数,因此在保存的时候把个数也存进去

         for(int i=0;i<nCount;i++)

         {

              ar<<pView->m_obArray.GetAt(i);

         }

     }

     else

     {

         // TODO: 在此添加加载代码

         int nCount;

         ar>>nCount;

         CGraph *pGraph;

         for(int i=0;i<nCount;i++)

         {

              ar>>pGraph;

              pView->m_obArray.Add(pGraph);

         }

     }

}

但是读取数据的时候还需要重绘图像:转回View类,因为在View类加载的时候会自动调用OnDraw(),OnDraw()中添加如下语句。

     int nCount;

     nCount=m_obArray.GetSize();

     for(int i=0;i<nCount;i++)

     {

         ((CGraph*)m_obArray.GetAt(i))->Draw(pDC);

     }

基本上完成了通过serialize保存和打开文件。

serialize的好处:通过缓存来保存,当缓存区满了才进行一次读/写,因此提高了应用程序的效率

 

另外 Archive 对象是支持序列化的,因此在读写数据的时候即可以按以上方法在 Doc 类的 Serialize 来保存和打开数据。

因此修改 Doc 中的 Serialize

void CGraphicSerialDoc::Serialize(CArchive& ar)

{

     POSITION pos=GetFirstViewPosition();// 获得第一个视类对象在对象列表中的位置

     CGraphicSerialView *pView=(CGraphicSerialView*)GetNextView(pos);// 找到当前视类对象的指针,由于这是单文档,因此只有一个视类对象

     if (ar.IsStoring())

     {

         // TODO: 在此添加存储代码

     }

     else

     {

         // TODO: 在此添加加载代码

     }

     pView->m_obArray.Serialize(ar);  // 将这个数据传递给

}

其中CObArray类对象读取数据的方法:(以下是MFC源代码,无须用户添加)

void CObArray::Serialize(CArchive& ar)

{

     ASSERT_VALID(this);

 

     CObject::Serialize(ar);

 

     if (ar.IsStoring())

     {

         ar.WriteCount(m_nSize);

         for (INT_PTR i = 0; i < m_nSize; i++)

              ar << m_pData[i];

     }

     else

     {

         DWORD_PTR nOldSize = ar.ReadCount();

         SetSize(nOldSize);

         for (INT_PTR i = 0; i < m_nSize; i++)

              ar >> m_pData[i];

     }

}

 

下面直接在 Doc 类中使用 CObArray 对象(取消之前定义在 View 类中的该类对象)

Doc 类中定义 CObArray m_obArray;

存图形数据的代码修改为:

     CGraphicDoc *pDoc=GetDocument();// 通过视类提供的GetDocument()方法来获得Doc类指针

     pDoc->m_obArray.Add(pGraph);

修改其他位置的 m_obArray

void CGraphicSerialView::OnDraw(CDC* pDC)

{

     ……

     nCount=pDoc->m_obArray.GetSize();

     for(int i=0;i<nCount;i++)

     {

          ((CGraph*)pDoc->m_obArray.GetAt(i))->Draw(pDC);   //Doc类中定义m_obArray的情况

     }

}

void CGraphicSerialDoc::Serialize(CArchive& ar)

{

……

     m_obArray.Serialize(ar);     //Doc 类中定义m_obArray的情况

}


当我们新建和打开文档的时候,我们之前的文档对象并没有被销毁(系统利用OnNewDocument方法所新建的对象)之前在堆上建立了文档对象。此时应删除文档对象。

在文档类中重载函数void CGraphicSerialDoc::DeleteContents()来删除文档对象,以避免内存泄露。

void CGraphicSerialDoc::DeleteContents()

{

     int nCount;

     nCount=m_obArray.GetSize();

     /*for(int i=0;i<nCount;i++)

     {

         delete m_obArray.GetAt(i);

         //m_obArray.RemoveAt(i);

     }

     m_obArray.RemoveAll();*/

     while(nCount--)

     {

         delete m_obArray.GetAt(nCount);

         m_obArray.RemoveAt(nCount);

     }

     CDocument::DeleteContents();

}

下面是关于内存泄露的在此的一段论述。(此文同时发布于)

http://www.cppblog.com/mymsdn/archive/2006/08/16/11266.html

关于内存泄露的问题(解决+剖析)

首先先阐明这篇随笔的意图,只在告诉读者,内存泄露的神不知鬼不觉,希望能引起大家的注意。
一段代码的意思如何正确表达,才能不造成内存泄露呢?很多朋友经常泄露了内存但却查找不到原因。当然在CLI/C++中利用托管对象堆上的垃圾收集器是可以更好地避免这一点。但是在更早的版本中,程序员有必要去手动删除这些相关资源。否则将在程序关闭的时候出现一些错误。
MFC
现在我们去重载一个虚函数virtualvoidDeleteContents();用来在销毁文档数据前调用框架删除一些文档类的数据,(MSDNCalled by the framework to delete the document's data without destroying the CDocument object itself.
先批评一段代码:

1 void  CGraphicDoc::DeleteContents() 
2 {
3      for ( int  i = 0 ;i < m_obArray.GetSize();i ++ )
4      {
5         ……
6     }
7     CDocument::DeleteContents();
8 }

评价:这段代码看似简练,但是却很浪费资源,在第3行的for循环中,i<m_obArray.GetSize();当每次进行判断的时候将再次调用GetSize(),如果这个数据的量是个天文数字,那么这样的调用无疑是一种灾难。

优化:

 1 void CGraphicDoc::DeleteContents() 
 2 {
 3     int nCount;
 4     nCount=m_obArray.GetSize();
 5     for(int i=0;i<nCount;i++)
 6      {
 7         ……
 8     }
 9     CDocument::DeleteContents();
10 }


填写for循环内的语句。这里的任务:删除之前利用CObArray : m_obArray对象保存的一个指针所指向的对象,以及指针本身。因此(以下提供几种常见的错误代码)
代码A

 1 void CGraphicDoc::DeleteContents() 
 2 {
 3     int nCount;
 4     nCount=m_obArray.GetSize();
 5     for(int i=0;i<nCount;i++)
 6      {
 7         delete m_obArray.GetAt(i);   //删除对象指针所指向的对象
 8         m_obArray.RemoveAt(i);   //删除对应的指针本身
 9     }
10     CDocument::DeleteContents();
11 }

代码B

 1 void CGraphicDoc::DeleteContents() 
 2 {
 3     int nCount;
 4     nCount=m_obArray.GetSize();
 5     for(int i=0;i<nCount;i++)
 6      {
 7         delete m_obArray.GetAt(i);
 8         m_obArray.RemoveAt(0);
 9     }
10     CDocument::DeleteContents();
11 }

代码A看起来似乎很符合常规思维,因此也很容易迷惑人。但是在程序运行的时候出现了错误。但是在MSDN中查找CObArray Class Members,查看RemoveAt,其中remarks中有这样一句:In the process, it shifts down all the elements above the removed element(s). It decrements the upper bound of the array but does not free memory. 它的意思就是假设你一共有3个数据 p[0]=ap[1]=bp[2]=c,当你删除第0个数据之后,数据将整体向前移动,变为p[0]=bp[1]=c。这样当你i=nCount-1的时候就根本没有这样的数让你删除,因为假设有那个时刻的话,数据的元素只有1个,而他的编号是0而不是nCount.因此出现了无法删除的现象。因此也就隐含了问题了。
因此有人突发奇想:如果我每次只删除第0个数据的话,那么是否就可以了呢?于是代码B诞生了。可是问题终究没能得到解决。因为假设有一组数据一共3个。删除了编号0的元素(delete语句),移除了该元素的指针,此时i=1,进入删除,又到了delete语句,这时候删除元素i=1这样的语句,这时实际上是删除了先前元素中的第二个元素,而不是第一个。而02中间的第1个元素则未被删除。又出现了隐含问题。其实只要将两个都改为0,每次都删除第一个就可以了。

 1 void CGraphicDoc::DeleteContents() 
 2 {
 3     int nCount;
 4     nCount=m_obArray.GetSize();
 5     for(int i=0;i<nCount;i++)
 6      {
 7         delete m_obArray.GetAt(0);
 8         m_obArray.RemoveAt(0);
 9     }
10     CDocument::DeleteContents();
11 }

另外可以将程序倒写过来,避免RemoveAt对其进行重新整合队列做产生的不可预料的麻烦。

 1 void CGraphicDoc::DeleteContents() 
 2 {
 3     int nCount;
 4     nCount=m_obArray.GetSize();
 5     while(nCount--)
 6      {
 7         delete m_obArray.GetAt(nCount);
 8         m_obArray.RemoveAt(nCount);
 9     }
10     CDocument::DeleteContents();
11 }
12

或者仔细察看MSDN中还有一个函数叫RemoveAll()它是用来删除整个CObArray集合对象的。

 1 void CGraphicDoc::DeleteContents() 
 2 {
 3     int nCount;
 4     nCount=m_obArray.GetSize();
 5     for(int i=0;i<nCount;i++)
 6      {
 7         delete m_obArray.GetAt(i);
 8     }
 9     m_obArray.RemoveAll();
10     CDocument::DeleteContents();
11 }

 

posted on 2008-07-18 20:05  volnet(可以叫我大V)  阅读(2135)  评论(0编辑  收藏  举报

使用Live Messenger联系我
关闭