CObject是“MFC类之母”,由它派生出庞大的类体系。CObject并不是对整个类体系进行语义抽象的结果,它只为所有派生类定义几种功能特性。由于这几项功能应用于MFC的大部分类中,成为MFC的普遍现象,有必要认真学习。下面就着重讨论这几项功能特性。
5.1.1 支持类诊断
CObject类定义了这样一个虚拟函数:
public:
virtual void CObject::AssertValid() const
{
ASSERT(this != NULL);
}
派生类可以对它进行重载,通过当前对象的状态,诊断它的有效性。诊断条件根据需要而定。一般通过ASSERT宏进行诊断,这样当调试出错时,程序会在失败的ASSERT所在代码行中断,便于调试。但在程序的发布版中,该诊断无效。
在派生类的重载版本中,出于习惯,一般要首先调用基类的版本对this指针进行检查。例如:
class CDate :public CObject
{
public:
CDate(unsigned int year,unsigned int month,unsigned int day)
{
m_Year=year;m_Month=month;m_Day=day;
MonthDays[0]=31;
MonthDays[1]=29;
MonthDays[2]=31;
MonthDays[3]=30;
MonthDays[4]=31;
MonthDays[5]=30;
MonthDays[6]=31;
MonthDays[7]=31;
MonthDays[8]=30;
MonthDays[9]=31;
MonthDays[10]=30;
MonthDays[11]=31;
}
void AssertValid() const
{
CObject::AssertValid();
ASSERT(m_Year>0&&m_Month>0&&m_Month<=12&&m_Day>0);
ASSERT((m_Month==2)?
((m_Year%100 && !(m_Year%4)|| !(m_Year%400))?(m_Day<=29):(m_Day<=28))
:(m_Day<=MonthDays[m_Month-1]));
}
unsigned int GetYear()const;
unsigned int GetMonth()const;
unsigned int GetDay()const;
inline void SetDate(unsigned int year,unsigned int month,unsigned int day);
private:
unsigned int m_Year,m_Month,m_Day;
unsigned char MonthDays[12];
};
void gFun()
{
CDate date(2000,2,29);
//校验date对象的有效性
date.AssertValid();
}
应该注意到,AssertValid()虚函数以const关键字修饰,所以该函数不能直接或间接改变类成员的状态。
同时,为了在调试时输出类实例的相关信息,CObject类又实现了这样一个虚函数:
public:
virtual void CObject::Dump(CDumpContext& dc) const
{
dc << "a " << GetRuntimeClass()->m_lpszClassName <<
" at " << (void*)this << "\n";
UNUSED(dc); //该语句无实际操作,意在标识发布版中这段代码无效
}
在说明该函数的作用之前,首先复习VC++的调试输出功能。VC++的强大调试功能是一个亮点,在调试过程中可以随时调用TRACE语句将诊断信息输出到调试窗口,避免了使用MessageBox()带来的麻烦。而这一切在发布版中会自动清除。
TRACE类似于C语言的printf语句,适合一般的数据类型输出,而在C++环境下,应该提供类实例的诊断输出工具。是的,MFC为程序员定义了这样的工具:CDumpContext 类,并定义了该类的一个实例afxDump。程序员可以直接使用afxDump对象,CDumpContext 类重载了各种数据类型的插入运算符“<<”,包括CObject引用和指针,使用方法就同C++的标准输出函数cout一样。CDumpContext也将信息输出到调试窗口,也只能在Debug版本中有效,如果以CObject或其派生类的对象作参数,输出的是对象的名称和地址。例如:
{
CDate date(2000,2,29);
date.AssertValid();
afxDump<<date<<"year="<<date.GetYear();
}
输出结果:
a CObject at $12F528
year=0x7D0
如果在类CDate的定义和实现中分别调用DECLARE_DYNAMIC宏和IMPLEMENT_ DYNAMIC宏,输出的是真实的类名称,而不是基类CObject。
那么CObject::Dump()虚函数与afxDump诊断输出有何联系呢?从该虚函数的实现代码可知,前者调用后者,输出类运行时名称和对象地址。深入CDumpContext的类代码又会发现,重载的CObject对象插入运算符又调用了实参的虚函数Dump()。所以,只要在CDate类中重载虚函数Dump(),输出必要的诊断信息,而后执行语句
afxDump<<date;
即可输出全部诊断信息,因为该语句调用了重载的Dump()虚函数。示例5.1是上例的改进代码:
示例清单5.1
class CDate :public CObject
{
DECLARE_DYNAMIC(CDate)
public:
CDate(unsigned int year,unsigned int month,unsigned int day);
void AssertValid() const;
#ifdef _DEBUG
void Dump(CDumpContext& dc) const
{
dc<<"virtual function Dump in CDate called\n";
//调用基类的虚函数输出类名和地址
CObject::Dump(dc);
dc<<"year="<<GetYear()<<"\n";
dc<<"month="<<GetMonth()<<"\n";
dc<<"day="<<GetDay()<<"\n";
}
#endif
unsigned int GetYear()const{return m_Year;}
unsigned int GetMonth()const{return m_Month;}
unsigned int GetDay()const{return m_Day;}
void SetDate(unsigned int year,unsigned int month,unsigned int day);
private:
unsigned int m_Year,m_Month,m_Day;
unsigned char MonthDays[12];
};
IMPLEMENT_DYNAMIC(CDate,CObject)
void gFun()
{
CDate date(2000,2,29);
date.AssertValid();
afxDump<<date;
}
函数gFun()输出:
virtual function Dump in CDate called
a CDate at $12F528
year=0x7D0
month=0x2
day=0x1D
重载Dump()还要注意两点:一是要将函数体用#ifdef _DEBUG/#endif括起,二是不要直接或间接改变对象的状态。
5.1.2 提供运行时类信息
类的运行时信息包括类名称、尺寸,以及不知道类名称而创建类实例的能力。如果需要随时访问CObject派生类对象的类名,通过定义虚函数很容易实现。例如:
class CObject
{
public:
virtual char * GetClassName()const
{return m_ClassName;}
static char m_ClassName[];
......
};
char CObject::m_ClassName[]="CObject";
class CDate:public CObject
{
public:
virtual char * GetClassName()const
{return m_ClassName;}
static char m_ClassName[];
};
char CDate::m_ClassName[]="CDate";
由此可知,只要每个派生类都定义一个静态字符串储存类名,并重载虚函数GetClass Name()返回该串即可。
对于类尺寸通过虚函数或静态成员也可以提供。那么,不知道类名称而创建类的实例,如何实现呢?我们知道,类的静态成员与类的实例无关,只要将类的创建信息存储在静态成员中,调用类的静态成员就可以创建类的实例了。见下面的示例5.2。
示例清单5.2
#include "stdio.h"
class CObject
{
public:
virtual void Declare()const
{
printf("I am CObject\n");
}
};
//定义函数指针
typedef CObject* (*FUN_CreateObject)();
class CDate:public CObject
{
public:
CDate(){}
virtual void Declare()const
{
printf("I am CDate\n");
}
//定义静态函数,用于创建该类的实例
static CObject* CreateObject()
{ return new CDate(); }
static FUN_CreateObject pfunCreateObject;
};
//静态的函数指针指向创建类实例的静态函数
FUN_CreateObject CDate::pfunCreateObject=CDate::CreateObject;
void gCreateHelper(FUN_CreateObject pfunCreateObject)
{
//不知道类名称,间接创建该类
if(NULL==pfunCreateObject)
{
printf("create object failed\n");
return;
}
CObject*pObject=(CObject*)pfunCreateObject();
if(NULL!=pObject)
{
//调用虚函数,验证所创建的类
pObject->Declare();
delete pObject;
}
}
int main(int argc, char* argv[])
{
gCreateHelper(CDate::pfunCreateObject);
return 0;
}
程序输出:
I am CDate
以上,我们撇开MFC自行解决了运行时类信息问题。下面学习MFC是如何实现的。
为了简化代码,MFC使用了一系列宏定义,提供运行时类信息。在上文对类诊断的讨论中,已经使用过DECLARE_DYNAMIC宏和IMPLEMENT_DYNAMIC宏,输出了类名称。这两个宏的作用是,在类中插入一个静态的CRuntimeClass结构型成员。该结构的定义简写如下:
struct CRuntimeClass
{
LPCSTR m_lpszClassName;
int m_nObjectSize;
CObject* (PASCAL* m_pfnCreateObject)();
CObject* CreateObject();
BOOL IsDerivedFrom(const CRuntimeClass* pBaseClass) const;
};
同时定义了一个虚函数用于返回这个静态CRuntimeClass结构型成员的指针,如下:
virtual CRuntimeClass* GetRuntimeClass() const;
并由IMPLEMENT_DYNAMIC宏将类名称存储在该结构成员的m_lpszClassName成员中,类尺寸存储在m_nObjectSize成员中,所以只要调用对象的
GetRuntimeClass()->m_lpszClassName;
就得到了对象的类名称等运行时信息。被插入的CRuntimeClass型静态成员被命名为class##class_name,即如果类名为CDynaClass,则该成员名为classCDynaClass。除使用虚拟函数GetRuntimeClass(),在知道类名的前提下,还可以使用RUNTIME_CLASS(class_name)宏取得这个CRuntimeClass指针。例如:
ASSERT(RUNTIME_CLASS(CDynaClass)->m_lpszClassName=="CDynaClass");
DECLARE_DYNAMIC宏定义简写如下:
#define DECLARE_DYNAMIC(class_name) \
public: \
static const AFX_DATA CRuntimeClass class##class_name; \
virtual CRuntimeClass* GetRuntimeClass() const; \
对于动态创建(即不知道类名而创建该类),MFC也提供了相应的宏定义,即DECLARE_DRNCREATE和IMPLEMENT_DRNCREATE(或DECLARE_SERIAL和IMPLEMENT_SERIAL)。该宏除包含#######_DYNAMIC宏的所有功能外,还在类中插入一个静态成员函数CreateObject(),其功能就如同示例5.2中所定义的一样。观察Cruntime Class结构可知,该结构包含一个函数指针型成员m_pfnCreateObject,其类型也同示例5.2中的CDate::pfunCreateObject相似。实际上IMPLEMENT_DRNCREATE宏正是将插入的静态成员函数CreateObject(),赋给插入的静态CRuntimeClass实例的m_pfnCreateObject指针。这样,只要取得这个CRuntimeClass指针,即使不知道类名,也可以创建该类的实例了。
应该注意到,CRuntimeClass结构也包含一个CreateObject()成员函数,该函数直接调用m_pfnCreateObject所指向的函数。例如:
CObject* CRuntimeClass::CreateObject()
{
if (m_pfnCreateObject == NULL)
{ return NULL;
}
CObject* pObject = NULL;
TRY
{ //创建类实例
pObject = (*m_pfnCreateObject)();
}
END_TRY
return pObject;
}
所以,在实际编程中往往调用CRuntimeClass::CreateObject()创建对象,而不是Cruntime Class::m_pfnCreateObject()。例如:
CRuntimeClass *pRunInfo=RUNTIME_CLASS(CDynaClass);
CObject *pObj=(CObject*)pRunInfo->CreateObject();
这种所谓的动态创建特性,进一步提高了类的抽象程度,可以利用它编写较为通用的代码,例如示例5.2中的全局函数gCreateHelper()。实现动态创建特性的类需要有无参的构造函数。
MFC的文档模板类CDocTemplate正是运用动态创建特性实现的通用代码,它可以创建出各种文档/视图结构。对下面的语句你肯定非常熟悉。
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
//将下面3个动态类的CRuntimeClass静态成员指针作为构造参数
RUNTIME_CLASS(CMyDoc),
RUNTIME_CLASS(CMainFrame),
RUNTIME_CLASS(CMyView));
虽然运行时类信息是通过MFC宏提供的,但离不开CObject类的支持。相关函数的参数都是以基类CObject为基础的,CObject除定义了相关的静态成员和虚函数,还重载了new和delete运算符支持动态创建。
5.1.3 支持类的连载
类的连载就是能够将对象的当前状态保存在磁盘文件上(或以其他方式保存),同时也能够将保存在文件中的对象信息加载到对象内存中,恢复对象的状态。
CObject支持的连载合成了CArchive类,如同诊断输出中使用的CDumpContext类。该类重载了“<<”和“>>”运算符,分别用于存储对象状态和恢复对象状态。该类的存储和读取文件的功能是通过合成CFile类对象实现的。下面是其构造函数:
CArchive(CFile* pFile, UINT nMode, int nBufSize = 4096, void* lpBuf = NULL);
参数1是文件指针,除磁盘文件外,也可以打开命名管道或端口设备,或者通过CSocketFile进行网络传输。首先打开该文件,然后调用CArchive构造函数;在完成存储或加载后,先关闭CArchive对象,再关闭文件。参数2是连载模式,即存储(CArchive::store)或加载(CArchive::load)。一个CArchive对象只能用于一种模式。参数3是临时缓冲的尺寸。
CObject类为连载定义了虚函数:
virtual void Serialize(CArchive& ar)
派生类可以对它重载,实现连载。下例是在示例5.1的基础上编写的连载演示。
virtual void CDate::Serialize(CArchive& ar)
{
if(ar.IsLoading())
{
ar>>m_Year>>m_Month>>m_Day;
}
else
ar<<m_Year<<m_Month<<m_Day;
}
void gStoreDate()
{
CFile fp;
CDate date(2000,2,29);
date.AssertValid();
afxDump<<date;
if(fp.Open("CDate.dat",CStdioFile::modeWrite|CFile::modeCreate))
{
CArchive ar(&fp,CArchive::store);
date.Serialize(ar);
ar.Close();
fp.Close();
}
}
void gLoadDate()
{
CFile fp;
CDate date(0,0,0);
if(fp.Open("CDate.dat",CStdioFile::modeRead))
{
CArchive ar(&fp,CArchive::load);
date.Serialize(ar);
ar.Close();
fp.Close();
}
date.AssertValid();
afxDump<<date;
}
如果gStoreDate()和gLoadDate()函数先后执行,二者的诊断输出相同。
为方便实现连载,MFC也提供了宏定义的支持,即DECLARE_SERIAL/IMPLEMENT_ SERIAL,该宏除实现连载还包括DECLARE_DRNCREATE/IMPLEMENT_DRNCREATE宏的所有功能。在CObject派生类中定义SERIAL宏、重载Serialize()虚函数、有默认构造器的3个前提下,连载该类的对象时,可以直接使用CArchive重载的“<<”和“>>”运算符操作对象。例如:
CDate date(0,0,0);
CArchive ar(&fp,CArchive::load);
Ar>>date;
其形式同
afxDump<<date;
一样。
注意,直接调用“<<”或“>>”运算符连载对象,与调用对象的Serialize()虚函数是有区别的。前者不仅连载对象的状态,还连载对象的类运行时信息。
在这种方式下,可以将磁盘文件中的类信息加载到还没有指向有效地址的对象指针中,对象的创建工作自动完成(因为该类已支持自动创建),但对象的释放要手动进行。例如:
{
CFile fp;
CDate *date=NULL;
if(fp.Open("CDate.dat",CStdioFile::modeRead))
{ CArchive ar(&fp,CArchive::load);
ar>>date;
//date->Serialize(ar);
ar.Close();
fp.Close();
}
if(date!=NULL)
{
date->AssertValid();
afxDump<<date;
delete date;
} }
但无论以何种方式连载类对象,存储和加载的方式要一致。
在文档/视图框架程序中,MFC框架(本书中的框架指MFC体系结构)会在存储文件(ID_FILE_SAVE或ID_FILE_SAVE_AS)和打开文件(ID_FILE_OPEN)时,调用文档类对象的连载操作,分别进行存储和加载。该功能无需显示定义CArchive对象,文档类也无需使用_SERIAL宏。