RTTI运行时类型识别
CRuntimeClass是MFC专用的。CRuntimeClass在文件AFX.H中声明,它是用来串起MFC从COBJECT继承下来的所有类。也可以把自己写的类加入这个链表。
struct CRuntimeClass
{
// Attributes
LPCSTR m_lpszClassName; //类名
int m_nObjectSize; //类对象大小
UINT m_wSchema; //分类编号(对不可分类的类,该值为-1)
CObject* (PASCAL* m_pfnCreateObject)(); // NULL => abstract class
CRuntimeClass* m_pBaseClass; //基类指针,但这里指针一定是指向父亲的,而不会指向祖父
// CRuntimeClass objects linked together in simple list
static CRuntimeClass* pFirstClass; // class list的链表头。注意这个与基类指针不同。并且,该对象是个静态变量,所有CRuntimeClass对象共享
CRuntimeClass* m_pNextClass; // 链表中紧跟当前对象的下一个对象。与当前对象不一定有继承关系
};
特别注意
:该
struct
使用了链表的概念,但是该链表与常规的数据结构链表是不太一样的。
该链表每次新加入的节点都是放在链表头上的
,类似栈。所以
pFirstClass
是随着每次新节点的加入一直在更新的,
pFirstClass
必然指向新加入的节点
。
而
m_pNextClass
则指向前一个加入的节点
。
如下图:
首先初始化staticCRuntimeClass* pFirstClass为NULL。
① 最开始加入链表的是CObject。此时:
在_IMPLEMENT_RUNTIMECLASS宏中先直接设置m_pBaseClass:
#m_pBaseClass= NULL;
然后在_IMPLEMENT_RUNTIMECLASS宏中调用AFX_CLASSINIT的构造函数设置m_pNextClass与pFirstClass:(这两者是先m_pNextClass后pFirstClass)
#m_pNextClass= pFirstClass =NULL;
#pFirstClass= CObject;
② 然后CCmdTarget加入链表。数据更新顺序同上:
#m_pBaseClass=CObject;
#m_pNextClass= pFirstClass= CObject;
#pFirstClass= CCmdTarget;
③ 接着CWinThread加入链表。数据更新顺序同上:
#m_pBaseClass=CCmdTarget;
#m_pNextClass= pFirstClass= CCmdTarget;
#pFirstClass= CWinThread;
每一个类都拥有这样一个static的CRuntimeClass 成员变量。由于每个类都有static的CRuntimeClass 成员变量,所以每个类的对象都是引用以及修改本类的static的CRuntimeClass 成员变量,而不会将父类或者子类的修改掉。(父类与子类中的static同名成员变量是会造成“覆盖”的,该覆盖指的是引用时若不加域名直接使用,则使用的是本类的,而不会引用父类的。但实际上在内存中父类的static同名成员变量是依然存在的)
每个static的CRuntimeClass 成员变量都有一定的命名规则(在CRuntimeClass中采用的方法是在每个类的类名之前冠以"class" 作为它的名称,如CView的名称为classCView),然后,经由某种手段将整个类别库构造好之后,「类别型录」能呈现类似这样的风貌:
注意是每个类共享一个CRuntimeClass成员变量。
例:
CCmdTargetcmd1,cmd2;
CWinThreadthread;
上面两个类定义了三个对象。其中:
① cmd1与cmd2共享一个CRuntimeClass成员变量。所以cmd1与cmd2所拥有的CRuntimeClass成员变量是同一个。
② thread单独使用一个CRuntimeClass成员变量。thread所使用的这个CRuntimeClass成员变量与cmd1、cmd2共享的那个CRuntimeClass成员变量不是同一个。
③ pFirstClass是CRuntimeClass结构体中的static变量,所以所有的对象都共享pFirstClass。因此,cmd1、cmd2、thread三者共享CRuntimeClass结构体中的static变量pFirstClass。
对于CView,其
CView.h
class CView : public CWnd
{
DECLARE_DYNAMIC(CView)
}
CView.cpp
IMPLEMENT_DYNAMIC (CsdiTestView, CView)
其中的两个宏DECLARE_DYNAMIC与IMPLEMENT_DYNAMIC展开后,代码如下:
//*****************************************************************************
#define DECLARE_DYNAMIC(class_name) \
public: \
static CRuntimeClass class##class_name; \
virtual CRuntimeClass*GetRuntimeClass() const;
#define IMPLEMENT_DYNAMIC(class_name,base_class_name) \
_IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, 0xFFFF,NULL)
*****************************************************************************//
① 出现在宏定义之中的##,用来告诉编译器,把两个字符串系在一起。如果你这么使用此宏:
DECLARE_DYNAMIC(CView)
编译器前置处理器为你做出的码是:
public:
static CRuntimeClass classCView;//静态成员变量,所有该类对象都共享该变量
virtual CRuntimeClass*GetRuntimeClass() const;
② IMPLEMENT_DYNAMIC宏之中又使用了一个_IMPLEMENT_RUNTIMECLASS宏,之所以这样做是因为_IMPLEMENT_RUNTIMECLASS宏在动态创建时还要用到。
_IMPLEMENT_RUNTIMECLASS宏定义:
//*****************************************************************************
#define _IMPLEMENT_RUNTIMECLASS(class_name,base_class_name,wSchema,pfnNew) \
static char _lpsz##class_name[] = #class_name; \
CRuntimeClass class_name::class##class_name = { \
_lpsz##class_name, sizeof(class_name),wSchema, pfnNew, \
RUNTIME_CLASS(base_class_name), NULL }; \
static AFX_CLASSINIT_init_##class_name(&class_name::class##class_name); \
CRuntimeClass* class_name::GetRuntimeClass() const \
{ return&class_name::class##class_name; } \
*****************************************************************************//
③ 其中又有RUNTIME_CLASS 宏,该宏用于取指定类的静态CRuntimeClass对象地址。由于在上面代码中调用该宏是RUNTIME_CLASS(base_class_name),NULL };所以传入的参数是base_class_name,从而获取的地址是基类(即父类)的静态CRuntimeClass对象地址。
其定义如下:
//*****************************************************************************
#define RUNTIME_CLASS(class_name) \
(&class_name::class##class_name)
*****************************************************************************//
④ 看起来整个IMPLEMENT_DYNAMIC 内容好象只是指定初值,不然,其曼妙处在于它所使用的一个structAFX_CLASSINIT,定义如下:
//*****************************************************************************
struct AFX_CLASSINIT
{ AFX_CLASSINIT(CRuntimeClass*pNewClass); };
*****************************************************************************//
这表示它有一个构造函数(别惊讶,C++ 的struct 与class 都有构造式),定义如下:
AFX_CLASSINIT::AFX_CLASSINIT(CRuntimeClass* pNewClass)
{
pNewClass->m_pNextClass = CRuntimeClass::pFirstClass;
CRuntimeClass::pFirstClass = pNewClass;
}
很明显,此构造式负责linkedlist 的串接工作。即将所有对象
// in header file
class CView : publicCWnd
{
DECLARE_DYNAMIC(CView)
...
};
// in implementationfile
IMPLEMENT_DYNAMIC(CView, CWnd)
上述的码展开来成为:
// 头文件
class CView : publicCWnd
{
public:
static CRuntimeClass classCView; \
virtual CRuntimeClass*GetRuntimeClass() const;
//展开后定义了一个CRuntimeClass静态对象以及该对象的获取函数
//静态成员变量classCView,所有该类对象都共享该变量
//获取函数在这里仅仅是声明,具体实现需要在CPP中进行
...
};
// 实现
static char _lpszCView[] = "CView";//CRuntimeClass的第一个成员变量
//注意这里定义了一个static的字符串,下面传参传入的就是这个字符串。这样做会使得所有的该类成员名称都相同
CRuntimeClass CView::classCView = {
_lpszCView, sizeof(CView), 0xFFFF, NULL,
&CWnd::classCWnd, NULL };
//该行代码用于为定义的CRuntimeClass对象classCView赋值。因为CRuntimeClass是个结构体,所以可以用{}将所有的值包含在内并直接使用=来进行赋值。
//注意这种赋值方式仅仅会为结构体的常规成员变量赋值,而static变量与函数都是单独存储在另一片内存区的,所以不会被赋值。
static AFX_CLASSINIT _init_CView(&CView::classCView);
CRuntimeClass* CView::GetRuntimeClass() const
{ return &CView::classCView; }//对CRuntimeClass对象的获取函数进行实现
所有的MFC类,只要在类内调用上面的两个宏DECLARE_DYNAMIC与IMPLEMENT_DYNAMIC,即可将CRuntimeClass对象及其相关函数塞入类中。从而可以实现运行时类型识别的功能。
DECLARE_DYNAMIC宏必须配合IMPLEMENT_DYNAMIC宏来使用,而IMPLEMENT_DYNAMIC宏又必须传入当前类的基类。MFC所有的类都是从CObject派生而来,所以除CObject之外的类调用这两个宏是没问题的。但CObject是没有基类的,所以CObject直接调用这两个宏会有问题。
于是,CObject就不再调用这两个宏,而是改为单独进行CRuntimeClass对象及其相关函数的实现,直接将代码写在了CObject类的.h文件中。
同时,由于CRuntimeClass内部的pFirstClass变量是static类型,CObject是第一个使用CRuntimeClass对象的类,所以在CObject所在的头文件需要对pFirstClass进行初值的赋值,MFC中将其赋值为NULL。
编译过程中:
比如定义了一个CWinApp对象、一个CFrameWnd对象、一个CDocument对象、一个CView对象。
① 首先编译CWinApp对象。检查CWinApp对象是否有基类;若有,则检查其基类是否编译过;若没有编译过,则先编译其基类;然后对其基类进行检测,检测其是否有基类……
不断重复上述步骤,直到检测到CObject为止。于是,就出现了这样的编译顺序:
CObject->CCmdTarget->CWinThread->CWinApp
② 然后编译CFrameWnd对象。检测CFrameWnd的基类,发现是CWnd,没有编译过;检测CWnd的基类,发现是CCmdTarget,CCmdTarget已经编译过了,故而不再编译。于是其编译顺序为:
CWnd->CFrameWnd
③ 接着编译CDocument对象。其基类是CCmdTarget,已经编译过了,故而编译顺序为:
CDocument
④ 最后编译CView对象。其基类是CWnd,已经编译过了,故而编译顺序为:
CView
如下图:
有了上面这张网,就可以实现RTTI的能力。
由于CObject是所有类的基类,因此在CObject中定义一个函数IsKindOf(),以便于所有类继承:该函数只需要在CObject中定义
BOOL CObject::IsKindof(const CRuntimeClass *pClass) const
{
CRuntimeClass* pClassThis=GetRuntimeClass();
while(pClassThis != NULL)
{
if(pClassThis==pClass)
return TRUE;
pClassThis=pClassThis->m_pBaseClass;
}
return FALSE;
}
当对某个类调用了IsKindOf()函数时,IsKindOf()函数就会判断当前类的CRuntimeClass对象与参数指定类的CRuntimeClass对象是否相等。
① 若相等,说明当前类就是指定类,也就是当前类与指定类是同一个类,返回TRUE。
② 若不相等,那就判断当前类的基类CRuntimeClass对象与指定类的CRuntimeClass对象是否相等,若相等,说明当前类的基类与指定类是同一个类,从而当前类是指定类的派生类,返回TRUE。
③ 若依然不相等,继续重复步骤②
④ 如果遍历到CObject依然与指定类的CRuntimeClass对象不相等,那么说明当前类既不是指定类也不是指定类的派生类,返回FALSE。
使用方法:
如有1个对象:
CMyDoc* pMyDoc;
则:
//判断pMyDoc是否属于CMyDoc,返回TRUE
pMyDoc-> IsKindOf(RUNTIME_CLASS (CMyDoc));
//判断pMyDoc是否属于CObject,返回TRUE
pMyDoc-> IsKindOf(RUNTIME_CLASS (CObject));
//判断pMyDoc是否属于CView,返回FALSE
pMyDoc-> IsKindOf(RUNTIME_CLASS (CView));
以上是为了实现在运行时可以识别某个类型。要做到动态创建,还需要在CRuntimeClass的struct中加入:
struct CRuntimeClass
{
……
CObject*CreateObject();
staticCRuntimeClass* PASCAL Load();
}
两个函数的实现:
CObject* CRuntimeClass::CreateObject()//该函数用于创建一个当前类的对象
{
if(m_pfnCreateObject==NULL)return NULL;
CObject*pObject;
pObject=(*m_pfnCreateObject)();//函数指针调用
returnpObject;
}
CRuntimeClass * PASCAL CRuntimeClass::Load()//该函数用于匹配输入的类名,并返回一个指向该类的CRuntimeClass指针。否则返回NULL
{
charszClassXXX[64];
CRuntimeClass*pClass;
cin>>szClassXXX; //假定这是我们希望动态创建的类名,这里用手动输入
for(pClass=pFirstClass;pClass!=NULL;pClass=pClass->m_pNextClass)
{
if(strcmp(szClassXXX,pClass->m_lpszClassName)==0)
return pClass;
}
return NULL;
}
同时要添加两个宏DECLARE_DYNCREATE与IMPLEMENT_DYNCREATE:
#define DECLARE_DYNCREATE(class_name)\
DECLARE_DYNAMIC(class_name) \
static CObject* PASCAL CreateObject();
#define IMPLEMENT_DYNCREATE(class_name,base_class_name) \
CObject*PASCAL class_name::CreateObject() \
{return new class_name; } \
IMPLEMENT_RUNTIMECLASS(class_name, base_class_name,0xFFFF, \
class_name::CreateObject,NULL)
可见DECLARE_DYNCREATE与IMPLEMENT_DYNCREATE这两个宏包含了RTTI所需要的两个宏DECLARE_DYNAMIC与IMPLEMENT_RUNTIMECLASS。因此,具备动态创建能力的类,必然具备RTTI能力。
但是并非所有的MFC类都用动态创建宏替换了RTTI宏。只有替换掉的才能动态创建,未替换掉的不能动态创建。
例如:
① CObject中两种宏都没有使用,所以CObject不具备动态创建能力。
② CCmdTarget、CWinThread、CWinApp、CDocument、CView都使用了RTTI宏,而没有使用动态创建宏,所以这些类具备RTTI能力,但却不能动态创建。
③ CWnd、CFrameWnd使用了动态创建宏,所以这两个类即能动态创建也具备RTTI能力。
由此可知:子类与父类的动态创建能力是没有关系的。可能子类具有动态创建能力而父类没有;也可能父类具有动态创建能力而子类没有。这要看具体父类与子类中各自调用的宏是哪一种。
仿真代码:
void main()
{
CRuntimeClass* pClassRef;
CObject* pOb;
while (1)
{
//首先根据输入的类名,获取一个该类的CRuntimeClass指针
//若获取不到则break
if((pClassRef = CRuntimeClass::Load()) == NULL)
break;
//然后调用上面CRuntimeClass指针的CreateObject()函数来创建对象
pOb = pClassRef->CreateObject();
if(pOb !=NULL)
pOb->SayHello();
}
}