第三讲 CObject类
CObject是大多数MFC类的根类或基类。CObject类有很多有用的特性:对运行时类信息的支持,对动态创建的支持,对串行化的支持,对象诊断输出,等等。MFC从CObject派生出许多类,具备其中的一个或者多个特性。程序员也可以从CObject类派生出自己的类,利用CObject类的这些特性。
本章将讨论MFC如何设计CObject类的这些特性。首先,考察CObject类的定义,分析其结构和方法(成员变量和成员函数)对CObject特性的支持。然后,讨论CObject特性及其实现机制。
- CObject的结构
以下是CObject类的定义:
class CObject
{
public:
//与动态创建相关的函数
virtual CRuntimeClass* GetRuntimeClass() const;
析构函数
virtual ~CObject(); // virtual destructors are necessary
//与构造函数相关的内存分配函数,可以用于DEBUG下输出诊断信息
void* PASCAL operator new(size_t nSize);
void* PASCAL operator new(size_t, void* p);
void PASCAL operator delete(void* p);
#if defined(_DEBUG) && !defined(_AFX_NO_DEBUG_CRT)
void* PASCAL operator new(size_t nSize, LPCSTR lpszFileName, int nLine);
#endif
//缺省情况下,复制构造函数和赋值构造函数是不可用的
//如果程序员通过传值或者赋值来传递对象,将得到一个编译错误
protected:
//缺省构造函数
CObject();
private:
//复制构造函数,私有
CObject(const CObject& objectSrc); // no implementation
//赋值构造函数,私有
void operator=(const CObject& objectSrc); // no implementation
// Attributes
public:
//与运行时类信息、串行化相关的函数
BOOL IsSerializable() const;
BOOL IsKindOf(const CRuntimeClass* pClass) const;
// Overridables
virtual void Serialize(CArchive& ar);
// 诊断函数
virtual void AssertValid() const;
virtual void Dump(CDumpContext& dc) const;
// Implementation
public:
//与动态创建对象相关的函数
static const AFX_DATA CRuntimeClass classCObject;
#ifdef _AFXDLL
static CRuntimeClass* PASCAL _GetBaseClass();
#endif
};
由上可以看出,CObject定义了一个CRuntimeClass类型的静态成员变量:
CRuntimeClass classCObject
还定义了几组函数:
构造函数析构函数类,
诊断函数,
与运行时类信息相关的函数,
与串行化相关的函数。
其中,一个静态函数:_GetBaseClass;五个虚拟函数:析构函数、GetRuntimeClass、Serialize、AssertValid、Dump。这些虚拟函数,在CObject的派生类中应该有更具体的实现。必要的话,派生类实现它们时可能要求先调用基类的实现,例如Serialize和Dump就要求这样。
静态成员变量classCObject和相关函数实现了对CObjet特性的支持。
- CObject类的特性
下面,对三种特性分别描述,并说明程序员在派生类中支持这些特性的方法。
- 对运行时类信息的支持
该特性用于在运行时确定一个对象是否属于一特定类(是该类的实例),或者从一个特定类派生来的。CObject提供IsKindOf函数来实现这个功能。
从CObject派生的类要具有这样的特性,需要:
- 定义该类时,在类说明中使用DECLARE_DYNAMIC(CLASSNMAE)宏;
- 在类的实现文件中使用IMPLEMENT_DYNAMIC(CLASSNAME,BASECLASS)宏。
- 对动态创建的支持
前面提到了动态创建的概念,就是运行时创建指定类的实例。在MFC中大量使用,如前所述框架窗口对象、视对象,还有文档对象都需要由文档模板类(CDocTemplate)对象来动态的创建。
从CObject派生的类要具有动态创建的功能,需要:
- 定义该类时,在类说明中使用DECLARE_DYNCREATE(CLASSNMAE)宏;
- 定义一个不带参数的构造函数(默认构造函数);
- 在类的实现文件中使用IMPLEMENT_DYNCREATE(CLASSNAME,BASECLASS)宏;
- 使用时先通过宏RUNTIME_CLASS得到类的RunTime信息,然后使用CRuntimeClass的成员函数CreateObject创建一个该类的实例。
例如:
CRuntimeClass* pRuntimeClass = RUNTIME_CLASS(CNname)
//CName必须有一个缺省构造函数
CObject* pObject = pRuntimeClass->CreateObject();
//用IsKindOf检测是否是CName类的实例
Assert( pObject->IsKindOf(RUNTIME_CLASS(CName));
- 对序列化的支持
“序列化”就是把对象内容存入一个文件或从一个文件中读取对象内容的过程。从CObject派生的类要具有序列化的功能,需要:
- 定义该类时,在类说明中使用DECLARE_SERIAL(CLASSNMAE)宏;
- 定义一个不带参数的构造函数(默认构造函数);
- 在类的实现文件中使用IMPLEMENT_SERIAL(CLASSNAME,BASECLASS)宏;
- 覆盖Serialize成员函数。(如果直接调用Serialize函数进行序列化读写,可以省略前面三步。)
对运行时类信息的支持、动态创建的支持、串行化的支持层(不包括直接调用Serailize实现序列化),这三种功能的层次依次升高。如果对后面的功能支持,必定对前面的功能支持。支持动态创建的话,必定支持运行时类信息;支持序列化,必定支持前面的两个功能,因为它们的声明和实现都是后者包含前者。
- 综合示例:
定义一个支持串行化的类CPerson:
class CPerson : public CObject
{
public:
DECLARE_SERIAL( CPerson )
// 缺省构造函数
CPerson(){}{};
CString m_name;
WORD m_number;
void Serialize( CArchive& archive );
// rest of class declaration
};
实现该类的成员函数Serialize,覆盖CObject的该函数:
void CPerson::Serialize( CArchive& archive )
{
// 先调用基类函数的实现
CObject::Serialize( archive );
// now do the stuff for our specific class
if( archive.IsStoring() )
archive << m_name << m_number;
else
archive >> m_name >> m_number;
}
使用运行时类信息:
CPerson a;
ASSERT( a.IsKindOf( RUNTIME_CLASS( CPerson ) ) );
ASSERT( a.IsKindOf( RUNTIME_CLASS( CObject ) ) );
动态创建:
CRuntimeClass* pRuntimeClass = RUNTIME_CLASS(CPerson)
//Cperson有一个缺省构造函数
CObject* pObject = pRuntimeClass->CreateObject();
Assert( pObject->IsKindOf(RUNTIME_CLASS(CPerson));
MFC提供了DECLARE_DYNAMIC、DECLARE_DYNCREATE、DECLARE_SERIAL声明宏的两种定义,分别用于静态链接到MFC DLL和动态链接到MFC DLL。对应的实现宏IMPLEMNET_XXXX也有两种定义,但是,这里实现宏就不列举了。
MFC对这些宏的定义如下:
#ifdef _AFXDLL //动态链接到MFC DLL
#define DECLARE_DYNAMIC(class_name) \
protected: \
static CRuntimeClass* PASCAL _GetBaseClass(); \
public: \
static const AFX_DATA CRuntimeClass class##class_name; \
virtual CRuntimeClass* GetRuntimeClass() const; \
#define _DECLARE_DYNAMIC(class_name) \
protected: \
static CRuntimeClass* PASCAL _GetBaseClass(); \
public: \
static AFX_DATA CRuntimeClass class##class_name; \
virtual CRuntimeClass* GetRuntimeClass() const; \
#else
#define DECLARE_DYNAMIC(class_name) \
public: \
static const AFX_DATA CRuntimeClass class##class_name; \
virtual CRuntimeClass* GetRuntimeClass() const; \
#define _DECLARE_DYNAMIC(class_name) \
public: \
static AFX_DATA CRuntimeClass class##class_name; \
virtual CRuntimeClass* GetRuntimeClass() const; \
#endif
// not serializable, but dynamically constructable
#define DECLARE_DYNCREATE(class_name) \
DECLARE_DYNAMIC(class_name) \
static CObject* PASCAL CreateObject();
#define DECLARE_SERIAL(class_name) \
_DECLARE_DYNCREATE(class_name) \
friend CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb);
由于这些声明宏都是在CObect派生类的定义中被使用的,所以从这些宏的上述定义中可以看出,DECLARE_DYNAMIC宏给所在类添加了一个CRuntimeClass类型的静态数据成员class##class_name(类名加前缀class,例如,若类名是CPerson,则该变量名称是classCPerson),且指定为const;两个(使用MFC DLL时,否则,一个)成员函数:虚拟函数GetRuntimeClass和静态函数_GetBaseClass(使用MFC DLL时)。
DECLARE_DYNCREATE宏包含了DECLARE_DYNAMIC,在此基础上,还定义了一个静态成员函数CreateObject。
DECLARE_SERIAL宏则包含了_DECLARE_DYNCREATE,并重载了操作符“>>”(友员函数)。它和前两个宏有所不同的是CRuntimeClass数据成员class##class_name没有被指定为const。
对应地,MFC使用三个宏初始化DECLARE宏所定义的静态变量并实现DECLARE宏所声明的函数:IMPLEMNET_DYNAMIC,IMPLEMNET_DYNCREATE,IMPLEMENT_SERIAL。
首先,这三个宏初始化CRuntimeClass类型的静态成员变量class#class_name。IMPLEMENT_SERIAL不同于其他两个宏,没有指定该变量为const。初始化内容在下节讨论CRuntimeClass时给出。
其次,它实现了DECLARE宏声明的成员函数:
- _GetBaseClass()
返回基类的运行时类信息,即基类的CRuntimeClass类型的静态成员变量。这是静态成员函数。
- GetRuntimeClass()
返回类自己的运行类信息,即其CRuntimeClass类型的静态成员变量。这是虚拟成员函数。
对于动态创建宏,还有一个静态成员函数CreateObject,它使用C++操作符和类的缺省构造函数创建本类的一个动态对象。
- 操作符的重载
对于序列化的实现宏IMPLEMENT_SERIAL,还重载了操作符<<和定义了一个静态成员变量
static const AFX_CLASSINIT _init_##class_name(RUNTIME_CLASS(class_name));
比如,对CPerson来说,该变量是_init_Cperson,其目的在于静态成员在应用程序启动之前被初始化,使得AFX_CLASSINIT类的构造函数被调用,从而通过AFX_CLASSINIT类的构造函数在模块状态的CRuntimeClass链表中插入构造函数参数表示的CRuntimeClass类信息。至于模块状态,在后文有详细的讨论。
重载的操作符函数用来在序列化时从文档中读入该类对象的内容,是一个友员函数。定义如下:
CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb)
{
pOb = (class_name*) ar.ReadObject(
RUNTIME_CLASS(class_name));
return ar;
}
回顾CObject的定义,它也有一个CRuntimeClass类型的静态成员变量classCObject,因为它本身也支持三个特性。
以CObject及其派生类的静态成员变量classCObject为基础,IsKindOf和动态创建等函数才可以起到作用。
这个变量为什么能有这样的用处,这就要分析CRuntimeClass类型变量的结构和内容了。下面,在讨论了CRuntimeClass的结构之后,考察该类型的静态变量被不同的宏初始化之后的内容。
从上面的讨论可以看出,在对CObject特性的支持上,CRuntimeClass类起到了关键作用。下面,考查它的结构和功能。
- CRuntimeClass的结构
CruntimeClass的结构如下:
Struct CRuntimeClass
{
LPCSTR m_lpszClassName;//类的名字
int m_nObjectSize;//类的大小
UINT m_wSchema;
CObject* (PASCAL* m_pfnCreateObject)();
//pointer to function, equal to newclass.CreateObject()
//after IMPLEMENT
CRuntimeClass* (PASCAL* m_pfnGetBaseClass)();
CRumtieClass* m_pBaseClass;
//operator:
CObject *CreateObject();
BOOL IsDerivedFrom(const CRuntimeClass* pBaseClass) const;
...
}
CRuntimeClass成员变量中有两个是函数指针,还有几个用来保存所在CruntimeClass对象所在类的名字、类的大小(字节数)等。
这些成员变量被三个实现宏初始化,例如:
m_pfnCreateObject,将被初始化指向所在类的静态成员函数CreateObject。CreateObject函数在初始化时由实现宏定义,见上文的说明。
m_pfnGetBaseClass,如果定义了_AFXDLL,则该变量将被初始化指向所在类的成员函数_GetBaseClass。_GetBaseClass在声明宏中声明,在初始化时由实现宏定义,见上文的说明。
下面,分析三个宏对CObject及其派生类的CRuntimeClass类型的成员变量class##class_name初始化的情况,然后讨论CRuntimeClass成员函数的实现。
- 成员变量class##class_name的内容
IMPLEMENT_DYNCREATE等宏将初始化类的CRuntimeClass类型静态成员变量的各个域,表3-1列出了在动态类信息、动态创建、序列化这三个不同层次下对该静态成员变量的初始化情况:
表3-1 静态成员变量class##class_name的初始化
CRuntimeClass成员变量
动态类信息
动态创建
序列化
m_lpszClassName
类名字符串
类名字符串
类名字符串
m_nObjectSize
类的大小(字节数)
类的大小(字节数)
类的大小(字节数)
m_wShema
0xFFFF
0xFFFF
1、2等,非0
m_pfnCreateObject
NULL
类的成员函数
CreateObject
类的成员函数
CreateObject
m_pBaseClass
基类的CRuntimeClass变量
基类的CRuntimeClass变量
基类的CRuntimeClass变量
m_pfnGetBaseClass
类的成员函数
_GetBaseClass
类的成员函数
_GetBaseClass
类的成员函数
_GetBaseClass
m_pNextClass
NULL
NULL
NULL
m_wSchema类型是UINT,定义了序列化中保存对象到文档的程序的版本。如果不要求支持序列化特性,该域为0XFFFF,否则,不能为0。
Cobject类本身的静态成员变量classCObject被初始化为:
{ "CObject", sizeof(CObject), 0xffff, NULL, &CObject::_GetBaseClass, NULL };
对初始化内容解释如下:
类名字符串是“CObject”,类的大小是sizeof(CObject),不要求支持序列化,不支持动态创建。
- 成员函数CreateObject
回顾3.2节,动态创建对象是通过语句pRuntimeClass->CreateObject完成的,即调用了CRuntimeClass自己的成员函数,CreateObject函数又调用m_pfnCreateObject指向的函数来完成动态创建任务,如下所示:
CObject* CRuntimeClass::CreateObject()
{
if (m_pfnCreateObject == NULL) //判断函数指针是否空
{
TRACE(_T("Error: Trying to create object which is not ")
_T("DECLARE_DYNCREATE \nor DECLARE_SERIAL: %hs.\n"),
m_lpszClassName);
return NULL;
}
//函数指针非空,继续处理
CObject* pObject = NULL;
TRY
{
pObject = (*m_pfnCreateObject)(); //动态创建对象
}
END_TRY
return pObject;
}
- 成员函数IsDerivedFrom
该函数用来帮助运行时判定一个类是否派生于另一个类,被CObject的成员函数IsKindOf函数所调用。其实现描述如下:
如果定义了_AFXDLL则,成员函数IsDerivedFrom调用成员函数m_pfnGetBaseClass指向的函数来向上逐层得到基类的CRuntimeClass类型的静态成员变量,直到某个基类的CRuntimeClass类型的静态成员变量和参数指定的CRuntimeClass变量一致或者追寻到最上层为止。
如果没有定义_AFXDLL,则使用成员变量m_pBaseClass基类的CRuntimeClass类型的静态成员变量。
程序如下所示:
BOOL CRuntimeClass::IsDerivedFrom(
const CRuntimeClass* pBaseClass) const
{
ASSERT(this != NULL);
ASSERT(AfxIsValidAddress(this, sizeof(CRuntimeClass), FALSE));
ASSERT(pBaseClass != NULL);
ASSERT(AfxIsValidAddress(pBaseClass, sizeof(CRuntimeClass), FALSE));
// simple SI case
const CRuntimeClass* pClassThis = this;
while (pClassThis != NULL)//从本类开始向上逐个基类搜索
{
if (pClassThis == pBaseClass)//若是参数指定的类信息
return TRUE;
//类信息不符合,继续向基类搜索
#ifdef _AFXDLL
pClassThis = (*pClassThis->m_pfnGetBaseClass)();
#else
pClassThis = pClassThis->m_pBaseClass;
#endif
}
return FALSE; // 搜索完毕,没有匹配,返回FALSE。
}
由于CRuntimeClass类型的成员变量是静态成员变量,所以如果两个类的CruntimeClass成员变量相同,必定是同一个类。这就是IsDerivedFrom和IsKindOf的实现基础。
- RUNTIME_CLASS宏
RUNTIME_CLASS宏定义如下:
#define RUNTIME_CLASS(class_name) (&class_name::class##class_name)
为了方便地得到每个类(Cobject或其派生类)的CRuntimeClass类型的静态成员变量,MFC定义了这个宏。它返回对类class_name的CRuntimeClass类型成员变量的引用,该成员变量的名称是“class”加上class_name(类的名字)。例如:
RUNTIME_CLASS(CObject)得到对classCObject的引用;
RUNTIME_CLASS(CPerson)得到对class CPerson的引用。
MFC对Cobject动态类信息、动态创建的实现原理:
动态类信息、动态创建都建立在给类添加的CRuntimeClass类型的静态成员变量基础上,总结如下。
C++不支持动态创建,但是支持动态对象的创建。动态创建归根到底是创建动态对象,因为从一个类名创建一个该类的实例最终是创建一个以该类为类型的动态对象。其中的关键是从一个类名可以得到创建其动态对象的代码。
在一个类没有任何实例之前,怎么可以得到该类的创建动态对象的代码?借助于C++的静态成员数据技术可达到这个目的:
- 静态成员数据在程序的入口(main或WinMain)之前初始化。因此,在一个静态成员数据里存放有关类型信息、动态创建函数等,需要的时候,得到这个成员数据就可以了。
- 不论一个类创建多少实例,静态成员数据只有一份。所有的类的实例共享一个静态成员数据,要判断一个类是否是一个类的实例,只须确认它是否使用了该类的这个静态数据。
从前两节的讨论知道,DECLARE_CREATE等宏定义了一个这样的静态成员变量:类型是CRuntimeClass,命名约定是“calss”加上类名;IMPLEMENT_CREATE等宏初始化该变量;RUNTIME_CLASS宏用来得到该成员变量。
动态类信息的原理在分析CRuntimeClass的成员函数IsDerivedFrom时已经作了解释。
动态创建的过程和原理了,用图表示其过程如下:
注:下面一个方框内的逐级缩进表示逐层调用关系。
由上所述可知,一个类要支持实现序列化,使得它的对象可以保存到文档中或者可以从文档中读入到内存中并生成对象,需要使用动态类信息,而且,需要覆盖基类的Serialize虚拟函数来完成其对象的序列化。
仅仅有类的支持是不够的,MFC还提供了一个归档类CArchive来支持简单类型的数据和复杂对象的读写。
CArchive 在文件和内存对象之间充当一个代理者的角色。它负责按一定的顺序和格式把内存对象写到文件中,或者读出来,可以被看作是一个二进制的流。它和文件类CFile的关系如图3-2所示:
一个CArchive对象在要序列化的对象和存储媒体(storage medium,可以是一个文件或者一个Socket)之间起了中介作用。它提供了系列方法来完成序列化,不仅能够把int、float等简单类型数据进行序列化,而且能够把复杂的数据如string等进行序列化,更重要的是它能把复杂的对象(包括复合对象)进行序列化。这些方法就是重载的操作符>>和<<。对于简单类型,它针对不同类型直接实现不同的读写操作;对于复杂的对象,其每一个支持序列化的类都重载了操作符>>,从前几节可以清楚地看到这点:IMPLEMENT_SERIAL给所在类重载了操作符>>。至于<<操作,就不必每个序列化类都重载了。
复杂对象的“<<”操作,先搜索本模块状态的CRuntimeClass链表看是否有“<<”第二个参数指定的对象类的运行类信息(搜索过程涉及到模块状态,将在9.5.2节描述),如果有(无,则返回),则先使用这些信息动态的创建对象(这就是是序列化类必须提供动态类信息、支持动态创建的原因),然后对该对象调用Serilize函数从存储媒体读入对象内容。
复杂对象的“>>”操作先把对象类的运行类信息写入存储媒体,然后对该对象调用Serilize函数把对象内容写入存储媒体。
在创建CArchive对象时,必须有一个CFile对象,它代表了存储媒介。通常,程序员不必做这个工作,打开或保存文档时MFC将自动的创建CFile对象和CArchive对象并在适当的时候调用序列化类的Serialize函数。在后面讨论打开(5.3.3.2节)或者关闭(6.1节)文档时将会看到这样的流程。
CArchive对象被创建时,需要指定它是用来读还是用来写,即指定序列化操作的方向。Serialize函数适用CArchive的函数IsStoring判定CArchive是用于读出数据还是写入数据。
在解释实现序列化的方法时,曾经提到如果程序员直接调用Serilize函数完成序列化,而不借助CArchive的>>和<<操作,则可以不需要动态类信息和动态创建。从上文的论述可以看出,没有CArchive的>>和<<操作,的确不需要动态类信息和动态创建特性。
返回页首 |