📄 天方夜谭vcl:开门.htm
字号:
* really a void**. Be sure when using this method to provide
* two levels of indirection and cast away one of them.
*/
bool GetInterface(const TGUID &IID, /* out */ void *Obj);
/* class method */
static PInterfaceEntry GetInterfaceEntry(const TGUID IID);
static PInterfaceTable * GetInterfaceTable(void);
ShortString ClassName()
{
return ClassName(ClassType());
}
bool ClassNameIs(const AnsiString string)
{
return ClassNameIs(ClassType(), string);
}
TClass ClassParent()
{
return ClassParent(ClassType());
}
void * ClassInfo()
{
return ClassInfo(ClassType());
}
long InstanceSize()
{
return InstanceSize(ClassType());
}
bool InheritsFrom(TClass aClass)
{
return InheritsFrom(ClassType(), aClass);
}
void * MethodAddress(const ShortString &Name)
{
return MethodAddress(ClassType(), Name);
}
ShortString MethodName(void *Address)
{
return MethodName(ClassType(), Address);
}
virtual HResult SafeCallException(TObject *, void *);
virtual void AfterConstruction();
virtual void BeforeDestruction();
virtual void Dispatch(void *Message);
virtual void DefaultHandler(void* Message);
private:
virtual TObject* NewInstance(TClass cls);
public:
virtual void FreeInstance();
virtual ~FObject(); /* Body provided by VCL {} */
};
</PRE>当然FObject::ClassType我们已经会写了,那就是 <PRE>TClass FObject::ClassType()
{
return *(TClass*)this;
}
</PRE>我们会在后面陆续把这些成员函数填充完整。先举个例,拿类名(ClassName)开刀吧。
<P>查查VMT表,vmtClassName = 0xffffffd4,我们就从这里下手。主要的步骤是:
<UL>
<LI>找到VMT的入口;
<LI>通过vmtClassName找到储存类名的地址;
<LI>获取类名。 </LI></UL>
<P>0xffffffd4也就相当于–
44,也就是VMT入口指向的地址开始,倒数第44字节到倒数第41字节这4个字节所代表的指针,指向类名。假设入口指向的地址是cls,那么vmtClassName所代表的地址就是(char*)cls
– 44,亦即(char*)cls + vmtClassName。
<P>注意一个字符串格式的问题,VCL既然是用Object
Pascal写的,其中储存类名的字符串的格式必然是Pascal传统方式,也就是第1个字节为字符串的长度,紧接着为字符串的实际内容。在C++
Builder中,与之对应的类型是ShortString。
<P align=center><IMG
src="天方夜谭VCL:开门.files/vcl05.gif"><BR><BR>图5 TObject::ClassName的运作方式
<P>代码如下: <PRE>ShortString FObject::ClassName(TClass cls)
{
ShortString* r = *(ShortString**)((char*)cls + vmtClassName);
return *r;
}
</PRE>我们不妨测试一下。 <PRE>#include <VCL.H>
#include <MEMORY>
#include <IOSTREAM>
using namespace std;
... 插入FObject相应的代码...
void main()
{
auto_ptr<TLIST> list(new TList);
FObject* p = (FObject*)list.get();
cout << AnsiString(p->ClassName()).c_str() << endl;
cout << AnsiString(list->ClassName()).c_str() << endl;
}
</PRE>输出结果在我们意料之内,都是“TList”。
<P>对于函数ClassNameIs,我们就可以轻而易举地完成了。 <PRE>bool FObject::ClassNameIs(TClass cls, const AnsiString string)
{
return string==ClassName(cls);
}
</PRE>有朋友可能奇怪,你怎么知道TObject::ClassName是这样的呢?
<P>三种办法:
<UL>
<LI>猜,用经验推测;
<LI>看Borland提供的原始码;
<LI>看编译以后的汇编码。 </LI></UL>
<P>在Borland提供的原始码中,我们可以看到TObject::ClassName的实现如下: <PRE>class function TObject.ClassName: ShortString;
asm
{ -> EAX VMT }
{ EDX Pointer to result string }
PUSH ESI
PUSH EDI
MOV EDI,EDX
MOV ESI,[EAX].vmtClassName
XOR ECX,ECX
MOV CL,[ESI]
INC ECX
REP MOVSB
POP EDI
POP ESI
end;
</PRE>熟悉汇编的朋友就可以由此写出相应的C/C++代码来。对于不会的朋友,根据我们的讲解,相信也可以轻而易举地完成吧。
<P>希望您在看这段的时候,不妨先用第1种办法,然后结合2、3看看,一定收获不小。
<H5>势如破竹</H5>
<P>接下来就太简单了,我们不再举例,把相应的成员函数补充完整即可。您不妨先自己试着写写,探索一下,再与汇编代码和文中的代码作比较,一定乐趣无穷。
<P>TObject::ClassInfo是做什么的?问我啊?我也不知道。VCL的帮助里说,用ClassInfo可以访问包含对象类型、祖先类和所有published属性信息的RTTI表。这个表只是内部使用,TObject提供了其它方法来访问RTTI信息。我们先写出它的实现。
<P align=center><IMG
src="天方夜谭VCL:开门.files/vcl06.gif"><BR><BR>图6 TObject::ClassInfo的运作方式 <PRE>void * FObject::ClassInfo(TClass cls)
{
return *(void**)((char*)cls + vmtTypeInfo);
}
</PRE>Borland的说法可信吗?这个函数返回值的类型是void
*,明摆着不愿意透露更多的信息。您不妨按上面ClassName的方法测试一下,对于TList,ClassInfo输出的结果居然是0!也就是一个空指针!什么东东?别急,后面我们会掀开这个void
*的面纱,现在姑且卖个关子。
<P>VCL框架中只存在单继承,这是由Object
Pascal语言的特性决定的。这样,每一个类只有唯一一个父类,函数TObject::ClassParent就能帮您把父类找出来。 <PRE>TClass FObject::ClassParent(TClass cls)
{
TClass* r = *(TClass**)((char*)cls + vmtParent);
return (r)? (*r) : 0;
}
</PRE>由此,我们也能很轻松地模拟TObject::InheritsForm的实现。 <PRE>bool FObject::InheritsFrom(TClass cls, TClass aClass)
{
while(aClass)
{
if(aClass==cls)return true;
cls = ClassParent(cls);
}
return false;
}
</PRE>要知道一个对象所占的字节数,TObject::InstanceSize就可以达到目的。 <PRE>long FObject::InstanceSize(TClass cls)
{
return *(long*)((char*)cls + vmtInstanceSize);
}
</PRE>有朋友可能说,C++不是有sizeof操作符吗?为什么不用呢?在VCL中,sizeof有两个缺陷。首先sizeof是完全静态的,也就是说,如果您写sizeof(...),编译以后,这会被替换为一个常数,没有任何的求值过程,因此不能动态求值;其次,VCL类必须与指针或引用的形式存在。所以对于
<PRE>TObject *a;
...
</PRE>sizeof(*a)这样的表达式是错误的。而且即使TObject不是VCL类,使用sizeof(*a)还是相当于sizeof(TObject),没有实际价值。
<H5>结束语</H5>
<P>现在我们已经打开了通向VCL类秘密的大门。回头一看,VMT跟VFT有什么区别与联系呢?其实VMT可以算是VFT具体化的一个结果,也就是说,VMT是在VFT基础上发展出来的一种具有“规范”性质的结构,所有的VCL类都遵循这个“规范”。这很像COM与C++纯虚基类的关系。
<P>通过VMT,VCL放置了一些重要的信息,由此来实现RTTI。所以“高级”RTTI功能其实是相当低级和简单的一项技术。就其实现方式而言,大致有三种。MFC用宏(macro)模拟算是一类,完全符合C++标准,不需要对语言进行扩充,也不依赖于特定的编译器,不过给人臃肿的感觉;VCL则是完全由编译器实现,同时扩充了C++的语言特性,必须在Borland的编译器上编译,但是很简洁;另外建构KDE基础的跨平台框架Qt[<A
href="http://www.c-view.org/journal/003/vcl_chong.htm#44"
name=4>4</A>],则采用了折中的方式,扩充了C++的关键字,书写很简洁,在编译之前必须用Qt提供的程序MOC进行预处理,把扩充部分的代码改写为符合C++标准的代码,然后才可以在任何符合C++标准的编译器上编译。
<BR><BR>
<TABLE align=center border=1 borderColor=#111111 cellPadding=2
style="BORDER-COLLAPSE: collapse">
<THEAD bgColor=#66ccff>
<TR>
<TD>代表作</TD>
<TD align=middle>实现方式</TD>
<TD>不依赖特定编译器</TD>
<TD>简洁程度</TD>
<TD align=middle>编译次数</TD></TR>
<TBODY>
<TR>
<TD align=middle>MFC</TD>
<TD align=middle>宏插入</TD>
<TD align=middle>是</TD>
<TD align=middle>一般</TD>
<TD align=middle>1</TD></TR>
<TR>
<TD align=middle>VCL</TD>
<TD align=middle>编译器生成</TD>
<TD align=middle>否</TD>
<TD align=middle>好</TD>
<TD align=middle>1</TD></TR>
<TR>
<TD align=middle>Qt</TD>
<TD align=middle>预编译程序生成</TD>
<TD align=middle>是</TD>
<TD align=middle>较好</TD>
<TD align=middle>2(包括MOC)</TD></TR></TBODY></TABLE>
<P>注:如果长期仅在Windows平台下进行开发的朋友,可能没有听说过Qt的大名。事实上在Linux世界里,这可是个响当当的名头。Qt是一套完善的C++框架,横跨Unix/Linux、Windows、Mac
OS诸多平台,内部机制相当有趣。Borland最新的Kylix和Delphi6所采用的跨平台框架CLX(分为BaseCLX、VisualCLX、DataCLX、NetCLX四个部分,BaseCLX与VCL顶部几个类相同),其可视化部分VisualCLX就建构在Qt上,这多少让我感到失望和不满。Qt本身就跨平台,VisualCLX建构在Qt上,自然也跨平台;但是CLX是用Object
Pascal包装了一个C++框架,我不敢想象,C++ Builder6中的CLX是否又用C++再来包装这个包装了C++框架的Object
Pascal框架呢?如果真是如此,其效率和调试难度……
<P>对于“高级”RTTI的实现形式,VCL用了TMetaClass(其中TClass就是TMetaClass*)来配合存储类的信息,也就是所谓“类的类”,这非常普遍。MFC中的CObject与CRuntimeClass,JDK中的java.lang.Object和java.lang.Class都是如此。比如对于一个TObject
*p,如何获取其父类名呢?我们必须借助TMetaClass:可以先用p->ClassType返回父类的信息(是一个TMetaClass*类型),再以此为参数传入TObject::ClassName就可以获得结果,也就是TObject::ClassName(p->ClassType())即可。
<P>同时我们也应该拆穿所谓“拥有更高级RTTI的语言本身也更高级”的谎言。至少从我那少得可怜的经验来看,对于一套框架,除非需要和IDE配合,否则在绝大部分情况下,RTTI是完全没有必要的,甚至是有害的[<A
href="http://www.c-view.org/journal/003/vcl_chong.htm#55"
name=5>5</A>]。希望使用和设计框架的朋友三思。
<H5>致谢</H5>
<P>非常感谢孟岩和孙春阳对本文所提出的宝贵意见。
<H5>参考</H5>
<P><A href="http://www.c-view.org/journal/003/vcl_chong.htm#1" name=11>1</A>.
Bjarne Stroustrup. The C++ Programming Language, 3e. Addison-Wesley, Reading,
MA. 1997.
<P><A href="http://www.c-view.org/journal/003/vcl_chong.htm#2" name=22>2</A>.
Stanley Lippman. Inside the C++ Object Model. Addison-Wesley, Reading, MA. 1996
<BR> 侯捷.《深度探索C++物件模型》.碁峰资讯股份有限公司.1998.
<BR> 侯捷.《深度探索C++对象模型》.华中科技大学出版社.2001.
<P><A href="http://www.c-view.org/journal/003/vcl_chong.htm#3" name=33>3</A>.
侯捷.《深入浅出MFC》,2e.松岗电脑图资料股份有限公司/华中科技大学出版社.1997/2001.
<P><A href="http://www.c-view.org/journal/003/vcl_chong.htm#4" name=44>4</A>.
虫虫.《Qt最新消息》.C++ View.2001,7.
<P><A href="http://www.c-view.org/journal/003/vcl_chong.htm#5" name=55>5</A>.
Robert C.Martin. “The Open-Closed Principle”. C++ Report. 1996, 1.
<BR> plpliuly,虫虫.《开放封闭原则OCP》.C++ View.2001,8.
</TABLE><BR>
<HR width=700>
<BR>
<TABLE align=center border=0 height=17 width=360>
<TBODY>
<TR align=middle>
<TD>©2000-2002 C-View.ORG All Rights
Reserved.</TD></TR></TBODY></TABLE><BR></BODY></HTML>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -