关闭

[MFC]串行化CArchive类

标签: MFCCArchive串行化串行化版本控制
846人阅读 评论(0) 收藏 举报
分类:

1. 串行化和并行化的概念:

    1) 问题背景——内存保存数据和磁盘保存数据的不同:

         i. 内存可以通过CPU进行寻址,因此内存中有指针这个概念,因此内存中的数据可以用非常复杂的数据结构组织;

         ii. 磁盘容量过于庞大,并且不能直接由CPU寻址,因此磁盘中没有指针这个概念,磁盘中的数据只能线性的按顺序一个个存储起来;

         iii. 因此说,内存中由数据结构组织起来的数据是立体的、复杂的,也就是并行化(有一种立体的感觉)的,而磁盘中的数据仅仅是简单的、线性的,也就是串行化的(像图这样的数据结构因为磁盘中没有指针的维护也只能线性地存取);

    2) 因此,将内存中的并行化的数据写入磁盘就是一种数据的串行化过程,而将数据从串行化的磁盘读入内存就是一种数据的并行化;

    3) MFC的串行化支持:

         i. 由于MFC的CFile类向磁盘读写数据要使用Read和Write,使用起来非常不方便,因此MFC又提供了一个CArchive类来支持串行化和并行化;

         ii. Archive即存档的意思,其实CArchive类即支持磁盘写入也支持读出,因此该词就理解为文档的意思,这也是MFC的文档/视图结构的基础,程序数据通过文档保存在磁盘中,这种保存和读取就是一种串并行化的行为;

         iii. 其中的主要步骤:首先将CArchive对象和一个文件CFile对象关联起来,然后就可以利用CArchive重载过的<<和>>进行数据输入输出(是指是读写磁盘),而这过程中CArchive底层调用了CFile的Read和Write函数,避免了程序员直接面对较为麻烦的CFile成员函数;

!!!其实CArchive类最最牛逼也最最高效的地方就是可以创建用户自定义的串行化类,然后直接就用<<和>>进行磁盘读写就行了,非常方便:

CArchive ar;
... // 将ar和具体的文件关联起来
CMyObject myObj; // 自定义的类符合串行化的协议
...
ar << myObj;

2. 串行化的基本用法:

    1) 利用CArchive的构造函数来和具体的文件相关联:

         i. 构造函数:CArchive::CArchive(CFile* pFile, UINT nMode, int nBufSize = 4096, void* lpBuf = NULL);

         ii. pFile就是与之关联的CFile对象,因此要先用CFile打开一个文件;

         iii. 第二个参数是串行化的模式,是下载还是存储:

CArchive::load:从磁盘读

CArchive::store:从向磁盘写

         iv. 第三个参数是串行化缓冲区的大小,一般就取默认值4KB即可,不用改;

         v. 第四个参数现在用不到;

    2) 向磁盘写:

CArchive ar(&file, CArchive::store);
ar << var_a << var_b;
...
    3) 从磁盘读:

CArchive ar(&file, CArchive::load);
ar >> var_a >> var_b;
...
!可以看到只需要简单的使用<<和>>运算符就能实现数据的磁盘存取;

    4) 但是要注意的是!并不是所有的类型的数据都可以直接使用<<和>>和CArchive对象进行串行化!

         i. MFC中所有的基本类型都是可以这样串行化的,像int、double、DWORD等基本类型都可以;

         ii. MFC也重载了一部分非基本类型对<<和>>的支持,比如CString、CTime、CSize、CPoint等都被重载过了,因此可以直接串行化;

         iii. 自定义的类想这样串行化必须要遵守串行化协议,必须现继承CObject,然后覆盖继承来的Serialize成员函数才能实现串行化,串行化是CObject就有的特性;


3. 定义可串行化类的步骤:

    1) 必须直接或间接继承CObject类,因为MFC对串行化的支持实现在CObject类中;

    2) 在类的声明中假如DECLARE_SERIAL宏,该宏只有一个参数就是该类的类名,比如DECALRE_SERIAL(CMyClass)

    3) 重载从CObject继承过来的Serialize函数,并在其中串行化派生类的数据成员,其函数原型是void CObject::Serialize(CArchive& ar);

    4) 必须要定义无参构造函数,因为在并行化时MFC底层会使用到,因此这点特别值得注意!!!

    5) !非常重要的一点——版本控制!(会在后面的小节中详细讲述);

示例:

class CLine: public CObject // 必须要直接或间接继承CObject,其中包含了对串行化的支持
{
	DECLARE_SERIAL(CLine) // 声明该类遵守了串行化的协议

protected:
	CPoint m_ptFrom; // 线段的两个点
	CPoint m_ptTo;

public:
	CLine(); // 必须要有无参构造函数,并行化时MFC底层会调用
	CLine(CPoint from, CPoint to) { m_ptFrom = from; m_ptTo = to; }
	void Serialize(CArchive& ar); // 必须实现串行化协议
};

void CLine::Serialize(CArchive& ar)
{
	CObject::Serialize(ar); // 必须要调用直接基类的Serialize函数以完成对基类部分数据的串行化!!!
        // 接下来再进行派生类部分数据的串行化
	if (ar.IsStoring()) // 判断是否是写磁盘
	{ // 写,串行化
		ar << m_ptFrom << m_ptTo;
	}
	else // 读,并行化
	{
		ar >> m_ptFrom >> m_ptTo;
	}
} // 实现之后就可以直接ar >> clineObj或ar << clineObj进行串行和并行化了

4. CArchive会引发的异常:

     1) 主要有三种异常:文件异常、内存异常和串行化异常;

     2) 文件异常:CFileException,以为串并行涉及到文件I/O,因此当然会存在文件异常;

     3) 内存异常:CMemoryException,串并行需要先在内存中开辟缓冲区以供向磁盘冲洗数据,因此内存异常主要反映在该缓冲区上,有时缓冲区容量可能会不足从而无法建立串行化或并行化对象;

     4) 串行化异常:CArchiveException,主要发生在版本控制方面,这个下一小节会详细讲述;

     5) 所有的CArchive成员函数都会抛出这样的异常!!!因此要注意异常处理;


5. CArchive的版本控制——应对软件升级和向下兼容问题:

    1) 问题背景:设想软件升级了,类中又增加了新的数据成员,这些数据成员当然也需要写入磁盘文档,因此非常理所当然地修改了Serialize函数,增加了这些新成员的串行化,那么问题就来!关键是以前已经用旧版本的Serialize向磁盘中写入数据过了,现在那些就的磁盘文档已经和新版本的数据格式不符合了,因此无法用新的串行化函数来读取它们,那该怎么办呢?难道那些文档干脆不要,删除掉、浪费掉吗?这显然不可取,不仅浪费资源而且会损失客户,导致严重的经济损失,因此需要对CArchive类加入版本控制的功能以兼容过去旧版本文档的并行化,即磁盘读取;

    2) MFC是这样解决串行化版本控制的:

         i. 刚开始构建串行化类时只有最初的一个版本,因此对该版本标号为1,并且在写磁盘的时候也将该版本号写入以进行标识;

         ii. 后来类又更新了,增加的新成员也需要串行化,所以要修改串行化函数,由于磁盘中存在标号为1的旧版本的文档,因此这个新版本就标号为2(依次递增1),写磁盘的时候就会将版本号2也记录在磁盘里面以作标识,但是从磁盘读取文档的时候会读取版本号这个重要数据,并在Serialize函数用GetObjectSchema函数获取该版本号并进行判断,如果是1则用老的代码读取,如果是2则用新的代码读取;

         iii. 如果有更新了,则新的版本号增加到3,读取时就要判断是这三个版本号中的哪个,再用相应的代码读取;

!!这就很好解决了向下兼容的问题;

    3) 以上的这种模式就是CArchive的可视化版本控制模式,接下来就要详细讲解3.中构建串行化类的第5步,具体如何实现版本控制了:

         i. 3.中其实还缺少一步,那就是要在类实现过程中添加一个宏来声明当前存在的版本数,即IMPLEMENT_SERIAL宏;

         ii. 该宏具有三个参数:IMPLEMENT_SERIAL(类名, 直接基类名, 当前版本号以及附加属性),例如

IMPLEMENT_SERIAL(CLine, CObject, 2 | VERSIONABLE_SCHEMA)

!其中版本号2有两层含义,第一层是当前的Serialize的版本号是2,第二层是磁盘中还存在着版本号为1的文档,因此需要在Serialize中提供对这类文档读取的支持;

!组合参数VERTIONABLE_SCHEMA是必加的参数,就表示采用可视化版本支持,这里其实就是版本模式的意思;

    4) 示例:

class CLine: public CObject 
{
	DECLARE_SERIAL(CLine) 
		
protected:
	CPoint m_ptFrom;
	CPoint m_ptTo;

	COLORREF m_clrLine; // 多了一个线段的颜色属性
	
public:
	CLine(); 
	CLine(CPoint from, CPoint to) { m_ptFrom = from; m_ptTo = to; }
	void Serialize(CArchive& ar);
};
// 实现中需要IMPLEMENT_SERIAL(CLine, CObject, 2 | VERSIONABLE_SCHEMA)
void CLine::Serialize(CArchive& ar)
{
	CObject::Serialize(ar);
	
	if (ar.IsStoring()) 
	{ // 串行化
		ar << m_ptFrom << m_ptTo << m_clrLine; // 串行化多一个颜色
	}
	else // 只有并行化需要版本控制!由于已经有了新的版本,老的版本不会再写了
	{
		UINT nSchema = ar.GetObjectSchema(); // 获取读取的版本号
		switch (nSchema)
		{
		case 1: // 版本1
			ar >> m_ptFrom >> m_ptTo;
			m_clrLine = RGB(0, 0, 0); // 原来文档中没有的颜色数据默认为白色
			break;
		case 2:
			ar >> m_ptFrom >> m_ptTo >> m_clrLine;
			break;
		default: // 不可能出现别的版本号,以防万一手动抛出异常(也有可能读了错误类型的文件)
			AfxThrowArchiveException(CArchiveException::badSchema); // 框架函数,参数是异常类型
			break;
		}
	}
} // 实现之后就可以直接ar >> clineObj或ar << clineObj进行串行和并行化了
!函数说明:

    a. 判断是串行还是并行:

BOOL CArchive::IsStoring() const;

BOOL CArchive::IsLoading() const;

    b. 在并行化是获取对象数据的版本号:

UINT CArchive::GetObjectSchema();

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:304244次
    • 积分:5736
    • 等级:
    • 排名:第4685名
    • 原创:149篇
    • 转载:0篇
    • 译文:128篇
    • 评论:29条
    文章分类
    最新评论