举个例子说明虚函数、多态、早l定和晚l定Q?br>
李氏两兄妹(哥哥和妹妹)(j)参加姓氏q动?x)(不同姓氏l队参加Q,哥哥男子目比赛Q妹妹参加女子项目比赛,开q式有一个参赛队伍代表发a仪式Q兄妹俩都想
去露露脸Q可只能一人去Q最l他们决定到时抓阄决定,而组委会(x)也不反对Q它才不兛_(j)是哥哥还是妹Ҏ(gu)发言Q只要派一个姓李的来说两句话就行。运动会(x)如期?
行,妹妹抓阄获得代表李家发言的机?x),哥哥参加了(jin)男子项目比赛,妹妹参加了(jin)女子项目比赛。比赛结果就不是我们兛_(j)的了(jin)?br> 现在让我们来做个cLQ只讨论与运动会(x)相关的话题)(j)Q?br> Q?Q类的设计:(x)
李氏兄妹属于李氏家族Q李氏是基类Q这里还是抽象的U基c)(j)Q李氏又zZ个子c(李氏男和李氏奻I(j)Q李氏男?x)所有男子项目的比赛Q李氏男的成员函
敎ͼ(j)Q李氏女?x)所有女子项目的比赛Q李氏女的成员函敎ͼ(j)。姓李的人都?x)发aQ基c虚函数Q,李氏男和李氏女承自李氏当然也会(x)发言Q只是男奌话声音不一
P内容也会(x)又差异,lh感觉不同Q李氏男和李氏女分别重新定义发言q个虚函敎ͼ(j)。李氏两兄妹是李氏男和李氏女两个类的实体?br> Q?Q程序设计:(x)
李氏兄妹填写参赛报名表?br> Q?Q编译:(x)
李氏兄妹的参赛报名表被上交给l委?x)(~译器)(j)Q哥哥和妹妹分别参加男子和女子的比赛Q组委会(x)一看就明白?jin)(早绑定?j)Q只是发a人选不明确Q组委会(x)看到?
名表上写的是“李家代表”Q基cL针)(j)Q组委会(x)不能定到底是谁Q就做了(jin)个备注:(x)如果是男的,是哥哥李某某;如果是女的,是妹妹李某某(晚绑定)(j)。组
委会(x)做好其它准备工作后,qq动?x)开始了(jin)Q编译完毕)(j)?br> Q?Q程序运行:(x)
q动?x)开始了(jin)Q程序开始运行)(j)Q开q式上我们听C(jin)李家妹妹的发aQ如果是哥哥q气好抓阄胜出,我们听到哥哥的发言Q多态)(j)。然后就是看到兄妹俩参加比赛?jin)。。?/p>
但愿q个比喻说清楚了(jin)虚函数、多态、早l定和晚l定的概念和它们之间的关pR再说一下,早绑定指~译器在~译期间即知道对象的具体cdq确定此对象调用成员函数的确切地址Q而晚l定是根据指针所指对象的cd信息得到cȝ虚函数表指针q而确定调用成员函数的切地址?br>
2、揭密晚l定的秘?/p>
~译器到底做?jin)什么实现的虚函数的晚绑定呢Q我们来探个I竟?/p>
~译器对每个包含虚函数的cdZ个表Q称为V TA B L EQ。在V TA B L E中,~译器放|特定类的虚函数地址。在每个带有虚函数的c?
中,~译器秘密地|一指针Q称为v p o i n t e rQ羃写ؓ(f)V P T RQ,指向q个对象的V TA B L E?span style="color: #000000;">通过基类指针做虚函数?用时Q也是做多态调用时Q,~译器静(rn)态地插入取得q个V P T RQƈ在V TA B L E表中查找函数地址的代码,q样p调用正确的函C晚捆l发生?/span>为每个类讄V TA B L E、初始化V P T R、ؓ(f)虚函数调用插入代码,所有这些都是自动发生的Q所以我们不必担?j)这些。利用虚函数Q?q个对象的合适的函数p被调用,哪怕在~译器还不知道这个对象的特定cd的情况下。(《C++~程思想?/span>Q?/p>
————这D话U色加粗部分g有点问题Q我个h的理解看后面的ȝ?/span> 在Q何类中不存在昄的类型信息,可对象中必须存放cM息,否则cd不可能在q行时徏立。那q个cM息是什么呢Q我们来看下面几个类Q?/p>
class no_virtual class one_virtual class two_virtual 以上三个cMQ?br> no_virtual没有虚函敎ͼsizeof(no_virtual)=4Q类no_virtual的长度就是其成员变量整型a的长度; q个VPTR可以看作类的类型信息?/p>
那我们来看看~译器是怎么建立VPTR指向的这个虚函数表的。先看下面两个类Q?br> class base class derived : public base 两个cVPTR指向的虚函数表(VTABLEQ分别如下:(x) 个hȝ如下Q?/span> VPTR
常常位于对象的开_(d)~译器能很容易地取到VPTR的|从而确定VTABLE的位|。VPTRL向VTABLE的开始地址Q所有基cd它的子类的虚?
数地址Q子c自己定义的虚函数除外)(j)在VTABLE中存储的位置L相同的,如上面basecdderivedcȝVTABLE中vfun1和vfun2
的地址L按相同的序存储。编译器知道vfun1位于VPTR处,vfun2位于VPTR+1处,因此在用基类指针调用虚函数时Q编译器首先获取指针?
向对象的cd信息QVPTRQ,然后去调用虚函数。如一个basecL针pBase指向?jin)一个derived对象Q那pBase->vfun2
()被编译器译?nbsp;VPTR+1 的调用,因ؓ(f)虚函数vfun2的地址在VTABLE中位于烦(ch)引ؓ(f)1的位|上。同理,pBase->vfun3
()被编译器译?nbsp;VPTR+2的调用。这是所谓的晚绑定?/p>
我们来看一下虚函数调用的汇~代码,以加q解?/p>
void test(base* pBase) int main(int argc, char* argv[]) derived td;~译生成的汇~代码如下:(x) pBase->vfun2();~译生成的汇~代码如下:(x) 现在应该对多态、虚函数、晚l定有比较清楚的?jin)解了(jin)吧?br>
{
public:
void fun1() const{}
int fun2() const { return a; }
private:
int a;
}
{
public:
virtual void fun1() const{}
int fun2() const { return a; }
private:
int a;
}
{
public:
virtual void fun1() const{}
virtual int fun2() const { return a; }
private:
int a;
}
one_virtual有一个虚函数Qsizeof(one_virtual)=8Q?br> two_virtual
有两个虚函数Qsizeof(two_virtual)=8Q?nbsp;有一个虚函数和两个虚函数的类的长度没有区别,其实它们的长度就是no_virtual?
长度加一个void指针的长度,它反映出Q如果有一个或多个虚函敎ͼ~译器在q个l构中插入一个指针( V P T RQ。在one_virtual ?
two_virtual之间没有区别。这是因为V P T R指向一个存攑֜址的表Q只需要一个指针,因ؓ(f)所有虚函数地址都包含在q个表中?/p>
{
public:
void bfun(){}
virtual void vfun1(){}
virtual int vfun2(){}
private:
int a;
}
{
public:
void dfun(){}
virtual void vfun1(){}
virtual int vfun3(){}
private:
int b;
}
basec?br> —————?br> VPTR—?gt; |&base::vfun1 |
—————?br> |&base::vfun2 |
—————?br>
derivedc?br> ——————?br> VPTR—?gt; |&derived::vfun1 |
——————?br> |&base::vfun2 |
——————?br> |&derived::vfun3 |
——————?br>
每当创徏一个包含有虚函数的cL从包含有虚函数的cL生一个类Ӟ~译器就个类创徏一个VTABLEQ如上图所C。在q个表中Q编译器攄?jin)在q个c?
中或在它的基cM所有已声明为virtual的函数的地址。如果在q个zcM没有对在基类中声明ؓ(f)virtual的函数进行重新定义,~译器就使用基类
的这个虚函数地址。(在derived的VTABLE中,vfun2的入口就是这U情c(din))(j)然后~译器在q个cM攄VPTR。当使用单承时Q对于每
个对象只有一个VPTR?span style="font-weight: bold; color: #ff0000;">VPTR必须被初始化为指向相应的VTABLEQ这在构造函C发生?/span>
一旦VPTR被初始化为指向相应的VTABLEQ对象就"知道"它自己是什么类型。但只有当虚函数被调用时q种自我认知才有用?/p>
1、从包含虚函数的cL生一个类Ӟ~译器就cdZ个VTABLE。其每一个表Ҏ(gu)该类的虚函数地址?br>2、在定义该派生类对象Ӟ先调用其基类的构造函敎ͼ然后再初始化VPTRQ最后再调用zcȝ构造函敎ͼ
从二q制的视野来看,所谓基cdcL一个大l构体,其中this指针开头的四个字节存放虚函数表头指针。执行子cȝ构造函数的时候,首先调用基类构造函
敎ͼthis指针作ؓ(f)参数Q在基类构造函C填入基类的vptrQ然后回到子cȝ构造函敎ͼ填入子类的vptrQ覆盖基cd入的vptr。如此以来完?
vptr的初始化?Q?br>3、在实现动态绑定时Q不能直接采用类对象Q而一定要采用指针或者引用。因为采用类对象传值方式,有(f)时基cd象的产生Q而采用指针,则是通过指针来访问外部的zcd象的VPTR来达到访问派生类虚函数的l果?
{
pBase->vfun2();
}
{
derived td;
test(&td);
return 0;
}
mov DWORD PTR _td$[esp+24], OFFSET FLAT:??_7derived@@6B@ ; derived::`vftable'
q译器的注释可知,此时PTR _td$[esp+24]中存储的是derivedcȝVTABLE地址?br>
test(&td);~译生成的汇~代码如下:(x)
lea eax, DWORD PTR _td$[esp+24]
mov DWORD PTR __$EHRec$[esp+32], 0
push eax
call ?test@@YAXPAVbase@@@Z ; test
调用test函数时完成了(jin)如下工作Q取对象td的地址Q将其压栈,然后调用test?/p>
mov ecx, DWORD PTR _pBase$[esp-4]
mov eax, DWORD PTR [ecx]
jmp DWORD PTR [eax+4]
首先从栈中取出pBase指针指向的对象地址赋给ecxQ然后取对象开头的指针变量中的地址赋给eaxQ此时eax的值即为VPTR的|也就?
VTABLE的地址。最后就是调用虚函数?jin),׃vfun2位于VTABLE的第二个位置Q相当于 VPTR+1Q每个函数指针是4个字节长Q所以最后的
调用被编译器译?nbsp;jmp DWORD PTR [eax+4]。如果是调用pBase->vfun1()Q这句就该被~译?
jmp DWORD PTR [eax]?br>
]]>