例子中用到多态的代码以黑体标Z(jin)Q它们一个很明显的特征就是通过一个基cȝ指针Q或者引用)(j)来调用不同子cȝҎ(gu)?br>
那么Q现在的问题是,q个功能是怎样实现的呢Q我们可以先来大概猜一下:(x)对于一般的Ҏ(gu)调用Q到?jin)汇~代码这一层次的时候,一般都是?Call funcaddr
q样的指令进行调用,其中funcaddr是要调用函数的地址。按理来_(d)当我使用指针pShape来调用Draw的时候,~译器应该将Shape::Draw的地址赋给funcaddrQ然后Call
指o(h)可以直接调用Shape::Draw?jin),q就跟用pShape来调用Shape::Erase一栗但是,q行l果却告诉我们,~译器赋lfuncaddr的值却是Circle::Drawde的倹{这p明,~译器在对待DrawҎ(gu)和EraseҎ(gu)时用了(jin)双重标准。那么究竟是谁有q么大的法力Qɾ~译器这个铁面无U的判官都要另眼相看呢?virtualQ!
CleverQ!正是virtualq个关键字一手导演了(jin)q一?#8220;乑֝大挪U?#8221;的好戏。说道这里,我们先要明确两个概念Q静(rn)态绑定和动态绑定?br>
1、静(rn)态绑定(static
bingdingQ,也叫早期l定Q简单来说就是编译器在编译期间就明确知道所要调用的Ҏ(gu)Qƈ该Ҏ(gu)的地址赋给?jin)Call指o(h)的funcaddr。因此,q行期间直接使用Call指o(h)可调用到相应的Ҏ(gu)?br> 2、动态绑定(dynamic
bindingQ,也叫晚期l定Q与?rn)态绑定不同,在编译期_(d)~译器ƈ不能明确知道I竟要调用的是哪一个方法,而这Q要知道q行期间使用的具体是哪个对象才能军_?br>
好了(jin)Q有?jin)这两个概念以后Q我们就可以_(d)virtual的作用就是告诉编译器Q我要进行动态绑定!~译器当然会(x)重你的意见Q而且Z(jin)完成你这个要求,~译器还要做很多的事情:(x)~译器自动在声明?jin)virtualҎ(gu)的类中插入一个指针vptr和一个数据结构VTableQvptr用以指向VTableQVTable是一个指针数l,里面存放着函数的地址Q,q保证二者遵守下面的规则Q?br>
1、VTable中只能存攑֣明ؓ(f)virtual的方法,其它Ҏ(gu)不能存放在里面。在上面的例子中QShape的VTable中就只有DrawQMoveTo和~Shape。方法Erase的地址q不能存攑֜VTable中。此外,如果Ҏ(gu)是纯虚函敎ͼ?
DrawQ那么同栯在VTable中保留相应的位置Q但是由于纯虚函数没有函CQ因此该位置中ƈ不存放Draw的地址Q而是可以选择存放一个出错处理的函数的地址Q当该位|被意外调用Ӟ可以用出错函数进行相应的处理?br>
2、派生类的VTalbe中记录的从基cMl承下来的虚函数地址的烦(ch)引号必须跟该虚函数在基类VTable中的索引号保持一致。如在上例中Q如果在Shape的VTalbe中,Draw?
1 P MoveTo 2 P~Shape?3 P那么Q不这些方法在Circle中是按照什么顺序定义的QCircle的VTable中都必须保证Draw?
1 PMoveTo?2受至?3Pq里是~Circle。ؓ(f)什么不是~Shape啊?嘿嘿Q忘啦,析构函数不会(x)l承的?br>
3、vptr是由~译器自动插入生成的Q因此编译器必须负责为其q行初始化。初始化的时间选在对象创徏Ӟ而地点就在构造函C。因此,~译器必M证每个类臛_有一个构造函敎ͼ若没有,自动为其生成一个默认构造函数?br>
4、vptr通常攑֜对象的v始处Q也是Addr(obj) == Addr(obj.vptr)?br>
你看Q天下果然没有免费的午餐Qؓ(f)?jin)实现动态绑定,~译器要为我们默默干?jin)这么多的脏话篏zR如果你想体验一下编译器的辛劻I那么可以试用C语言模拟一下上面的行ؓ(f)Q?】中有q么一个例子。好?jin),现在万事具备Q只Ơ东风了(jin)。编译,q接Q蝲入,GOQ当E序执行?
pShape->Draw()的时候,上面的设施也开始v作用?jin)。?br>
前面已经提到Q晚期绑定时之所以不能确定调用哪个函敎ͼ是因为具体的对象不确定。好?jin),当运行?strong>pShape->Draw()Ӟ对象出来?jin),它由pShape指针标出。我们找到这个对象后Q就可以扑ֈ它里面的vptrQ在对象的v始处Q,有了(jin)vptr后,我们找C(jin)VTableQ调用的函数在眼前?jin)。。等{,VTable中方法那么多Q我I竟使用哪个呢?不用着急,~译器早已ؓ(f)我们做好?jin)记录?x)~译器在创徏VTableӞ已经为每个virtual函数安排好了(jin)座次Qƈ且把q个索引可录了(jin)下来。因此,当编译器解析?strong>pShape->Draw()的时候,它已l?zhn)?zhn)的函数的名字用烦(ch)引号来代替了(jin)。这时候,我们通过q个索引号就可以在VTable中得C个函数地址QCall
itQ?br>
在这里,我们׃?x)到Z么会(x)有第二条规定?jin),通常Q我们都是用基类的指针来引用zcȝ对象Q但是不具体对象是哪个zcȝQ我们都可以使用相同的烦(ch)引号来取得对应的函数实现?br>
现实中有一个例子其实跟q个蛮像的:(x)报警?sh)话?10Q?19Q?20QVTable中不同的Ҏ(gu)Q。不同地方的人拨打不同的L(fng)所产生的结果都是不一L(fng)。譬如,在三环外的一个hQ具体对象)(j)跟一环内的一个hQ另外一个具体对象)(j)?19Q最后调用的消防队肯定是不一L(fng)Q这是多态了(jin)。这是怎么实现的呢Q每个h都知道一个报警中?j)(VTableQ里面有三个Ҏ(gu)
110Q?19Q?20Q。如果三环外的一个h需要火警抢险(一个具体对象)(j)Ӟ它就拨打119Q但是他肯定不知道最后是哪一个消防队?x)出现的。这得有报警中心(j)来决定,报警中心(j)通过q个具体对象Q例子中是具体位置?jin)?j)以及(qing)他说拨打的电(sh)话号码(可以理解成烦(ch)引号Q,报警中心(j)可以定应该调度哪一个消防队q行抢险Q不同的动作Q?br>
q样Q通过vptr和VTable的帮助,我们实C(jin)C++的动态绑定。当?dng)q仅仅是单承时的情况,多重l承的处理要相对复杂一点,下面要说一下最单的多重l承的情况,至于虚承的情况Q有兴趣的朋友可以看?
Lippman的《Inside the C++ Object
Model》,q里暂时׃展开?jin)。(主要是自p没搞清楚Q况且现在多重扉K不怎么使用?jin),虚承应用的Z(x)更了(jin)Q?br>
首先Q我要先说一下多重承下对象的内存布局Q也是说该对象是如何存放本w的数据的?/p>
在上面这个例子中Q一个Dog对象在内存中的布局如下所C:(x)
Dog |
Vptr1 |
Cute::i |
Vptr2 |
Pet::j |
Dog::z |
好了(jin)Q既然编译器帮我们自动完成了(jin)不同父类的地址转换Q我们调用虚函数的过E也p单承统一h?jin)?x)通过具体对象Q找到vptrQ通常指针的v始位|,因此Cute扑ֈ的是vptr1Q而Pet扑ֈ的是vptr2Q,通过vptrQ我们找到VTableQ然后根据编译时得到的VTable索引P我们取得相应的函数地址Q接着可以马上调用了(jin)?br>
在这里,Z也提一下两个特D的Ҏ(gu)在多态中的特别之处吧Q第一个是构造函敎ͼ在构造函C调用虚函数是不会(x)有多态行为的Q例子如下:(x)
W二个就是析构函敎ͼ使用多态的时候,我们l常使用基类的指针来引用zcȝ对象Q如果是动态创建的Q对象用完后,我们使用delete来释攑֯象。但是,如果我们不注意的话,?x)有意想不到的情况发生?/p>
所以,如果一个类设计用来被承的话,那么它的析构函数应该被声明ؓ(f)virtual的?/p>