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