注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

jasonyang9的博客

随便写写

 
 
 

日志

 
 

(怀旧系列)VC程序设计(孙鑫老师)听课笔记:13 文档和串行化  

2013-02-16 15:20:29|  分类: programming |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
(怀旧系列)VC程序设计(孙鑫老师)听课笔记:13 文档和串行化 - jasonyang9 - jasonyang9的博客

 ============
文档和串行化
============

新建一个单文档工程,增加一个“文件”菜单,和两个菜单项(ID_FILE_WRITE,写入文件;ID_FILE_READ,读取文件),在View类中增加命令响应函数。
这里要用到CArchive,CArchive没有基类,可以保存对象到永久的存储介质上,或读取对象数据,持久性的过程叫做串行化。CArchive对象类似二进输入输出制流,带有缓冲机制,非冗余格式存储。必须先创建一个CFile对象,并保证其打开和保存的状态和文件相一致,一个文件只能有一个Archive。当构建CArchive对象时,和一个打开的文件关联。CArchive对象可以处理基本类型数据也可以处理CObject派生类的对象数据。重载的操作符(<<和>>)方便了提取和插入操作。

 

void CXXXView::OnFileWrite()
 {
  CFile file("1.txt", CFile::modeCreate | CFile::modeWrite);      // 构造CFile对象,指定文件名和模式
  CArchive ar(&file, CArchive::store);         // 构造CArchive对象,将CFile对象作为指针传递给它
  int i = 4;            // 定义一些变量
  char ch = 'a';
  float f = 1.3f;            // 浮点型在赋值时需要加上f,否则为double型
  CString str("http://www.sunxin.org");
  ar << i << ch << f << str;          // 用插入操作符将变量保存起来(串行化)
 }

 void CXXXView::OnFileRead()
 {
  CFile file("1.txt", CFile::modeRead);         // 以读取方式打开文件
  CArchive ar(&file, CArchive::load);         // 以读取方式构造CArchive对象
  int i;             // 定义一些变量
  char ch;
  float f;
  CString str;
  CString strResult;
  ar >> i >> ch >> f >> str;          // 用提取操作符将变量读取出来(读取时的顺序和保存时一致)
  strResult.format("%d, %c, %f, %s", i, ch, f, str);       // 格式化结果到strResult
  MessageBox(strResult);           // 显示
 }


CXXXDoc中有一个OnNewDocument函数,当程序启动时会调用此函数(可以在其中调用SetTitle来设置文档的标题)。它是一个虚函数,被框架调用,作为File|New命令的一部分。

 

BOOL CXXXDoc::OnNewDocument()
 {
  if (!CDocument::OnNewDocument())
   return FALSE;

  SetTitle("http://www.sunxin.org");
  return TRUE;
 }


资源视图中的String Table下有一个IDR_MAINFRAME,内容是用\n分隔的7个子串:“Graphic\n\nGraphic\n\n\nGraphic.Document\nGraphi Document”,在第2和第3个\n中间没有任何内容,所以新建文档是“无标题”。
这个IDR_MAINFRAME是在CXXXApp的InitInstance中被引用的。

 

BOOL CXXXApp::InitInstance()
 {
  ...
  CSingleDocTemplate* pDocTemplate;         // 定义单文档模板的指针
  pDocTemplate = new CSingleDocTemplate(         // 构造单文档模板的对象(将文档、视类、框架关联在一起)
   IDR_MAINFRAME,           // 这里引用了IDR_MAINFRAME(IDR_MAINFRAME不仅代表了字符串资源,还包括菜单和图标资源)
   RUNTIME_CLASS(CXXXDoc),
   RUNTIME_CLASS(CMainFrame),
   RUNTIME_CLASS(CXXXView));
  AddDocTemplate(pDocTemplate);
  ...
 }


可以通过CDocTemplate::GetDocString来获取这7个子串的内容(windowTitle、docName、fileNewName、filterName、filterExt、regFileTypeId、regFileTypeName)。
String Table中IDR_MAINFRAME字符串资源中各子串的含义:
* (1) CDocTemplate::windowTitle,主窗口标题栏上的字符串,MDI程序不需要指定,将以IDR_MAINFRAME字符串为默认值。
* (2) CDocTemplate::docName,缺省文档的名称。如果没有指定,缺省文档的名称是“无标题”。
* (3) CDocTemplate::fileNewName,文档类型的名称。如果应用程序支持多种类型的文档,此字符串显示在“File|New”对话框中。如果没有指定,就不能在“File|New”对话框中处理这种文件。
* (4) CDocTemplate::filterName,文档类型的描述和一个适用于此类型的通配符过滤器。这个字符串将出现在“File|Open”对话框中的文件类型列表框中。要和CDocTemplate::filterExt一起使用。
* (5) CDocTemplate::filterExt,文档的扩展名。如果没有指定,就不能在“File|Open”对话框中处理这种文档。要和CDocTemplate::filterName一起使用。
* (6) CDocTemplate::regFileTypeId,如果以::RegisterShellFileTypes向系统的注册表注册文件类型,这个值会出现在HKEY_CLASSES_ROOT之下,成为其子项,并仅供Windows内部使用。如果没有指定,这种文件类型就无法注册。
* (7) CDocTemplate::regFileTypeName,也是存储在注册表中的文件类型名称。会显示于程序中用以访问注册表的对话框内。(?)

ID_FILE_NEW的命令响应函数CWinApp::OnFileNew调用OnNewDocument(一个虚函数,如果在派生类中有则调用派生类的实现)。
深挖MFC源代码,可以发现其调用的过程。

 

void CWinApp::OnFileNew()
 {
  if (m_pDocManager != NULL)          // 判断CDocManager指针变量是否为NULL(CDocManager维护文档模板指针,在CXXXApp::InitInstance中调用AddDocTemplate(pDocTemplate);时就加入了指针)
   m_pDocManager->OnFileNew();         // 调用CDocManager指针所指OnFileNew
 }

 void CDocManager::OnFileNew()
 {
  if (m_templateList.IsEmpty())          // 判断CPtrList类(指针链表)对象m_templateList(文档模板指针链表)是否为空
  {
   ...
  }

  CDocTemplate* pTemplate = (CDocTemplate*)m_templateList.GetHead();     // 定义临时变量,并赋值为文档模板指针链表的头
  if (m_templateList.GetCount() > 1)         // 判断文档模板指针链表的元素总数是否大于1(单文档应用程序的文档模板指针链表中只有一个模板)
  {
   CNewTypeDlg dlg(&m_templateList);
   int nID = dlg.DoModal();
   if (nID == IDOK)
    pTemplate = dlg.m_pSelectedTemplate;
   else
    return;
  }

  ASSERT(pTemplate != NULL);
  ASSERT_KINDOF(CDocTemplate, pTemplate);

  pTemplate->OpenDocumentFile(NULL);         // 调用文档模板的OpenDocumentFile函数
 }


由于CSingleDocTemplate派生于CDocTemplate,而且其OpenDocumentFile为一个纯虚函数,那么在pTemplate->OpenDocumentFile时(单文档应用程序),就会调用CSingleDocTemplate::OpenDocumentFile这个函数的实现了。

 

CDocument* CSingleDocTemplate::OpenDocumentFile(LPCTSTR lpszPathName,
  BOOL bMakeVisible)
 {
  CDocument* pDocument = NULL;          // 定义文档类指针
  CFrameWnd* pFrame = NULL;          // 定义框架类指针
  ...

  if (m_pOnlyDoc != NULL)
  {
   ...
  }
  else
  {
   pDocument = CreateNewDocument();        // 用CreateNewDocument来创建一个文档类对象
   ASSERT(pFrame == NULL);
   bCreated = TRUE;
  }

  ...

  if (pFrame == NULL)
  {
   ...
   pFrame = CreateNewFrame(pDocument, NULL);       // 用CreateNewFrame来创建一个框架类对象和一个视类对象(即每个文档模板都会有一个相应的文档类对象、框架类对象和视类对象,三位一体对文档服务)
   ...
  }

  ...

  if (lpszPathName == NULL)
  {
   ...
   if (!pDocument->OnNewDocument())        // 调用文档对象的OnNewDocument(一个虚函数,如果在派生类中有则调用派生类的实现,指向的是具体应用程序CXXXDoc类的指针的OnNewDocument)
   {
    TRACE0("CDocument::OnNewDocument returned FALSE.\n");
    if (bCreated)
     pFrame->DestoryWindow();
    return NULL;
   }
   ...
  }

  ...
 }


ID_FILE_NEW命令响应跟踪运行的过程为:CWinApp::OnFileNew→CDocManager::OnFileNew(pTemplate->OpenDocumentFile(NULL);)→CSingleDocTemplate::OpenDocumentFile(pDocument = CreateNewDocument();pFrame = CreateNewFrame(pDocument, NULL);pDocument->OnNewDocument();)→CXXXDoc::OnNewDocument()。
注意:单文档程序会在每次ID_FILE_NEW处理过程中重复利用文档对象、视类对象和框架对象,而多文档程序会新建这3个对象。

CXXXDoc中有一个Serialize函数,是文档类提供的用来保存和加载数据的函数,在File|Save和File|Open时被调用。
注意:在保存文件后,文档类对象和数据关联在一起(文档对象指针所代表的数据),如果打开同一份文件,MFC框架判断和数据(文档对象指针所代表)相关联的文档类指针已存在,就不会调用Serialize函数。

 

void CXXXDoc::Serialize(CArchive& ar)
 {
  if (ar.IsStoring())           // 判断是保存还是打开
  {
   int i = 5;
   char ch = 'b';
   float f = 1.2f;
   CString str("http://www.sunxin.org");
   ar << i << ch << f << str;
  }
  else
  {
   int i;
   char ch;
   float f;
   CString str;
   CString strResult;
   ar >> i >> ch >> f >> str;
   strResult.format("%d, %c, %f, %s", i, ch, f, str);
   AfxMessageBox(strResult);
  }
 }


深挖MFC源代码,查找OnFileOpen的调用过程。

 

void CWinApp::OnFileOpen()
 {
  ASSERT(m_pDocManager != NULL);
  m_pDocManager->OnFileOpen();
 }

 void CDocManager::OnFileOpen()
 {
  CString newName;
  if (!DoPromptFileName(newName, AFX_IDS_OPENFILE,       // 提示文件名的打开对话框
   OFN_HIDEREADONLY | OFN_FILEMUSTEXIST, TRUE, NULL))
   return;

  AfxGetApp()->OpenDocumentFile(newName);         // 调用CWinApp的OpenDocumentFile
 }

 BOOL CDocManager::OnPromptFileName(CString& fileName, UINT nIDSTitle, DWORD lFlags, ...)
 {
  CFileDialog dlgFile(bOpenfileDialog);         // 构造打开文件对话框
  ...
 }

 CDocument* CWinApp::OpenDocumentFile(LPCTSTR lpszFileName)
 {
  ASSERT(m_pDocManager != NULL);
  return m_pDocManager->OpenDocumentFile(lpszFileName);       // 调用文档管理器的OpenDocumentFile
 }

 CDocument* CDocManager::OpenDocumentFile(LPCTSTR lpszFileName)
 {
  ...
  CDocument* pOpenDocument = NULL;         // 定义文档类指针
  ...
  while (pos != NULL)
  {
   ...
   match = pTemplate->MatchDocType(szPath, pOpenDocument);      // 判断和数据(文档对象指针所代表)相关联的文档类指针是否已存在
   ...
  }
  ...
  if (pOpenDocument != NULL)          // 如果已有匹配,则pOpenDocument不为空
  {
   ...
   return pOpenDocument;          // 如果pOpenDocument不为空,直接返回pOpenDocument而不是调用文档模板的OpenDocumentFile,也就不会调用Serialize
  }
  ...
  return pBestTemplate->OpenDocumentFile(szPath);        // 调用文档模板的OpenDocumentFile(对于单文档程序就是CSingleDocTemplate的OpenDocumentFile)
 }

 CDocument* CSingleDocTemplate::OpenDocumentFile(LPCTSTR lpszPathName, BOOL bMakeVisible)
 {
  ...
  if (m_pOnlyDoc != NULL)
  {
   ...
  }
  else
  {
   ...
  }

  if (pDocument == NULL)
  {
   ...
  }
  ...
  if (pFrame == NULL)
  {
   ...
  }
  if (lpszPathName == NULL)
  {
   ...
  }
  else
  {
   ...
   if (!pDocument->OnOpenDocument(lpszPathName))       
   {
    ...
   }
   ...
  }
  ...
 }

 BOOL CDocument::OnOpenDocument(LPCTSTR lpszPathName)
 {
  ...
  CFile* pFile = GetFile(lpszPathName, CFile::modeRead | CFile::shareDenyWrite, &fe);   // 得到文件指针
  ...
  CArchive loadArchive(pFile, CArchive::load | CArchive::bNoFlushOnDelete);    // 构建CArchive对象
  ...
  TRY
  {
   CWaitCursor wait;
   if (pFile->GetLength() != 0)
    Serialize(loadArchive);         // 串行化载入(CDocument的Serialize是一个虚函数,会调用派生类的实现)
   loadArchive.Close();
   ReleaseFile(pFile, FALSE);
  }
  ...
 }


ID_FILE_OPEN命令响应跟踪运行的过程为:CWinApp::OnFileOpen→CDocManager::OnFileOpen(调用OnPromptFileName弹出文件打开对话框)→CWinApp::OpenDocumentFile→CDocManager::OpenDocumentFile→CSingleDocTemplate::OpenDocumentFile→CDocument::OnOpenDocument→CXXXDoc::Serialize。
以上是第一次打开该文件,如果是保存后再次打开同一个文件的过程为:CWinApp::OnFileOpen→CDocManager::OnFileOpen(调用OnPromptFileName弹出文件打开对话框)→CWinApp::OpenDocumentFile→CDocManager::OpenDocumentFile直接返回pOpenDocument。

单文档程序在新建或打开文件时,文档对象并不被销毁,而只是清空数据。
CWinApp有一个pDocManager指向文档管理器,文档管理器负责管理文档模板,文档模板将文档类、框架类和视类三位一体,为文档服务。

要让CArchive对象保存一个类对象数据,该类必须支持串行化,即该类需要有一个Serialize成员函数,并使用DECLARE_SERIAL和IMPLEMENT_SERIAL宏。
让一个类支持串行化的步骤是:
1. Deriving your class from CObject(让该类从CObject派生);
2. Overriding the Serialize member function(重载Serialize成员函数);
3. Using the DECLARE_SERIAL marco in the class declaration(在类声明中使用DECLARE_SERIAL宏);
4. Defining a constructor that takes no arguments(定义一个不带参数的构造函数);
5. Using the IMPLEMENT_SERIAL marco in the implementation file for your class(在类的实现代码中使用IMPLEMENT_SERIAL宏)。

将第11课的CGraph.cpp和CGraph.h文件加入工程(注释掉CGraph.cpp中的#include "Graphic.h"这个是第11课工程的头文件)。修改CGraph.h,让它从CObject派生。

 

// CGraph.h

 class CGraph : public CObject           // 让该类从CObject派生
 {
  DECLARE_SERIAL(CGraph)           // 在类声明中使用DECLARE_SERIAL宏
  ...
 public:
  CGraph();            // 定义一个不带参数的构造函数(如果已有就不需要重复定义)
  void Serialize(CArchive& ar);          // 重载Serialize成员函数
 };

 // CGraph.cpp

 IMPLEMENT_SERIAL(CGraph, CObject, 1)          // 在类的实现代码中使用IMPLEMENT_SERIAL宏(注意:最后一个参数是串行化的版本号)

 void CGraph::Serialize(CArchive& ar)          // Serialize成员函数的具体实现
 {
  if (ar.IsStoring())           // 判断是否存储
  {
   ar << m_nDrawType << m_ptOrigin << m_ptEnd;       // 存储(将3个成员变量串行化)
  }
  else
  {
   ar >> m_nDrawType >> m_ptOrigin >> m_ptEnd;       // 加载
  }
 }


以上就是CGraph串行化的步骤。
对CGraph增加一个图形绘制的函数,让其拥有数据加载、保存和图形绘制的功能,更加符合面向对象的思想。

 

void CGraph::Draw(CDC *pDC)
 {
  CBrush *pBrush = CBrush::FromeHandle((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;
  default:
   break;
  }

  pDC->SelectObject(pOldBrush);          // 恢复原画刷
 }


将第11课工程文件中的菜单资源复制到此工程中(方法是用资源管理器打开其工程资源Graphic.rc,打开Menu,在绘图菜单上右击选择复制,切换到资源视图,在菜单资源中选择粘贴。注意其中一些ID号可能会出现问题,需要手动修正)。
为这些导入的菜单项增加命令响应(在View类),增加成员变量UINT m_nDrawType,CPoint m_ptOrigin,在构造函数中对它们初始化为0。

 

 void CXXXView::OnDot()
 {
  m_nDrawType = 1;
 }

 void CXXXView::OnLine()
 {
  m_nDrawType = 2;
 }

 void CXXXView::OnRectangle()
 {
  m_nDrawType = 3;
 }

 void CXXXView::OnEllipse()
 {
  m_nDrawType = 4;
 }


增加ON_LBUTTONDOWN和ON_LBUTTONUP的消息处理。

 

void CXXXView::OnLButtonDown(UINT nFlags, CPoint point)
 {
  m_ptOrigin = point;
  CView::OnLButtonDown(nFlags, point);
 }

 void CXXXView::OnLButtonUp(UINT nFlags, CPoint point)
 {
  CClientDC dc(this);
  CBrush *pBrush = CBrush::FromeHandle((HBRUSH)GetStockObject(NULL_BRUSH));
  dc.SelectObject(pBrush);

  switch(m_nDrawType)
  {
  case 1:
   dc.SetPixel(m_ptEnd, RGB(0, 0, 0));
   break;
  case 2:
   dc.MoveTo(m_ptOrigin);
   dc.LineTo(point);
   break;
  case 3:
   dc.Rectangle(CRect(m_ptOrigin, point));
   break;
  case 4:
   dc.Ellipse(CRect(m_ptOrigin, point));
   break;
  default:
   break;
  }


  CGraph *pGraph = new CGraph(m_pDrawType, m_ptOrigin, point);      // 在堆上构造CGraph对象,将地址赋给pGraph指针(注意:需要包含CGraph头文件)
  m_obArray.Add(pGraph);           // 增加成员变量CObArray m_obArray集合类对象,然后将pGraph加入其中

  CView::OnLButtonUp(nFlags, point);
 }


修改Serialize函数,将集合类中的元素写入到文件保存和从文件读取。

 

void CXXXDoc::Serialize(CArchive& ar)
 {
  POSITION pos = GetFirstViewPosition();         // 获取第一个视类对象的(在集合类中的)位置
  CXXXView *pView = (CGraphView*)GetNextView(pos);       // 根据位置返回视类对象指针(注意需要强制类型转换,并包含CXXXView头文件)

  if (ar.IsStoring())
  {
   int nCount = pView->m_obArray.GetSize();
   ar << nCount;           // 将集合类中的CGraph对象的数目先保存在文件中,这样在读取时就能由此得知需要读取多少个CGraph对象了
   for (int i = 0; i < nCount; i++)        // 最好不要将pView->m_obArray.GetSize()放到for循环判断条件中,因为这样每次循环时都会调用GetSize函数,效率较低
   {
    ar << pView->m_obArray.GetAt(i);       // 由于CGraph类已支持串行化,可以直接保存
   }
  }
  else
  {
   int nCount;
   ar >> nCount;           // 取出所保存的CGraph对象的数目
   CGraph *pGraph;           // 定义CGraph指针(需要包含CGraph头文件)
   for (int i = 0; i < nCount; i++)        // 循环
   {
    ar >> pGraph;          // 取出一个CGraph对象(这里没有先构造CGraph对象,因为在读取时会自动调用带参数的构造方法,构造出一个对象,再将该对象的地址赋给指针变量)
    pView->m_obArray.Add(pGraph);        // 加入到集合类中(注意:这里m_obArray的清空工作可能是由View类的构造函数完成的,每次ID_FILE_OPEN时应该会重新构造View类?)
   }
  }
 }


修改CXXXView::OnDraw以支持重绘。

 

 void CXXXView::OnDraw(CDC* pDC)
 {
  CXXXDoc* pDoc = GetDocument();
  ASSERT_VALID(pDoc);
  int nCount;
  nCount = m_obArray.GetSize();
  for (int i = 0; i < nCount; i++)
  {
   ((CGraph*)m_obArray.GetAt(i))->Draw(pDC);
  }
 }


疑问:在响应ID_FILE_OPEN时是哪个函数调用了Invalidate,将OnDraw调用?是由于打开文件对话框的遮挡引起的重绘?

在文档类中调用Serialize串行化对象(如CGraph)时,实际上是调用了对象本身的串行化函数来完成的(具体保存对象中的哪些数据)。

由于CObArray本身支持串行化,那么可将CXXXDoc::Serialize简写。

 

void CXXXDoc::Serialize(CArchive& ar)
 {
  POSITION pos = GetFirstViewPosition();
  CXXXView *pView = (CGraphView*)GetNextView(pos);

  pView->m_obArray.Serialize(ar);
 }


深挖MFC源代码,查看其具体的实现方法。

 

void CObArray::Serialize(CArchive& ar)
 {
  ASSERT_VALID(this);

  CObject::Serialize(ar);           // 首先调用基类的Serialize

  if (ar.IsStoring())           // 判断是不是存储
  {
   ar.WriteCount(m_nSize);          // 写入存储的元素数目
   for (int i = 0; i < m_nSize; i++)        // 循环
    ar << m_pData[i];         // 写入元素(具体如何写入由该元素的Serialize实现)
  }
  else
  {
   DWORD nOldSize = ar.ReadCount();        // 读出存储的元素数目
   SetSize(nOldSize);          // 设置集合类的元素数目
   for (int i = 0; i < m_nSize; i++)        // 循环
    ar >> m_pData[i];         // 读出元素(具体如何读出由该元素的Serialize实现)
  }
 }


可将CObArray m_obArray转移到CXXXDoc中,这样还需要修改CXXXView的OnLButtonUp和OnDraw。

 

void CXXXView::OnLButtonUp(UINT nFlags, CPoint point)
 {
  ...
  CXXXDoc *pDoc->GetDocument();          // 用View类的GetDocument即可获得文档类对象的指针
  pDoc->m_obArray.Add(pGraph);          // 增加元素
  ...
 }

 void CXXXView::OnDraw(CDC* pDC)
 {
  CXXXDoc* pDoc = GetDocument();
  ASSERT_VALID(pDoc);
  int nCount;
  nCount = pDoc->m_obArray.GetSize();
  for (int i = 0; i < nCount; i++)
  {
   ((CGraph*)pDoc->m_obArray.GetAt(i))->Draw(pDC);
  }
 }


修改CXXXDoc的Serialize。

 

void CXXXDoc::Serialize(CArchive& ar)
 {
  m_obArray.Serialize(ar);          // 由于m_obArray已是文档类自己的成员变量,可进一步简化代码
 }


这样就将数据存放在了文档类,而在视类中访问数据,用来绘制和显示等。

关于Document/View结构
* 在MFC中,文档类负责管理数据,提供保存和加载数据的功能。视类负责数据的显示,以及给用户提供对数据的编辑和修改功能。
* MFC提供Document/View结构,将一个应用程序所需要的“数据处理和显示”的函数空壳都设计好了,这些函数都是虚函数,可以在派生类中重写这些函数。有关文件读写的操作在CDocument的Serialize中进行,有关数据和图形显示的操作在CView的OnDraw中进行。在其派生类中,只需要去关注Seriliaze和OnDraw函数即可,其他细节不需要理会,程序就可以良好的运行。
* 当按下“File|Open”,Application Framework会激活文件打开对话框,指定文件名,然后自动调用CXXXDoc::Serialize读取文件。Application Framework还会调用CXXXView::OnDraw,传递一个显示DC,重新绘制窗口的内容。
* MFC提供Document/View架构是希望程序员将精力放在数据结构的设计和数据显示的操作上,而不是对象和对象之间、模块和模块之间的通信上。
* 一个文档对象可以和多个视类对象相关联(通过GetFirstViewPosition和GetNextView才能返回视类对象的指针),而一个视类对象只能和一个文档对象相关联(GetDocument)。

此工程目前还有一个内存泄漏的问题,就是在File|New或File|Open时,在OnLButtonUp中CGraph *pGraph = new CGraph(m_pDrawType, m_ptOrigin, point);分配的堆上的内存并没有被释放。
由于CDocument::OnNewDocument和OnOpenDocument和关闭程序时都会自动调用DeleteContents来确保文档是空的,应该重写这个函数,释放堆上的内存。

 

void CXXXDoc::DeleteContents()
 {
  int nCount;
  nCount = m_obArray.GetSize();

  for (int i = 0; i < nCount; i++)
  {
   delete m_obArray.GetAt(i);         // 用delete删除指针所指的对象,释放堆内存
   m_obArray.RemoveAt(i);          // 删除集合类中所保存的元素(指针本身)
  }

  CDocument::DeleteContents();
 }


以上代码的执行会产生非法操作,原因是m_obArray.RemoveAt(i);会引起集合元素的重排,元素总数的上界会减小。
如果整个集合中有3个元素,当执行:
第1次循环时,集合中有3个元素,delete m_obArray.GetAt(0)正常,m_obArray.RemoveAt(0)正常;
第2次循环时,集合中有2个元素,delete m_obArray.GetAt(1)正常(但删除的是原来的第3个元素所指对象),m_obArray.RemoveAt(1)正常(但移走的是原来的第3个元素);
第3次循环时,集合中有1个元素,delete m_obArray.GetAt(2)产生非法操作(此时已没有第3个元素了),m_obArray.RemoveAt(2)也出错。

如果将代码改写为:

 

void CXXXDoc::DeleteContents()
 {
  for (int i = 0; i < m_obArray.GetSize(); i++)
  {
   delete m_obArray.GetAt(i);
   m_obArray.RemoveAt(i);
  }

  CDocument::DeleteContents();
 }


则会出现内存泄漏,少删除元素的错误。
如果整个集合中有3个元素,当执行:
第1次循环时,集合中有3个元素,变量i为0,m_obArray.GetSize()返回3,delete m_obArray.GetAt(0)正常,m_obArray.RemoveAt(0)正常;
第2次循环时,集合中有2个元素,变量i为1,m_obArray.GetSize()返回2,delete m_obArray.GetAt(1)正常(但删除的是原来的第3个元素所指对象),m_obArray.RemoveAt(1)正常(但移走的是原来的第3个元素);
第3次循环时,集合中有1个元素,变量i为2,m_obArray.GetSize()返回1,循环退出。

正确的处理方法之一是:

 

void CXXXDoc::DeleteContents()
 {
  int nCount;
  nCount = m_obArray.GetSize();

  for (int i = 0; i < nCount; i++)
  {
   delete m_obArray.GetAt(i);         // 用delete删除指针所指的对象,释放堆内存
  }

  m_obArray.RemoveAll();           // 一次性移除集合类中所保存的元素(指针本身)

  CDocument::DeleteContents();
 }


还有一种方法是:

 

void CXXXDoc::DeleteContents()
 {
  nCount = m_obArray.GetSize();

  while (nCount--)
  {
   delete m_obArray.GetAt(nCount);         // 从最大的元素开始删除
   m_obArray.RemoveAt(nCount);
  }

  CDocument::DeleteContents();
 }


补充:除了用String Table中的IDR_MAINFRAME来设置文件类型,还可以在AppWizard的第四步中点击Advanced按钮来设置文件的类型。

  评论这张
 
阅读(538)| 评论(0)
推荐 转载

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017