📄 天方夜谭vcl:多态.htm
字号:
int r;
point p = {dx, dy};
dispatch(-1, &p, &r);
return r;
}
Dynamic void draw(void *hdc) {dispatch(-2, hdc, 0);}
Dynamic void save(void* o) const {dispatch(-3, o, 0);}
Dynamic void load(void* i) {dispatch(-4, i, 0);}
};
<P style="BACKGROUND: #ccffcc"><CODE>const DMT Shape::dmt_Shape =
DMT(0, 4, -1, -2, -3, -4, &Shape::f_move, 0, 0, 0);</CODE></P>
</PRE>
<P>背景突出部分就是有改动的地方。如果子类Triangle要改写Shape::draw,那么只需要 <PRE>class Triangle {
private:
void f_draw(void*);
...
protected:
static const DMT dmt_Triangle;
...
public:
Triangle() {dmt = &dmt_Triangle; ...}
...
};
const DMT Triangle::dmt_Triangle =
DMT(Shape::dmt_Shape, ..., -2, ..., &Triangle::f_draw...);
</PRE>这就是对“Dynamic函数”的另一种实现,这样可以分离数据和代码。当然这个示例并不具备实际应用价值,在静态成员初始化、调用约定、可读性等诸多设计上都有不少问题,仅仅起演示作用。
<H5>动态(dynamic)函数</H5>
<P>Object
Pascal提供了两种函数实现多态:一种是我们熟悉的虚拟(virtual)函数,另外一种则是动态(dynamic)函数,其实就是对前面的“Dynamic函数”提供的语言级别的支持。
<P>可能有些用C++ Builder的朋友说,C++ Builder里怎么看到啊?在C++
Builder里,标识动态函数的宏(macro)是DYNAMIC,也就是__declspec(dynamic),这是Borland对C++的扩充。像TControl::Click、TControl::MouseMove等等都是动态函数。DYNAMIC的用法和virtual基本一致,我所发现的不同仅仅是,当子类改写父类相应函数时,子类中virtual可以省略,而DYNAMIC则不行。
<P>那么,每个类的动态函数的入口在哪里呢?上次,我们已经挖出了VMT的分布图,里面就有vmtDynamicTable =
0xffffffd0这么一句,字面就告诉我们,这是动态方法表DMT(Dynamic Method Table)的入口。不妨检验一下。 <PRE>#include <VCL.H>
#include <IOSTREAM>
struct A: private TObject
{
DYNAMIC void f1() = 0;
void f3() {}
virtual void f4() {}
DYNAMIC void f2() = 0;
};
struct B: A
{
DYNAMIC void f1() {}
DYNAMIC void f2() {}
};
void main()
{
A* p = new B;
std::cout<<(void*)p<<STD::ENDL; p->f1();
p->f2();
delete p;
}
</PRE>这个程序会输出“0118095C”。当然不同的机器上这个数值可能有所不同,总之先记下了。
<P>其中p->f1();的汇编代码是 <PRE>push dword ptr [ebp-0x30]
or edx,-0x01 ;这句其实就相当于mov edx,0xffffffff
mov eax,[ebp-0x30]
call System::FindDynaInst(void *, int)
call eax
pop ecx
</PRE>p->f2();的汇编代码是 <PRE>push dword ptr [ebp-0x30]
mov edx,0xfffffffe
mov eax,[ebp-0x30]
call System::FindDynaInst(void *, int)
call eax
pop ecx
</PRE>程序很简单,我们说明一下。
<UL>
<LI>or edx,-0x01跟mov
edx,0xffffffff的效果是完全一样的,任何数和0xffffffff进行“或”运算的结果当然都是0xffffffff;
<LI>这两段唯一的不同就是mov edx,0xffffffff(也就是or edx,-0x01)和mov
edx,0xfffffffe,我们上次已经说过了补码表示法,这里其实就是分别传递的A::f1和A::f2的函数代号–1和–2;
<LI>执行过mov eax,[ebp-0x30]这一句后,我们可以发现eax的值就是刚才我们记下的数(0118095C),这里包含了指向VMT入口的指针;
<LI>向System::FindDynaInst传入的两个参数就分别是包含指向VMT入口的指针,以及相应函数的代号,分别在eax和edx里;
<LI>显然System::FindDynaInst把对应函数代号的函数指针放在eax里,call eax就调用相应的函数。
</LI></UL>这就是整个大的流程。现在我们关心的是,System::FindDynaInst(void*,
int)到底做了些什么。我们可以跟踪进去,再跳一层,我们来到了函数中,源代码就是Source\Vcl\system.pas中的_FindDynaInst。 <PRE>procedure _FindDynaInst;
asm
PUSH EBX
MOV EBX,EDX ;EBX储存了函数的代号
MOV EAX,[EAX] ;EAX获得VMT入口地址
CALL GetDynaMethod ;调用GetDynaMethod
MOV EAX,EBX
POP EBX
JNE @@exit
POP ECX
JMP _AbstractError
@@exit:
end;
</PRE>那么我们还得看看GetDynaMethod的源代码。 <PRE>procedure GetDynaMethod;
{function GetDynaMethod(vmt: TClass; selector: Smallint) : Pointer;}
asm
{ -> EAX vmt of class }
{ BX dynamic method index }
{ <- EBX pointer to routine }
{ ZF = 0 if found }
{ trashes: EAX, ECX }
PUSH EDI
XCHG EAX,EBX ;交换eax和ebx的值
JMP @@haveVMT ;交换后ebx是VMT入口地址,eax是函数代号
@@outerLoop:
MOV EBX,[EBX] ;取地址
@@haveVMT:
MOV EDI,[EBX].vmtDynamicTable ;EDI是DMT的入口
TEST EDI,EDI ;测试是否存在DMT(EDI是否为0)
JE @@parent ;若DMT不存在,在父类中继续找
MOVZX ECX,word ptr [EDI] ;取头两个字节,即动态函数个数
PUSH ECX
ADD EDI,2 ;跳至黄色部分(见后面的图)
REPNE SCASW ;查找eax
JE @@found ;若找到则跳转
POP ECX
@@parent:
MOV EBX,[EBX].vmtParent ;在父类中继续
TEST EBX,EBX ;是否有父类
JNE @@outerLoop ;有则继续查找
JMP @@exit ;无则跳转
@@found:
POP EAX
ADD EAX,EAX ;以下两步是清除ZF,其中ECX值为0
SUB EAX,ECX { this will always clear the Z-flag ! }
MOV EBX,[EDI+EAX*2-4] ;edi-1是函数代号所在处
@@exit:
POP EDI
end;
</PRE>看汇编头晕吧?嘿嘿,对着注释看看这个图就清楚了。vmtDynamicTable所指向的地址,就是一个DMT,而它的结构,我们前面已经分析过了。唯一需要说明的是
<PRE>ADD EAX,EAX ;EAX值为n,自加后为2*n
SUB EAX,ECX ;ecx值已经递减为0,这句仅仅是清除ZF标志位
MOV EBX,[EDI+EAX*2-4] ;
</PRE>清除ZF是因为_FindDynaInst要由此判断是否找到相应的函数。而edi-4为函数代号所在的地方,edi-4+4*n即为函数指针所在,也就是edi+eax*2-4。
<P>其实不需要与汇编纠缠,在前面我们已经知道了其原理,大同小异罢了。
<H5>结束语</H5>
<P>C++的重用性是对源代码级而言,而对二进制级重用性的支持则捉襟见肘。特别是动态连接库DLL的广泛运用,更显出解决这个问题的重要性。COM的口号之一正是COM
as a Better C++7。讲COM的书中往往指出若干C++的不足,其实不少是可以解决的。比如
<UL>
<LI>问题:不同编译器的名字粉碎机制不同,导致不同编译器编译的模块不能顺利连接。
<LI>解决:使用DEF文件。
<LI>代价:操作麻烦,增加维护负担,但对程序效率没有任何影响。 </LI></UL>
<UL>
<LI>问题:不同版本的类大小不一样,主要原因是成员变量增加或减少,导致分配空间时出错。
<LI>解决:隐藏实现,成员变量仅保留一个指针void *,在运行时动态申请空间。
<LI>代价:可读性和性能均受影响。 </LI></UL>
<P>添加普通成员函数没有什么大的问题,但是添加虚函数则影响VFT,可能导致程序错误甚至系统崩溃。解决的办法在前面已经说明,其中良好的设计是必不可少的。建议
<UL>
<LI>根类的设计一定要慎重,VCL从开始至今,TObject类的变化始终很少,否则牵一发而动全身,维护性就大打折扣;
<LI>类层次应尽可能浅,尽量避免使用继承等耦合性很强的关系,严格遵循Liskov替换原则LSP[<A
href="http://www.c-view.org/journal/004/vcl_chong.htm#99" name=9>9</A>];
<LI>如果程序只在WINDOWS下运行,可以考虑使用COM;
<LI>如果始终使用Borland的编译器,并对性能要求不高,可以考虑使用动态(dynamic)函数;
<LI>多写几个无用的虚函数占位,也是个不错的方法。 </LI></UL>
<P>动态函数应用在合适的地方,这一点可以参考VCL各个类中动态函数的使用情况。另外,动态函数所节约的VFT空间微不足道,在有的情况反而DMT的空间占得更多。总体来说,动态函数在时间上吃亏,空间上占的便宜也不大。在我看来,解除了父类和子类VFT之间的关联性,才是动态函数最大的好处。
<P>不论是辨证唯物主义,还是道家思想,都强调事物的两面性。不论什么方法,都是一把双刃剑,所谓“祸兮福之所倚,福兮祸之所伏”。我们要做的,就是权衡利弊,结合具体的环境,扬长避短。
<H5>参考</H5>
<P><A href="http://www.c-view.org/journal/004/vcl_chong.htm#1" name=11>1</A>.
虫虫.《<A
href="http://www.c-view.org/journal/003/vcl_chong.htm">天方夜谭VCL:开门</A>》.C++
View.2001,9.
<P><A href="http://www.c-view.org/journal/004/vcl_chong.htm#2" name=22>2</A>.
George Shepherd, Brad King. Inside ATL. Microsoft Press. 1999.
<P><A href="http://www.c-view.org/journal/004/vcl_chong.htm#3" name=33>3</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/004/vcl_chong.htm#4" name=44>4</A>.
GoF. Design Patterns: Elements of Reusable Object-Oriented Software.
Addison-Wesley, Reading, MA. 1995.
<BR> 李英军等.《设计模式:可复用面向对象软件软件的基础》.机械工业出版社.2000.
<P><A href="http://www.c-view.org/journal/004/vcl_chong.htm#5" name=55>5</A>.
CKER.《<A
href="http://www.c-view.org/journal/001/bcb_vcl.htm">深入BCB理解VCL的消息机制</A>》.C++
View第1期.
<P><A href="http://www.c-view.org/journal/004/vcl_chong.htm#6" name=66>6</A>.
Dale Rogerson. Inside COM. Microsoft Press. 1997.
<BR> 杨秀章.《COM技术内幕》.清华大学出版社.1999.
<P><A href="http://www.c-view.org/journal/004/vcl_chong.htm#7" name=77>7</A>.
Don Box. Essential COM. Addison-Wesley, Reading, MA. 1998.
<BR> 侯捷.《COM本质论》.碁峰资讯股份有限公司.1999.
<BR> 潘爱民.《COM本质论》.中国电力出版社.2001.
<P><A href="http://www.c-view.org/journal/004/vcl_chong.htm#8" name=88>8</A>.
Brent E. Rector and Chris Sells. ATL Internals. Addison-Wesley, Reading, MA.
1999. <BR> 潘爱民,新语.《ATL深入解析》.中国电力出版社.2001.
<P><A href="http://www.c-view.org/journal/004/vcl_chong.htm#9" name=99>9</A>.
Robert C.Martin. “The Liskov Substitution Principle”. C++ Report. 1996, 3.
<BR> 虫虫,plpliuly.《Liskov替换原则LSP》.C++ View.2001,9.
</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 + -