分类: C/C++
2010-12-05 21:20:04
正文:
“文档序列化”显然可以分成两个部分“写文件”和“读文件”。我在本文中也将从这两个方面来为你挖掘文档序列化的奥秘。
///////////////////////////////////////////////
/* 1.“写读文件”的共同基础 */
//////////////////////////////////////////////
无论是写还是读都等借助CRuntimeClass结构以及一些神秘的宏的帮助。在前几篇文章中我们没少和CRuntimeClass结构打交道,什么MFC执行期类型识别,什么动态创建技术等等。提到的这两种技术是文档序列化的基础,下面我们就看看为什么可以这么说:
除了与动态创建有关的成员外,在CRuntimeClass结构中还与序列化有关的重要成员有:
//in afx.h
struct CRuntimeClass
{
// Attributes
...//
LPCSTR m_lpszClassName;
int m_nObjectSize;
UINT m_wSchema; // schema number of the loaded class
...//
void Store(CArchive& ar) const;
static CRuntimeClass* PASCAL Load(CArchive& ar, UINT* pwSchemaNum);
const AFX_CLASSINIT* m_pClassInit;
...//
}
其中两个重要函数Store和Load的源代码如下:这两个函数主要写和读a runtime class description,其中包括m_lpszClassName和m_wSchema(版本号);
//in arccore.cpp
void CRuntimeClass::Store(CArchive& ar) const
// stores a runtime class description
{
WORD nLen = (WORD)lstrlenA(m_lpszClassName);
ar << (WORD)m_wSchema << nLen;
ar.Write(m_lpszClassName, nLen*sizeof(char));
}
// loads a runtime class description
CRuntimeClass* PASCAL CRuntimeClass::Load(CArchive& ar, UINT* pwSchemaNum)
{
WORD nLen;
char szClassName[64];
WORD wTemp;
ar >> wTemp; *pwSchemaNum = wTemp;
ar >> nLen;
// load the class name
if (nLen >= _countof(szClassName) ||
ar.Read(szClassName, nLen*sizeof(char)) != nLen*sizeof(char))
{
return NULL;
}
szClassName[nLen] = '\0';
// match the string against an actual CRuntimeClass
CRuntimeClass* pClass = FromName(szClassName);
if (pClass == NULL)
{
// not found, trace a warning for diagnostic purposes
TRACE(traceAppMsg, 0, "Warning: Cannot load %hs from archive. Class not defined.\n",szClassName);
}
return pClass;
}
下面看看那两个神秘的宏DECLARE_SERIAL和IMPLEMENT_SERIAL吧!
//in afx.h
#define DECLARE_SERIAL(class_name) \
_DECLARE_DYNCREATE(class_name) \
AFX_API friend CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb);
#define IMPLEMENT_SERIAL(class_name, base_class_name, wSchema) \
CObject* PASCAL class_name::CreateObject() \
{ return new class_name; } \
AFX_COMDAT AFX_CLASSINIT _init_##class_name(RUNTIME_CLASS(class_name)); \
_IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, wSchema, \
class_name::CreateObject, &_init_##class_name) \
CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb) \
{ pOb = (class_name*) ar.ReadObject(RUNTIME_CLASS(class_name)); \
return ar; } \
宏的定义验证了上面的“基础”一说;
下面是IMPLEMENT_SERIAL进行初始化时的辅助结构AFX_CLASSINIT及函数AfxClassInit的定义:
// generate static object constructor for class registration
void AFXAPI AfxClassInit(CRuntimeClass* pNewClass);
struct AFX_CLASSINIT
{ AFX_CLASSINIT(CRuntimeClass* pNewClass) { AfxClassInit(pNewClass); } };
//in objcore.cpp
void AFXAPI AfxClassInit(CRuntimeClass* pNewClass)
{
AFX_MODULE_STATE* pModuleState = AfxGetModuleState();
AfxLockGlobals(CRIT_RUNTIMECLASSLIST);
pModuleState->m_classList.AddHead(pNewClass);
AfxUnlockGlobals(CRIT_RUNTIMECLASSLIST);
}
有了前面文章的基础,在这里我就不详细将宏展开详解了,有了上面基础,我们就可以"真刀真枪"的读写文件了,比较起来“写文件”较容易,所以就让我们拿它先开刀吧!^_^
//////////////////////////////////////
/* 2.“写文件” */
//////////////////////////////////////
大家都应该了解“写文件”的“导火索”是什么吧?你说对了"save"or"save as",下面我们就沿着这个导火索一路下行看看到底发生了什么吧?
这里大家会发现一个小问题:那就是你在MFC应用程序向导为你做的SDI or MDI代码中找不到
"save"or"save as"功能项的处理函数,但它的功能却还能淋漓尽致的展现在你面前。这是为什么呢?原来这些函数是由CDocument类提供的。你的Doc类继承CDocument类的同时也将这些处理函数完全集成了下来。下面我们就来看看他们的卢山真面目吧!
当你按下"save"or"save as"功能键(包括菜单中的和工具栏中的)后,应用程序将调用
CMyDoc::OnFileSave() or CMyDoc::OnFileSaveAs()(以后将只提及一个),但由于CMyDoc类继承了其基类
CDocument的处理函数,所以实际调用的是CDocument::OnFileSave();
//in doccore.cpp
void CDocument::OnFileSave()
{
DoFileSave();
}
BOOL CDocument::DoFileSave()
{
DWORD dwAttrib = GetFileAttributes(m_strPathName);
if (dwAttrib & FILE_ATTRIBUTE_READONLY)
{
// we do not have read-write access or the file does not (now) exist
if (!DoSave(NULL))
{
TRACE(traceAppMsg, 0, "Warning: File save with new name failed.\n");
return FALSE;
}
}
else
{
if (!DoSave(m_strPathName))
{
TRACE(traceAppMsg, 0, "Warning: File save failed.\n");
return FALSE;
}
}
return TRUE;
}
BOOL CDocument::DoSave(LPCTSTR lpszPathName, BOOL bReplace)
// Save the document data to a file
// lpszPathName = path name where to save document file
// if lpszPathName is NULL then the user will be prompted (SaveAs)
// note: lpszPathName can be different than 'm_strPathName'
// if 'bReplace' is TRUE will change file name if successful (SaveAs)
// if 'bReplace' is FALSE will not change path name (SaveCopyAs)
{
CString newName = lpszPathName;
if (newName.IsEmpty())
{
...//
if (!AfxGetApp()->DoPromptFileName(newName,
bReplace ? AFX_IDS_SAVEFILE : AFX_IDS_SAVEFILECOPY,
OFN_HIDEREADONLY | OFN_PATHMUSTEXIST, FALSE, pTemplate))//***
return FALSE; // don't even attempt to save
}
CWaitCursor wait;
if (!OnSaveDocument(newName))
{
if (lpszPathName == NULL)
{
// be sure to delete the file
TRY
{
CFile::Remove(newName);
}
CATCH_ALL(e)
{
TRACE(traceAppMsg, 0, "Warning: failed to delete file after failed SaveAs.\n");
DELETE_EXCEPTION(e);
}
END_CATCH_ALL
}
return FALSE;
}
// reset the title and change the document name
if (bReplace)
SetPathName(newName);
return TRUE; // success
}
CDocument::DoSave函数继续调用OnSaveDocument(...)函数来完成写入任务。
BOOL CDocument::OnSaveDocument(LPCTSTR lpszPathName)
{
CFileException fe;
CFile* pFile = NULL;
pFile = GetFile(lpszPathName, CFile::modeCreate |
CFile::modeReadWrite | CFile::shareExclusive, &fe);
...//
CArchive saveArchive(pFile, CArchive::store | CArchive::bNoFlushOnDelete);
saveArchive.m_pDocument = this;
saveArchive.m_bForceFlat = FALSE;
TRY
{
CWaitCursor wait;
Serialize(saveArchive); // save me
saveArchive.Close();
ReleaseFile(pFile, FALSE);
}
...//
SetModifiedFlag(FALSE); // back to unmodified
return TRUE; // success
}
该函数创建了一个与要保存的文件相关联的CArchive实例saveArchive,并调用了函数 CMyDoc::Serialize(CArchive&ar);
void CMyDoc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
// TODO:在此添加存储代码/写入
}
else
{
// TODO:在此添加加载代码/读出
}
}
看看该函数,你有些傻眼了,空函数几乎什么也没有,这又对了,因为MFC不知道你的数据是什么样式的,所以它没有这个能力越俎代庖。如果你没有为该函数添加代码,则该函数也就到此为止了,但我们要把其奥秘挖掘出来就不能到此结束,我们也用类似侯捷老师的Scribble例子给CMyDoc类加一点代码,改为:
class CStroke:public CObject//线条类
{ ...//
protected: UINT m_nPenWidth;
public: CArray
}
class CMyDoc:public CDocument
{
...//
CTypedPtrList
}
void CMyDoc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
// TODO:在此添加存储代码/写入
}
else
{
// TODO:在此添加加载代码/读出
}
m_strokList.Serialize(ar);
}
void CStroke::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
// TODO:在此添加存储代码/写入
ar<<(WORD)m_nPenWidth;
m_pointArray.Serialize(ar);
}
else
{
// TODO:在此添加加载代码/读出
WORD w;
ar>>w;
m_nPenWidth=w;
m_pointArray.Serialize(ar);
}
下面我们可以继续我们的挖掘了:
CTypedPtrList::Serialize函数被调用,由于CTypedPtrList并为改写Serialize函数,所以实际调用的是其基类CObList的Serialize函数;
//in list_o.cpp
void CObList::Serialize(CArchive& ar)
{
ASSERT_VALID(this);
CObject::Serialize(ar);
if (ar.IsStoring())
{
ar.WriteCount(m_nCount);//***
for (CNode* pNode = m_pNodeHead; pNode != NULL; pNode = pNode->pNext)
{
ASSERT(AfxIsValidAddress(pNode, sizeof(CNode)));
ar << pNode->data;//***
}
}
else
{
...//
}
}
其中void CArchive::WriteCount(DWORD_PTR dwCount)函数用于将CObList中的表元素个书写入。
CArchive重载了<<运算符,代码如下:
//in afx.inl
_AFX_INLINE CArchive& AFXAPI operator<<(CArchive& ar, const CObject* pOb)
{ ar.WriteObject(pOb); return ar; }
//in arcobj.cpp
void CArchive::WriteObject(const CObject* pOb)
{
...//
// make sure m_pStoreMap is initialized
MapObject(NULL);
if (pOb == NULL)
{
// save out null tag to represent NULL pointer
*this << wNullTag;
}
else if ((nObIndex = (DWORD)(DWORD_PTR)(*m_pStoreMap)[(void*)pOb]) != 0)
// assumes initialized to 0 map
{
// save out index of already stored object
if (nObIndex < wBigObjectTag)
*this << (WORD)nObIndex;
else
{
*this << wBigObjectTag;
*this << nObIndex;
}
}
else
{
// write class of object first
CRuntimeClass* pClassRef = pOb->GetRuntimeClass();
WriteClass(pClassRef);//***
// enter in stored object table, checking for overflow
CheckCount();
(*m_pStoreMap)[(void*)pOb] = (void*)(DWORD_PTR)m_nMapCount++;
// cause the object to serialize itself
((CObject*)pOb)->Serialize(*this);//***
}
}
((CObject*)pOb)->Serialize(*this);
循线而上,你可以发现该函数最终调用的是CStroke::Serialize(CArchive&ar);
也许你可能不晓得wNullTag,dwBigClassTag等之类是何东东?看看下面它们是如何定义的:
// Pointer mapping constants,in arcobj.cpp
#define wNullTag ((WORD)0) // special tag indicating NULL ptrs
#define wNewClassTag ((WORD)0xFFFF) // special tag indicating new CRuntimeClass
#define wClassTag ((WORD)0x8000) // 0x8000 indicates class tag (OR'd)
#define dwBigClassTag ((DWORD)0x80000000) // 0x8000000 indicates big class tag (OR'd)
#define wBigObjectTag ((WORD)0x7FFF) // 0x7FFF indicates DWORD object tag
#define nMaxMapCount ((DWORD)0x3FFFFFFE) // 0x3FFFFFFE last valid mapCount
他们不是别的,只是一些记号,比如当你写入一个CStroke类时,它首先判断CStroke以前是否出现过,若没有,则写入 wNewClassTag (0xFFFF),否则写入wClassTag+一定的offsets,表示这是与前面相同的旧类。
//MFC文档:to store the version and class information of a base class during serialization of the derived class.
void CArchive::WriteClass(const CRuntimeClass* pClassRef)
{
...//
// make sure m_pStoreMap is initialized
MapObject(NULL);
// write out class id of pOb, with high bit set to indicate
// new object follows
// ASSUME: initialized to 0 map
DWORD nClassIndex;
if ((nClassIndex = (DWORD)(DWORD_PTR)(*m_pStoreMap)[(void*)pClassRef]) != 0)
{
// previously seen class, write out the index tagged by high bit
if (nClassIndex < wBigObjectTag)
*this << (WORD)(wClassTag | nClassIndex);
else
{
*this << wBigObjectTag;
*this << (dwBigClassTag | nClassIndex);
}
}
else
{
// store new class
*this << wNewClassTag;
pClassRef->Store(*this);//***
// store new class reference in map, checking for overflow
CheckCount();
(*m_pStoreMap)[(void*)pClassRef] = (void*)(DWORD_PTR)m_nMapCount++;
}
}
最终调用void CRuntimeClass::Store(CArchive& ar) const;来存储class information;
到这就"写"完了。下面总结一下:
我们看看本程序到底向硬盘中写了什么?
按顺序应该是:CTypedPtrList中的元素个数---〉新旧类标志---〉版本号(m_wSchema)---〉
类名称字符串中的字符个数----〉类名称(ANSI码)---〉调用其它成员的 Serialize 函数。---〉以此类推。
//////////////////////////////////////
/* 3.“读文件” */
//////////////////////////////////////
看完了“写文件”,让我们看看“读文件”的内幕吧!“读文件”顾名思义,即当你打开一个文件时,应用程序从硬盘中将文件的数据读出的过程。
当你选中“文件”菜单中的“打开”或在工具栏中单击“打开”项时,应用程序将连续调用以下序列的函数:
CWinApp::OnOpenFile()——>CDocManager::OnFileOpen()-->CWinApp::OpenDocumentFile(LPCTSTR lpszFileName)-->CDocManager::OpenDocumentFile(LPCTSTR lpszFileName)--->
CSingleDocTemplate::OpenDocumentFile(LPCTSTR lpszPathName,BOOL bMakeVisible)-->
//in Doccore.cpp
BOOL CDocument::OnOpenDocument(LPCTSTR lpszPathName)
{ ...//
CFileException fe;
CFile* pFile = GetFile(lpszPathName,
CFile::modeRead|CFile::shareDenyWrite, &fe);
if (pFile == NULL)
{
ReportSaveLoadException(lpszPathName, &fe,
FALSE, AFX_IDP_FAILED_TO_OPEN_DOC);
return FALSE;
}
DeleteContents();
SetModifiedFlag(); // dirty during de-serialize
CArchive loadArchive(pFile, CArchive::load | CArchive::bNoFlushOnDelete);//***
loadArchive.m_pDocument = this;
loadArchive.m_bForceFlat = FALSE;
TRY
{
CWaitCursor wait;
if (pFile->GetLength() != 0)
Serialize(loadArchive); // load me***
loadArchive.Close();
ReleaseFile(pFile, FALSE);
}
CATCH_ALL(e)
{
ReleaseFile(pFile, TRUE);
DeleteContents(); // remove failed contents
TRY
{
ReportSaveLoadException(lpszPathName, e,
FALSE, AFX_IDP_FAILED_TO_OPEN_DOC);
}
END_TRY
DELETE_EXCEPTION(e);
return FALSE;
}
END_CATCH_ALL
SetModifiedFlag(FALSE); // start off with unmodified
return TRUE;
}
--->void CMyDoc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
// TODO:在此添加存储代码/写入
}
else
{
// TODO:在此添加加载代码/读出
}
m_strokList.Serialize(ar);
}
//in list_o.cpp
-->void CObList::Serialize(CArchive& ar)
{
ASSERT_VALID(this);
CObject::Serialize(ar);
if (ar.IsStoring())
{
...//
}
else
{
DWORD_PTR nNewCount = ar.ReadCount();//读入CTypedPtrList中的元素个数
CObject* newData;
while (nNewCount--)
{
ar >> newData;//***
AddTail(newData);
}
}
}
---〉
_AFX_INLINE CArchive& AFXAPI operator>>(CArchive& ar, CObject*& pOb)
{ pOb = ar.ReadObject(NULL); return ar; }
--->//in arcobj.cpp
CObject* CArchive::ReadObject(const CRuntimeClass* pClassRefRequested)
{
...//
// attempt to load next stream as CRuntimeClass
UINT nSchema;
DWORD obTag;
CRuntimeClass* pClassRef = ReadClass(pClassRefRequested, &nSchema, &obTag);//***
// check to see if tag to already loaded object
CObject* pOb;
if (pClassRef == NULL)
{
if (obTag > (DWORD)m_pLoadArray->GetUpperBound())
{
// tag is too large for the number of objects read so far
AfxThrowArchiveException(CArchiveException::badIndex,
m_strFileName);
}
pOb = (CObject*)m_pLoadArray->GetAt(obTag);
if (pOb != NULL && pClassRefRequested != NULL &&
!pOb->IsKindOf(pClassRefRequested))
{
// loaded an object but of the wrong class
AfxThrowArchiveException(CArchiveException::badClass,
m_strFileName);
}
}
else
{
// allocate a new object based on the class just acquired
pOb = pClassRef->CreateObject();//***
if (pOb == NULL)
AfxThrowMemoryException();
// Add to mapping array BEFORE de-serializing
CheckCount();
m_pLoadArray->InsertAt(m_nMapCount++, pOb);
// Serialize the object with the schema number set in the archive
UINT nSchemaSave = m_nObjectSchema;
m_nObjectSchema = nSchema;
pOb->Serialize(*this);//***
m_nObjectSchema = nSchemaSave;
ASSERT_VALID(pOb);
}
return pOb;
}
--->//读取CRuntimeClass信息
CRuntimeClass* CArchive::ReadClass(const CRuntimeClass* pClassRefRequested,
UINT* pSchema, DWORD* pObTag)
{
...//
// make sure m_pLoadArray is initialized
MapObject(NULL);
// read object tag - if prefixed by wBigObjectTag then DWORD tag follows
DWORD obTag;
WORD wTag;
*this >> wTag;//***读取标志位
if (wTag == wBigObjectTag)
*this >> obTag;
else
obTag = ((wTag & wClassTag) << 16) | (wTag & ~wClassTag);
// check for object tag (throw exception if expecting class tag)
if (!(obTag & dwBigClassTag))
{
if (pObTag == NULL)
AfxThrowArchiveException(CArchiveException::badIndex, m_strFileName);
*pObTag = obTag;
return NULL;
}
CRuntimeClass* pClassRef;
UINT nSchema;
if (wTag == wNewClassTag)
{
// new object follows a new class id
if ((pClassRef = CRuntimeClass::Load(*this, &nSchema)) == NULL)//***
AfxThrowArchiveException(CArchiveException::badClass, m_strFileName);
// check nSchema against the expected schema
if ((pClassRef->m_wSchema & ~VERSIONABLE_SCHEMA) != nSchema)
{
if (!(pClassRef->m_wSchema & VERSIONABLE_SCHEMA))
{
// schema doesn't match and not marked as VERSIONABLE_SCHEMA
AfxThrowArchiveException(CArchiveException::badSchema,
m_strFileName);
}
else
{
// they differ -- store the schema for later retrieval
if (m_pSchemaMap == NULL)
m_pSchemaMap = new CMapPtrToPtr;
ASSERT_VALID(m_pSchemaMap);
m_pSchemaMap->SetAt(pClassRef, (void*)(DWORD_PTR)nSchema);
}
}
CheckCount();
m_pLoadArray->InsertAt(m_nMapCount++, pClassRef);
}
else
{
...//
}
// check for correct derivation
if (pClassRefRequested != NULL &&
!pClassRef->IsDerivedFrom(pClassRefRequested))
{
AfxThrowArchiveException(CArchiveException::badClass, m_strFileName);
}
// store nSchema for later examination
if (pSchema != NULL)
*pSchema = nSchema;
else
m_nObjectSchema = nSchema;
// store obTag for later examination
if (pObTag != NULL)
*pObTag = obTag;
// return the resulting CRuntimeClass*
return pClassRef;
}
--->
CRuntimeClass* PASCAL CRuntimeClass::Load(CArchive& ar, UINT* pwSchemaNum)
// loads a runtime class description
{
WORD nLen;
char szClassName[64];
WORD wTemp;
ar >> wTemp; *pwSchemaNum = wTemp;//读入版本号
ar >> nLen;//读入类名称字符串中的字符个数
// load the class name,读入类名称
if (nLen >= _countof(szClassName) ||
ar.Read(szClassName, nLen*sizeof(char)) != nLen*sizeof(char))
{
return NULL;
}
szClassName[nLen] = '\0';
// match the string against an actual CRuntimeClass
CRuntimeClass* pClass = FromName(szClassName);
if (pClass == NULL)
{
// not found, trace a warning for diagnostic purposes
TRACE(traceAppMsg, 0, "Warning: Cannot load %hs from archive. Class not defined.\n",
szClassName);
}
return pClass;
}
---〉在CArchive::ReadObject函数中有pOb->Serialize(*this);即调用CStroke类的Serialize函数。以此类推。
总结:我们看看本程序到底从硬盘中读了什么?
按顺序应该是:CTypedPtrList中的元素个数---〉新旧类标志---〉版本号(m_wSchema)---〉
类名称字符串中的字符个数----〉类名称(ANSI码)---〉调用其它成员的 Serialize 函数。---〉以此类推。
由此可以看出读入数据的顺序与写入数据时的顺序完全相同。
/////////////////////////////////////
/* 4.结局 */
/////////////////////////////////////
至此,MFC技术内幕系列文章都已结束,文章中肯定有很多纰漏和错误,希望读者们批评指点
chinaunix网友2010-12-07 10:06:12
很好的, 收藏了 推荐一个博客,提供很多免费软件编程电子书下载: http://free-ebooks.appspot.com