天天看點

MFC淺析(8) CArchive 原理

MFC 提供CArchive類實作資料的緩沖區讀寫,同時定義了類對象的存儲與讀取方案。

以下對CArchvie 的内部實作作分析。

1.概述

2.内部資料

3.基本資料讀寫

4.緩沖區的更新

5.指定長度資料段落的讀寫

6.字元串的讀寫

7.CObject派生對象的讀寫

1.概述

CArchive使用了緩沖區,即一段記憶體空間作為臨時資料存儲地,對CArchive的讀寫都先依次排列到此緩沖區,當緩沖區滿或使用者要求時,将此段整理後的資料讀寫到指定的存儲煤質。

當建立CArchive對象時,應指定其模式是用于緩沖區讀,還是用于緩沖區寫。

可以這樣了解,CArchive對象相當于鐵路的貨運練排程站,零散的貨物被收集,當總量到達火車運量的時候,由火車裝運走。

當接到火車的貨物時,則貨物由被分散到各自的貨主。與貨運不同的是,交貨、取貨是按時間循序執行的,而不是憑票據。是以必須保證送貨的和取貨的貨主按同樣的循序去存或取。

對于大型的貨物,則是拆散成火車機關,運走,取貨時,依次取各部分,組裝成原物。

2.内部資料

緩沖區指針 BYTE* m_lpBufStart,指向緩沖區,這個緩沖區有可能是底層CFile(如派生類CMemFile)對象提供的,但一般是CArchive自己建立的。

緩沖區尾部指針 BYTE* m_lpBufMax;

緩沖區目前位置指針 BYTE* m_lpBufCur;

初始化時,如果是讀模式,目前位置在尾部,如果是寫模式,目前位置在頭部:

m_lpBufCur = (IsLoading()) ? m_lpBufMax : m_lpBufStart;

3.基本資料讀寫

對于基本的資料類型,例如位元組、雙字等,可以直接使用">>"、"<<"符号進行讀出、寫入。

 //操作符定義捕:

 //插入操作

 CArchive& operator<<(BYTE by);

 CArchive& operator<<(WORD w);

 CArchive& operator<<(LONG l);

 CArchive& operator<<(DWORD dw);

 CArchive& operator<<(float f);

 CArchive& operator<<(double d);

 CArchive& operator<<(int i);

 CArchive& operator<<(short w);

 CArchive& operator<<(char ch);

 CArchive& operator<<(unsigned u);

 //提取操作

 CArchive& operator>>(BYTE& by);

 CArchive& operator>>(WORD& w);

 CArchive& operator>>(DWORD& dw);

 CArchive& operator>>(LONG& l);

 CArchive& operator>>(float& f);

 CArchive& operator>>(double& d);

 CArchive& operator>>(int& i);

 CArchive& operator>>(short& w);

 CArchive& operator>>(char& ch);

 CArchive& operator>>(unsigned& u);

下面以雙字為例,分析原碼

雙字的插入(寫)

CArchive& CArchive::operator<<(DWORD dw)

{

 if (m_lpBufCur + sizeof(DWORD) > m_lpBufMax) //緩沖區空間不夠

  Flush();  //緩沖區内容送出到實際存儲煤質。

 if (!(m_nMode & bNoByteSwap))

  _AfxByteSwap(dw, m_lpBufCur);  //處理位元組順序

 else

  *(DWORD*)m_lpBufCur = dw;      //添入緩沖區

 m_lpBufCur += sizeof(DWORD);     //移動目前指針

 return *this;

}

雙字的提取(讀)

CArchive& CArchive::operator>>(DWORD& dw)

 if (m_lpBufCur + sizeof(DWORD) > m_lpBufMax) //緩沖區要讀完了

  FillBuffer(sizeof(DWORD) - (UINT)(m_lpBufMax - m_lpBufCur));  //重新讀入内容到緩沖區

 dw = *(DWORD*)m_lpBufCur;  //讀取雙字

 m_lpBufCur += sizeof(DWORD); //移動目前位置指針

  _AfxByteSwap(dw, (BYTE*)&dw);  //處理位元組順序

4.緩沖區的更新

以上操作中,當緩沖區将插入滿或緩沖區将提取空時,都将對緩沖區進行更新處理

緩沖區将插入緻滿時調用Flush();

void CArchive::Flush()

 ASSERT_VALID(m_pFile);

 ASSERT(m_bDirectBuffer || m_lpBufStart != NULL);

 ASSERT(m_bDirectBuffer || m_lpBufCur != NULL);

 ASSERT(m_lpBufStart == NULL ||

  AfxIsValidAddress(m_lpBufStart, m_lpBufMax - m_lpBufStart, IsStoring()));

 ASSERT(m_lpBufCur == NULL ||

  AfxIsValidAddress(m_lpBufCur, m_lpBufMax - m_lpBufCur, IsStoring()));

 if (IsLoading())

 {

  // unget the characters in the buffer, seek back unused amount

  if (m_lpBufMax != m_lpBufCur)

   m_pFile-> Seek(-(m_lpBufMax - m_lpBufCur), CFile::current);

  m_lpBufCur = m_lpBufMax;    // 指向尾

 }

 else   //寫模式

  if (!m_bDirectBuffer)

  {

   // 内容寫入到檔案

   if (m_lpBufCur != m_lpBufStart)

    m_pFile-> Write(m_lpBufStart, m_lpBufCur - m_lpBufStart);

  }

  else

   //如果是直接針對記憶體區域的的(例如CMemFile中) (隻需移動相關指針,指向新的一塊記憶體)

    m_pFile-> GetBufferPtr(CFile::bufferCommit, m_lpBufCur - m_lpBufStart);

   // get next buffer

   VERIFY(m_pFile-> GetBufferPtr(CFile::bufferWrite, m_nBufSize,

    (void**)&m_lpBufStart, (void**)&m_lpBufMax) == (UINT)m_nBufSize);

   ASSERT((UINT)m_nBufSize == (UINT)(m_lpBufMax - m_lpBufStart));

  m_lpBufCur = m_lpBufStart; //指向緩沖區首

緩沖區将提取空将調用GFileeBuffer,nBytesNeeded為目前剩餘部分上尚有用的位元組

void CArchive::FillBuffer(UINT nBytesNeeded)

 ASSERT(IsLoading());

 ASSERT(nBytesNeeded > 0);

 ASSERT(nBytesNeeded <= (UINT)m_nBufSize);

  AfxIsValidAddress(m_lpBufStart, m_lpBufMax - m_lpBufStart, FALSE));

  AfxIsValidAddress(m_lpBufCur, m_lpBufMax - m_lpBufCur, FALSE));

 UINT nUnused = m_lpBufMax - m_lpBufCur;

 ULONG nTotalNeeded = ((ULONG)nBytesNeeded) + nUnused;

 // 從檔案中讀取

 if (!m_bDirectBuffer)

  ASSERT(m_lpBufCur != NULL);

  ASSERT(m_lpBufStart != NULL);

  ASSERT(m_lpBufMax != NULL);

  if (m_lpBufCur > m_lpBufStart)

   //保留剩餘的尚未處理的部分,将它們移動到頭

   if ((int)nUnused > 0)

   {

    memmove(m_lpBufStart, m_lpBufCur, nUnused);

    m_lpBufCur = m_lpBufStart;

    m_lpBufMax = m_lpBufStart + nUnused;

   }

   // read to satisfy nBytesNeeded or nLeft if possible

   UINT nRead = nUnused;

   UINT nLeft = m_nBufSize-nUnused;

   UINT nBytes;

   BYTE* lpTemp = m_lpBufStart + nUnused;

   do

    nBytes = m_pFile-> Read(lpTemp, nLeft);

    lpTemp = lpTemp + nBytes;

    nRead += nBytes;

    nLeft -= nBytes;

   while (nBytes > 0 && nLeft > 0 && nRead < nBytesNeeded);

   m_lpBufCur = m_lpBufStart;

   m_lpBufMax = m_lpBufStart + nRead;

  // 如果是針對記憶體區域(CMemFile),移動相關指針,指向新的一塊記憶體

  if (nUnused != 0)

   m_pFile-> Seek(-(LONG)nUnused, CFile::current);

  UINT nActual = m_pFile-> GetBufferPtr(CFile::bufferRead, m_nBufSize,

   (void**)&m_lpBufStart, (void**)&m_lpBufMax);

  ASSERT(nActual == (UINT)(m_lpBufMax - m_lpBufStart));

  m_lpBufCur = m_lpBufStart;

 // not enough data to fill request?

 if ((ULONG)(m_lpBufMax - m_lpBufCur) < nTotalNeeded)

  AfxThrowArchiveException(CArchiveException::endOfFile);

5.指定長度資料段落的讀寫

以下分析

UINT Read(void* lpBuf, UINT nMax); 讀取長度為nMax的資料

void Write(const void* lpBuf, UINT nMax); 寫入指定長度nMax的資料

對于大段資料的讀寫,先使用目前緩沖區中的内容或空間讀取或寫入,若這些空間夠用了,則結束。

否則,從剩餘的資料中找出最大的緩沖區整數倍大小的一塊資料,直接讀寫到存儲煤質(不反複使用緩沖區)。

剩餘的餘數部分,再使用緩沖區讀寫。

(說明:緩沖區讀寫的主要目的是将零散的資料以緩沖區大小為尺度來處理。對于大型資料,其中間的部分,不是零散的資料,使用緩沖區已經沒有意思,故直接讀寫)

①讀取

UINT CArchive::Read(void* lpBuf, UINT nMax)

 if (nMax == 0)

  return 0;

 UINT nMaxTemp = nMax;  //還需要讀入的長度,讀入一部分,就減相應數值,直到此數值變為零

 //處理目前緩沖區中剩餘部分。

 //如果要求讀入位元組小于緩沖區中剩餘部分,則第一部分為要求讀入的位元組數,

 //否則讀入全部剩餘部分

 UINT nTemp = min(nMaxTemp, (UINT)(m_lpBufMax - m_lpBufCur));  

 memcpy(lpBuf, m_lpBufCur, nTemp);

 m_lpBufCur += nTemp;

 lpBuf = (BYTE*)lpBuf + nTemp; //移動讀出内容所在區域的指針

 nMaxTemp -= nTemp;

 //目前緩沖區中剩餘部分不夠要求讀入的長度。

 //還有位元組需要讀,則需要根據需要執行若幹次填充緩沖區,讀出,直到讀出指定位元組。

 if (nMaxTemp != 0) 

  //計算出去除尾數部分的位元組大小(整數個緩沖區大小)

  //對于這些部分,位元組從檔案對象中讀出,放到輸出緩沖區

  nTemp = nMaxTemp - (nMaxTemp % m_nBufSize); 

  UINT nRead = 0;

  UINT nLeft = nTemp;

  UINT nBytes;

  do

   nBytes = m_pFile-> Read(lpBuf, nLeft); //要求讀入此整數緩沖區部分大小

   lpBuf = (BYTE*)lpBuf + nBytes;

   nRead += nBytes;

   nLeft -= nBytes;

  while ((nBytes > 0) && (nLeft > 0)); 知道讀入了預定大小,或到達檔案尾

  nMaxTemp -= nRead;

  if (nRead == nTemp) //讀入的位元組等于讀入的整數倍部分  該讀最後的餘數部分了

   // 建立裝有此最後餘數部分的内容的CArchive的工作緩沖區。

   if (!m_bDirectBuffer)

    UINT nLeft = max(nMaxTemp, (UINT)m_nBufSize);

    UINT nBytes;

    BYTE* lpTemp = m_lpBufStart;

    nRead = 0;

    do

    {

     nBytes = m_pFile-> Read(lpTemp, nLeft);  //從檔案中讀入到CArchive緩沖區

     lpTemp = lpTemp + nBytes;

     nRead += nBytes;

     nLeft -= nBytes;

    }

    while ((nBytes > 0) && (nLeft > 0) && nRead < nMaxTemp);

    m_lpBufMax = m_lpBufStart + nRead;

   else

    nRead = m_pFile-> GetBufferPtr(CFile::bufferRead, m_nBufSize,

     (void**)&m_lpBufStart, (void**)&m_lpBufMax);

    ASSERT(nRead == (UINT)(m_lpBufMax - m_lpBufStart));

   //讀出此剩餘部分到輸出

   nTemp = min(nMaxTemp, (UINT)(m_lpBufMax - m_lpBufCur));

   memcpy(lpBuf, m_lpBufCur, nTemp);

   m_lpBufCur += nTemp;

   nMaxTemp -= nTemp;

 return nMax - nMaxTemp;

②儲存,寫入

void CArchive::Write(const void* lpBuf, UINT nMax)

  return;

 //讀入可能的部分到緩沖區目前的剩餘部分

 UINT nTemp = min(nMax, (UINT)(m_lpBufMax - m_lpBufCur));

 memcpy(m_lpBufCur, lpBuf, nTemp);

 lpBuf = (BYTE*)lpBuf + nTemp;

 nMax -= nTemp;

 if (nMax > 0)  //還有未寫入的部分

  Flush();    //将目前緩沖區寫入到存儲煤質

  //計算出整數倍緩沖區大小的位元組數

  nTemp = nMax - (nMax % m_nBufSize);

  m_pFile-> Write(lpBuf, nTemp);  //直接寫到檔案

  lpBuf = (BYTE*)lpBuf + nTemp;

  nMax -= nTemp;

  //剩餘部分添加到緩沖區

  if (m_bDirectBuffer)

   // sync up direct mode buffer to new file position

  // copy remaining to active buffer

  ASSERT(nMax < (UINT)m_nBufSize);

  ASSERT(m_lpBufCur == m_lpBufStart);

  memcpy(m_lpBufCur, lpBuf, nMax);

  m_lpBufCur += nMax;

6.字元串的讀寫

①CArchive提供的WriteString和ReadString

字元串寫

void CArchive::WriteString(LPCTSTR lpsz)

 ASSERT(AfxIsValidString(lpsz));

 Write(lpsz, lstrlen(lpsz) * sizeof(TCHAR));  //調用Write,将字元串對應的一段資料寫入

字元串讀(讀取一行字元串)

LPTSTR CArchive::ReadString(LPTSTR lpsz, UINT nMax)

 // if nMax is negative (such a large number doesn't make sense given today's

 // 2gb address space), then assume it to mean "keep the newline".

 int nStop = (int)nMax < 0 ? -(int)nMax : (int)nMax;

 ASSERT(AfxIsValidAddress(lpsz, (nStop+1) * sizeof(TCHAR)));

 _TUCHAR ch;

 int nRead = 0;

 TRY

  while (nRead < nStop)

   *this >> ch;  //讀出一個位元組

   // stop and end-of-line (trailing '/n' is ignored)  遇換行—回車

   if (ch == '/n' || ch == '/r')

    if (ch == '/r')

     *this >> ch;

    // store the newline when called with negative nMax

    if ((int)nMax != nStop)

     lpsz[nRead++] = ch;

    break;

   lpsz[nRead++] = ch;

 CATCH(CArchiveException, e)

  if (e-> m_cause == CArchiveException::endOfFile)

   DELETE_EXCEPTION(e);

   if (nRead == 0)

    return NULL;

   THROW_LAST();

 END_CATCH

 lpsz[nRead] = '/0';

 return lpsz;

ReadString到CString對象,可以多行字元

BOOL CArchive::ReadString(CString& rString)

 rString = &afxChNil;    // empty string without deallocating

 const int nMaxSize = 128;

 LPTSTR lpsz = rString.GetBuffer(nMaxSize);

 LPTSTR lpszResult;

 int nLen;

 for (;;)

  lpszResult = ReadString(lpsz, (UINT)-nMaxSize); // store the newline

  rString.ReleaseBuffer();

  // if string is read completely or EOF

  if (lpszResult == NULL ||

   (nLen = lstrlen(lpsz)) < nMaxSize ||

   lpsz[nLen-1] == '/n')

   break;

  nLen = rString.GetLength();

  lpsz = rString.GetBuffer(nMaxSize + nLen) + nLen;

 // remove '/n' from end of string if present

 lpsz = rString.GetBuffer(0);

 nLen = rString.GetLength();

 if (nLen != 0 && lpsz[nLen-1] == '/n')

  rString.GetBufferSetLength(nLen-1);

 return lpszResult != NULL;

②使用CString對象的"<<"與">>"符讀寫字元串

CString定義了輸入輸出符,可以象基本類型的資料一樣使用CArchive

的操作符定義

friend CArchive& AFXAPI operator<<(CArchive& ar, const CString& string);

friend CArchive& AFXAPI operator>>(CArchive& ar, CString& string);

// CString serialization code

// String format:

//      UNICODE strings are always prefixed by 0xff, 0xfffe

//      if < 0xff chars: len:BYTE, TCHAR chars

//      if >= 0xff characters: 0xff, len:WORD, TCHAR chars

//      if >= 0xfffe characters: 0xff, 0xffff, len:DWORD, TCHARs

CArchive& AFXAPI operator<<(CArchive& ar, const CString& string)

 // special signature to recognize unicode strings

#ifdef _UNICODE

 ar << (BYTE)0xff;

 ar << (WORD)0xfffe;

#endif

 if (string.GetData()-> nDataLength < 255)

  ar << (BYTE)string.GetData()-> nDataLength;

 else if (string.GetData()-> nDataLength < 0xfffe)

  ar << (BYTE)0xff;

  ar << (WORD)string.GetData()-> nDataLength;

  ar << (WORD)0xffff;

  ar << (DWORD)string.GetData()-> nDataLength;

 ar.Write(string.m_pchData, string.GetData()-> nDataLength*sizeof(TCHAR));

 return ar;

// return string length or -1 if UNICODE string is found in the archive

AFX_STATIC UINT AFXAPI _AfxReadStringLength(CArchive& ar)

 DWORD nNewLen;

 // attempt BYTE length first

 BYTE bLen;

 ar >> bLen;

 if (bLen < 0xff)

  return bLen;

 // attempt WORD length

 WORD wLen;

 ar >> wLen;

 if (wLen == 0xfffe)

  // UNICODE string prefix (length will follow)

  return (UINT)-1;

 else if (wLen == 0xffff)

  // read DWORD of length

  ar >> nNewLen;

  return (UINT)nNewLen;

  return wLen;

CArchive& AFXAPI operator>>(CArchive& ar, CString& string)

 int nConvert = 1;   // if we get ANSI, convert

#else

 int nConvert = 0;   // if we get UNICODE, convert

 UINT nNewLen = _AfxReadStringLength(ar);

 if (nNewLen == (UINT)-1)

  nConvert = 1 - nConvert;

  nNewLen = _AfxReadStringLength(ar);

  ASSERT(nNewLen != -1);

 // set length of string to new length

 UINT nByteLen = nNewLen;

 string.GetBufferSetLength((int)nNewLen);

 nByteLen += nByteLen * (1 - nConvert);  // bytes to read

 nByteLen += nByteLen * nConvert;    // bytes to read

 if (nNewLen == 0)

  string.GetBufferSetLength(0);

  string.GetBufferSetLength((int)nByteLen+nConvert);

 // read in the characters

 if (nNewLen != 0)

  ASSERT(nByteLen != 0);

  // read new data

  if (ar.Read(string.m_pchData, nByteLen) != nByteLen)

   AfxThrowArchiveException(CArchiveException::endOfFile);

  // convert the data if as necessary

  if (nConvert != 0)

   CStringData* pOldData = string.GetData();

   LPSTR lpsz = (LPSTR)string.m_pchData;

   LPWSTR lpsz = (LPWSTR)string.m_pchData;

   lpsz[nNewLen] = '/0';    // must be NUL terminated

   string.Init();   // don't delete the old data

   string = lpsz;   // convert with operator=(LPWCSTR)

   CString::FreeData(pOldData);

MFC中多數類都從CObject類派生,CObject類與CArchive類有着良好的合作關系,能實作将對象序列化儲存到檔案或其他媒介中去,或者讀取預先儲存的對象,動态建立對象等功能。

①CObject定義了針對CArvhive的輸入輸出操作符,可以向其他基本資料類型一樣使用"<<"、"<<"符号

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

 { ar.WriteObject(pOb); return ar; }

CArchive& AFXAPI operator>>(CArchive& ar, CObject*& pOb)

 { pOb = ar.ReadObject(NULL); return ar; }

當使用這些符号時,實際上執行的是CArchive的WriteObject和ReadObject成員

②WriteObject與ReadObject

在WriteObject與ReadObject中先寫入或讀取運作時類資訊(CRuntimeClas),再調用Serialze(..),按其中的代碼讀寫具體的對象資料。

是以,隻要在CObject派生類中重載Serilize()函數,寫入具體的讀寫過程,就可以使對象具有存儲與建立能力。

//将對象寫入到緩沖區

void CArchive::WriteObject(const CObject* pOb)

 DWORD nObIndex;

 // 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)(*m_pStoreMap)[(void*)pOb]) != 0)

  // assumes initialized to 0 map

  // save out index of already stored object

  if (nObIndex < wBigObjectTag)

   *this << (WORD)nObIndex;

   *this << wBigObjectTag;

   *this << nObIndex;

  // 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*)m_nMapCount++;

  // 調用CObject的Serialize成員,按其中的代碼寫入類中資料。

  ((CObject*)pOb)-> Serialize(*this);

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,

  // 建立對象

  pOb = pClassRef-> CreateObject();

  if (pOb == NULL)

   AfxThrowMemoryException();

  // Add to mapping array BEFORE de-serializing

  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); //調用CObject的Serialize,按其中代碼讀入對象資料。

  m_nObjectSchema = nSchemaSave;

  ASSERT_VALID(pOb);

 return pOb;

③運作時類資訊的讀寫

為了避免衆多重複的同類對象寫入重複的類資訊,CArchive中使用CMap對象儲存和檢索類資訊。