首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> VC >> 關於VC++

MFC CArchive 核心原理:緩衝區管理與物件序列化全流程

     編輯:關於VC++

前言

CArchive 是 MFC 序列化架構的核心引擎,它實現了高效的緩衝區 I/O、基本型別與 CString 的流式讀寫、以及 CObject 派生物件的多態序列化。本文將從緩衝區模型、基本型別讀寫、大塊資料處理、字串序列化格式、物件圖儲存與類別資訊管理五大維度,系統剖析其內部運作機制。

一、緩衝區模型:鐵路調度站隱喻

CArchive 內部維護一塊連續記憶體緩衝區,三個指標協同工作:

  • m_lpBufStart:緩衝區起始位址
  • m_lpBufMax:緩衝區末尾位址(容量邊界)
  • m_lpBufCur:當前讀寫位置

初始化邏輯:

m_lpBufCur = IsLoading() ? m_lpBufMax : m_lpBufStart;
// 讀模式:游標從尾部起(資料已滿)
// 寫模式:游標從頭部起(資料為空)

這設計使得「讀取」是向前消費,「寫入」是向後堆疊,直觀且高效。

二、基本型別流式運算子:以 DWORD 為例

插入(寫入)

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) - (m_lpBufMax - m_lpBufCur));

    dw = *(DWORD*)m_lpBufCur;                    // 讀取
    m_lpBufCur += sizeof(DWORD);

    if (!(m_nMode & bNoByteSwap))
        _AfxByteSwap(dw, (BYTE*)&dw);            // 大小端轉換

    return *this;
}

關鍵點:

  1. 自動緩衝區管理:空間/資料不足時透明呼叫 Flush/FillBuffer。
  2. 位元組序處理:bNoByteSwap 模式控制是否執行 _AfxByteSwap,支援跨平台資料交換。
  3. 連續操作:返回 *this 支援鏈式呼叫 ar << a << b << c。

三、緩衝區更新核心:Flush 與 FillBuffer

Flush() —— 寫模式下的提交

void CArchive::Flush()
{
    if (IsLoading()) { /* 讀模式:退回未讀資料,游標歸尾 */ }
    else { // 寫模式
        if (!m_bDirectBuffer) {
            if (m_lpBufCur != m_lpBufStart)
                m_pFile->Write(m_lpBufStart, m_lpBufCur - m_lpBufStart);
        } else { // CMemFile 等直接緩衝
            if (m_lpBufCur != m_lpBufStart)
                m_pFile->GetBufferPtr(CFile::bufferCommit, m_lpBufCur - m_lpBufStart);
            // 取得下一塊緩衝區
            VERIFY(m_pFile->GetBufferPtr(CFile::bufferWrite, m_nBufSize,
                (void**)&m_lpBufStart, (void**)&m_lpBufMax) == (UINT)m_nBufSize);
        }
        m_lpBufCur = m_lpBufStart; // 重置游標
    }
}

FillBuffer() —— 讀模式下的補充

void CArchive::FillBuffer(UINT nBytesNeeded)
{
    UINT nUnused = m_lpBufMax - m_lpBufCur; // 剩餘未讀
    ULONG nTotalNeeded = nBytesNeeded + nUnused;

    if (!m_bDirectBuffer) {
        if (m_lpBufCur > m_lpBufStart) {
            // 移動殘留資料至緩衝區頭部
            memmove(m_lpBufStart, m_lpBufCur, nUnused);
            m_lpBufCur = m_lpBufStart;
            m_lpBufMax = m_lpBufStart + nUnused;
        }
        // 從檔案讀取滿足需求
        UINT nRead = nUnused, nLeft = m_nBufSize - nUnused;
        BYTE* lpTemp = m_lpBufStart + nUnused;
        do {
            nBytes = m_pFile->Read(lpTemp, nLeft);
            lpTemp += nBytes; nRead += nBytes; nLeft -= nBytes;
        } while (nBytes > 0 && nLeft > 0 && nRead < nBytesNeeded);
        m_lpBufMax = m_lpBufStart + nRead;
    } else { // 直接緩衝
        if (nUnused) m_pFile->Seek(-(LONG)nUnused, CFile::current);
        UINT nActual = m_pFile->GetBufferPtr(CFile::bufferRead, m_nBufSize,
            (void**)&m_lpBufStart, (void**)&m_lpBufMax);
        m_lpBufCur = m_lpBufStart;
    }
    if ((ULONG)(m_lpBufMax - m_lpBufCur) < nTotalNeeded)
        AfxThrowArchiveException(CArchiveException::endOfFile);
}

四、大塊資料 Read/Write:避免緩衝區來回拷貝

對於大於緩衝區的資料塊,採用「頭尾用緩衝區,中間直接 I/O」策略:

  1. 先消耗/填滿當前緩衝區剩餘部分。
  2. 計算整數倍緩衝區大小的中間段,直接呼叫 m_pFile->Read/Write,繞過緩衝區。
  3. 最後餘數部分再次使用緩衝區。

此設計顯著提升大檔案(如影像、資料庫匯出)的序列化效能。

五、字串序列化:兩套機制並存

1. WriteString / ReadString —— 逐行文字模式

  • WriteString 直接呼叫 Write(lpsz, lstrlen(lpsz)*sizeof(TCHAR)),不寫入長度前綴。
  • ReadString 逐字元讀取直到 '\n' 或 '\r',支援負 nMax 保留換行符。
  • 適合純文字日誌、設定檔等人類可讀資料。

2. CString 專用 << / >> —— 二進位長度前綴格式

寫入格式(以 Unicode 為例):

// 特徵標記
ar << (BYTE)0xff << (WORD)0xfffe;

// 長度編碼
if (len < 255)       ar << (BYTE)len;
else if (len < 0xfffe) ar << (BYTE)0xff << (WORD)len;
else                 ar << (BYTE)0xff << (WORD)0xffff << (DWORD)len;

// 字元資料
ar.Write(str.m_pchData, len * sizeof(TCHAR));

讀取邏輯:_AfxReadStringLength 解析前綴,自動偵測 ANSI/Unicode 並轉換。

優勢:二進位安全、支援嵌入 null、跨平台字元集自適應、可嵌套於物件圖中。

六、CObject 物件圖序列化:身分識別與共享指標

WriteObject 流程

void CArchive::WriteObject(const CObject* pOb)
{
    MapObject(NULL); // 確保 m_pStoreMap 初始化
    if (pOb == NULL) { *this << wNullTag; return; }

    DWORD nObIndex = (DWORD)(*m_pStoreMap)[(void*)pOb];
    if (nObIndex != 0) { // 已寫入過
        *this << (nObIndex < wBigObjectTag ? (WORD)nObIndex : (wBigObjectTag, nObIndex));
        return;
    }

    // 首次寫入
    CRuntimeClass* pClassRef = pOb->GetRuntimeClass();
    WriteClass(pClassRef); // 寫入類別資訊
    CheckCount();
    (*m_pStoreMap)[(void*)pOb] = (void*)m_nMapCount++;
    pOb->Serialize(*this); // 遞迴序列化成員
}

ReadObject 流程

CObject* CArchive::ReadObject(const CRuntimeClass* pClassRefRequested)
{
    UINT nSchema; DWORD obTag;
    CRuntimeClass* pClassRef = ReadClass(pClassRefRequested, &nSchema, &obTag);

    if (pClassRef == NULL) { // 共享指標標記
        CObject* pOb = (CObject*)m_pLoadArray->GetAt(obTag);
        // 類別相容性檢查...
        return pOb;
    }

    // 新物件
    CObject* pOb = pClassRef->CreateObject(); // 動態建立
    CheckCount();
    m_pLoadArray->InsertAt(m_nMapCount++, pOb); // 註冊以支援共享指標

    UINT nSchemaSave = m_nObjectSchema;
    m_nObjectSchema = nSchema; // 設定版本號供 Serialize 使用
    pOb->Serialize(*this);
    m_nObjectSchema = nSchemaSave;
    return pOb;
}

關鍵機制

  • 物件身分映射表:m_pStoreMap(寫)與 m_pLoadArray(讀)維護 物件指標 ⇄ 索引 雙向映射,解決共享指標與循環引用。
  • 類別資訊去重:WriteClass/ReadClass 同樣使用 Map 機制,同一類別僅序列化一次 CRuntimeClass。
  • 版本協商:m_nObjectSchema 傳遞給 Serialize,衍生類可根據版本決定讀寫邏輯。

七、總結

CArchive 以緩衝區為核心,層層封裝:

  1. 位元組流:基本型別、大塊資料、字串的高效編碼。
  2. 物件圖:身分識別、類別資訊、版本控制、多態建立。
  3. 儲存抽象:透過 CFile 多型支援檔案、記憶體、Socket 等多種後端。

掌握其內部協議(特別是物件索引與類別去重邏輯),對於除錯序列化失效、跨版本資料遷移、實作自訂儲存後端極具指導意義。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved