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

MFC 序列化機制完整指南:CObject 派生、DECLARE/IMPLEMENT_SERIAL、CArchive 讀寫、版本控制與自定義類實戰

     編輯:關於C語言

MFC 序列化提供比原始文件操作更高層、類型安全、透明字節流的持久化方案。本文系統講解使類支持序列化的五步驟、CArchive 核心操作、版本控制機制、動態數據結構(鏈表)序列化實戰、多態指針數組存儲及避坑指南。

一、 使類支持序列化的五步驟 (必備儀式)

  1. 繼承 CObject:class CMyData : public CObject
  2. 聲明宏:類定義中 DECLARE_SERIAL(CMyData)
  3. 默認構造函數:CMyData() {} (反序列化時調用)
  4. 實現 Serialize:virtual void Serialize(CArchive& ar)
  5. 實現宏:CPP 文件 IMPLEMENT_SERIAL(CMyData, CObject, VERSION)

二、 Serialize 核心實現模式

void CMyData::Serialize(CArchive& ar) {
    CObject::Serialize(ar); // 調用基類 (通常為空)
    
    if (ar.IsStoring()) {
        // 存儲 (寫入文件)
        ar << m_strName << m_nAge << m_dSalary;
        // 複雜類型
        ar << m_rectBounds; // CRect 支持 <<
        m_font.Serialize(ar); // CFont 等 GDI 對象有 Serialize
    } else {
        // 加載 (讀取文件)
        ar >> m_strName >> m_nAge >> m_dSalary;
        ar >> m_rectBounds;
        m_font.Serialize(ar);
    }
}

CArchive 核心操作符

操作符支持類型說明
<< / >>基本類型 (int, float, double, BYTE, WORD, DWORD, LONG)自動處理字節序
<< / >>CString, CTime, CTimeSpan, COleDateTime字符串長度前綴 + 內容
<< / >>MFC 集合類 (CArray, CList, CMap, CStringArray 等)自動存儲計數 + 元素
<< / >>GDI 對象 (CRect, CPoint, CSize, CFont, CPen, CBrush)調用對象 Serialize
Write/Read原始緩衝區ar.Write(pBuf, len) / ar.Read(pBuf, len)
WriteString/ReadString文本行文本模式換行處理

三、 版本控制機制

IMPLEMENT_SERIAL(CMyData, CObject, 2) // 版本號 = 2

void CMyData::Serialize(CArchive& ar) {
    int schema = ar.GetObjectSchema(); // 獲取文件中對象版本
    
    if (ar.IsStoring()) {
        // 寫入時始終用最新版本
        ar << m_strName << m_nAge << m_dSalary << m_bActive; // v2 新增字段
    } else {
        // 讀取時根據版本兼容
        ar >> m_strName >> m_nAge >> m_dSalary;
        if (schema >= 2) {
            ar >> m_bActive; // v2 才有此字段
        } else {
            m_bActive = TRUE; // v1 默認值
        }
    }
}

版本策略最佳實踐

  • 版本號單調遞增 (1, 2, 3...)
  • 新字段只追加在末尾,不改變舊字段順序
  • 讀取時按版本分支,舊版本字段設置合理默認值
  • 不可移除字段,只能廢棄 (讀取後忽略)

四、 實戰:動態鏈表序列化 (身份證記錄系統)

類定義

// PIDRecord.h
struct PIDNode {
    char szName[32];
    char szID[18];
    // 注意:指針不直接序列化!
};

class CPIDList : public CObject {
    DECLARE_SERIAL(CPIDList)
public:
    CPIDList();
    virtual ~CPIDList();
    
    void AddRecord(LPCTSTR name, LPCTSTR id);
    // ... 其他操作
    
protected:
    PIDNode* m_pHead; // 鏈表頭指針
    
    virtual void Serialize(CArchive& ar);
};

IMPLEMENT_SERIAL(CPIDList, CObject, 1)

序列化實現 (關鍵:指針轉順序存儲)

void CPIDList::Serialize(CArchive& ar) {
    CObject::Serialize(ar);
    
    if (ar.IsStoring()) {
        // 1. 計算節點數
        int count = 0;
        for (PIDNode* p = m_pHead; p; p = p->pNext) count++;
        
        // 2. 寫入節點數
        ar << count;
        
        // 3. 順序寫入節點數據 (不寫指針!)
        for (PIDNode* p = m_pHead; p; p = p->pNext) {
            ar.Write(p->szName, sizeof(p->szName));
            ar.Write(p->szID, sizeof(p->szID));
        }
    } else {
        // 1. 讀取節點數
        int count;
        ar >> count;
        
        // 2. 清空現有鏈表
        ClearList();
        
        // 3. 順序讀取並重建鏈表
        for (int i = 0; i < count; ++i) {
            PIDNode* pNode = new PIDNode;
            ar.Read(pNode->szName, sizeof(pNode->szName));
            ar.Read(pNode->szID, sizeof(pNode->szID));
            pNode->pNext = nullptr;
            
            AppendNode(pNode); // 尾插法重建
        }
    }
}

五、 多態指針數組序列化 (關鍵難點)

場景:CArray m_shapes,CShape 有派生 CCircle, CRectangle。

解決方案:運行時類信息 + 動態創建

// 基類必須支持動態創建
class CShape : public CObject {
    DECLARE_SERIAL(CShape)
    DECLARE_DYNCREATE(CShape) // 關鍵!
public:
    virtual void Draw(CDC*) = 0;
    virtual void Serialize(CArchive& ar);
};

IMPLEMENT_SERIAL(CShape, CObject, 1)
IMPLEMENT_DYNCREATE(CShape, CObject) // 實現動態創建

// 派生類同樣需要
class CCircle : public CShape {
    DECLARE_SERIAL(CCircle)
    DECLARE_DYNCREATE(CCircle)
    // ...
};
IMPLEMENT_SERIAL(CCircle, CShape, 1)
IMPLEMENT_DYNCREATE(CCircle, CShape)

// 容器類序列化
void CDrawDoc::Serialize(CArchive& ar) {
    if (ar.IsStoring()) {
        ar << (int)m_shapes.GetSize();
        for (int i = 0; i < m_shapes.GetSize(); ++i) {
            m_shapes[i]->Serialize(ar); // 多態調用,自動寫入類名
        }
    } else {
        int count;
        ar >> count;
        m_shapes.SetSize(count);
        for (int i = 0; i < count; ++i) {
            CShape* pShape = NULL;
            pShape->Serialize(ar); // 關鍵:靜態調用基類 Serialize,內部會根據類名動態創建對象!
            m_shapes[i] = pShape;
        }
    }
}

原理:CObject::Serialize 內部寫入/讀取 CRuntimeClass 信息,反序列化時調用 CRuntimeClass::CreateObject() 動態創建正確類型對象,隨後調用該對象的 Serialize。

六、 文檔類自動序列化流程

// CDocument 成員
virtual void Serialize(CArchive& ar); // 子類重載

// 框架自動調用流程
// 1. 用戶選「保存」→ CDocument::OnSaveDocument(lpszPathName)
// 2. CFile file(lpszPath, CFile::modeCreate|modeWrite)
// 3. CArchive ar(&file, CArchive::store)
// 4. Serialize(ar) → 子類實現
// 5. ar.Close(), file.Close()

// 加載同理:OnOpenDocument → CArchive::load → Serialize

七、 避坑指南

  1. 忘記默認構造函數:反序列化會調用 pClass->CreateObject() 需無參構造
  2. 指針直接序列化:地址無意義,必須轉為順序數據重建結構
  3. 版本號倒退:導致新版本程序讀舊文件邏輯錯誤
  4. 多態容器缺少 DECLARE_DYNCREATE:無法動態創建派生對象
  5. 大對象性能:序列化會完整寫入內存映射,巨大文檔考慮分塊/壓縮/異步
  6. 跨模塊類名衝突:DLL 邊界傳遞序列化需確保 CRuntimeClass 正確導出

本文整理自 MFC 經典技術文檔,適用於 VC6~VS2022 所有版本。

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