理会动态联编(下篇)
副标题#e#
三 虚函数表VTABLE
动态联编进程跟我们揣摩的大抵沟通。编译器在执 行进程中碰着virtual要害字的时候,将自动安装动态联编需要的机制,首先为这 些包括virtual函数的类(留意不是类的实例)–纵然是祖先类包括虚函数而自己 没有–成立一张虚拟函数表VTABLE。在这些虚拟函数表中,编译器将依次凭据函 数声明序次安排类的特定虚函数的地点。同时在每个带有虚函数的类中安排一个 称之为vpointer的指针,简称vptr,这个指针指向这个类的VTABLE。
关于 虚拟函数表,有几点必需声明清楚:
1. 每一个种别只能有一个虚拟函数 表,假如该类没有虚拟函数,则不存在虚拟函数表。
2. C++编译时候编译 器会在含有虚函数的类中加上一个指向虚拟函数表的指针vptr。
3. 从一 个种别降生的每一个工具,将获取该种别中的vptr指针,这个指针同样指向类的 VTABLE。
因此类、工具、VTABLE的条理布局可以用下图暗示。个中X类和Y 类的工具的指针 都指向了X,Y的虚拟函数表,同时X,Y类自身也包括了指向虚拟函 数的指针。
为了方 便问题说明,我们将2.cpp例子举办扩展,扩展措施如下。//4.cpp
15. #include <iostream.h >
16. class shape{
17. public:
18. virtual void draw(){cout<<"shape::draw ()"<<endl;}
19. virtual void area() {cout<<"shape::area()"<<endl;}
20. void fun(){draw();area();}
21. };
22. class circle:public shape {
23. public:
24. void draw() {cout<<"circle::draw()"<<endl;}
25. void adjust(){cout<<"circle::adjust()"<<endl;}
26. };
27. main(){
28. shape oneshape;
29. oneshape.fun();
30.
31. circle circleshape;
32. shape& baseshape=circleshape;
33. baseshape.fun();
34. }
编译器在编译上面这段代码的时候将为这shape和circle两个工具 别离成立一个VTABLE表,这些表依次填充派生类工具和基类工具中声明的所有的 虚函数地点。假如派生类自己没有从头界说基类的虚函数,那么填充的就是基类 的虚函数地点。这样一旦假如函数挪用一个派生类不存在的要领时候可以或许自动调 用基类要领。然后编译器在每个类中安排一个vptr,一般置于工具的起始位置, 继而在工具的结构函数中将vptr初始化为本类的VTABLE的地点。整个功效机关如 下。
图一
#p#副标题#e#
图一中的rectangle的VTABLE中的area() 和triangle的VTABLE的adjust() 都是填充的基类的虚函数地点。 C++ 编译措施时候按下面的步调举办工 作:
①为种种成立虚拟函数表,假如没有虚函数则不成立。
②临时 不毗连虚函数,而是将各个虚函数的地点放入虚拟函数表中。
③直接毗连 各静态函数。
这些事情做完之后,模块图如图二:
图二
执行时候,降生了oneshape和circleshape两个工具,oneshape工具的 vptr指针指向shape的VTABLE,circleshape工具的vptr指针指向circleshape的 VTABLE,在执行oneshape.fun()的时候,fun函数的this指针指向了oneshape工具 ,进入fun()之后措施继承执行this->draw(),由于this指向oneshape工具, oneshape的vptr又指向shape类的VTABLE,这样就从VTABLE中获得需要绑定的函数 的地点,并毗连起来。同样,this-> area()也经过oneshape工具而毗连到相 应的函数上,如图三。
图三
此刻我们执行baseshape.fun()函数。
circle circleshape;
shape& baseshape=circleshape;
baseshape.fun();
函数进入fun函数之后,函数的this指针将指向 basefun工具,另一方面basefun指向一个circleshape,因此this指针指向的实际 上为circleshape工具,而circleshape的vptr指针指向circle类的虚拟函数表, 这样编译器将从虚拟表中取出circle::draw()和circle::area()的地点,举办连 接。因为circle自己没有从头界说area()要领,因此编译器利用shape的area()方 法。如图四。
图四
遵循上面的思路,基于基类的指针总能找到正确的子类工具的实现。可是 象上面的 this->draw是怎么编译的呢。
四 编译黑幕
在上面的 措施中,this指针差异,从而毗连到差异的fun函数。那么C++如何编译这些指令 呢。原理在于:所有的基类的派生类的虚拟函数表的顺序与基类的顺序是一样的 ,对付基类中不存在要领再凭据声明序次举办排放。这样不管是shape照旧circle 可能从shape又担任出来的其余的类它们的虚拟函数表的第一项老是draw函数的地 址,然后是area的地点。对付circle类,下面的才是adjust的地点。因此不管对 于shape照旧circle,this->draw老是编译成 call this->VTABLE[0]; this->area()老是翻译成 call this->VTABLE[1]; 措施到真正运行时候将 会发明this的真正指向的工具,假如是shape,则挪用shape->VTABLE[0],假如 是circle,则挪用circle->VTABLE[1],如图五。
图五
请看下面的这个例子。
#p#分页标题#e#
35. #include
36. class shape{
37. public:
38. virtual void draw() {cout<<"shape::draw()"<draw();//OK
51. oneshape->adjust();//错误,编译器无法通过
52.
53. circle* circleshape;
54. circleshape->adjust();
55. }
在措施编译期间,由于oneshape为shape范例的,因此它将查抄shape 的虚拟函数表,发明VTABLE[0]为draw函数的地点,于是翻译成p->VTABLE[0] 。将来执行期间,p 实际上指向的是circle工具,因此真正挪用的为circle- >VTABLE[0]处的函数,即circle::draw。同样对付adjust函数,C++ 编译器也 会去查抄shape的VTABLE,功效编译器无法找到adjust函数,因此编译无法通过。 对付circleshape,因为它是circleshape范例的,因此它将会查抄circle的 VTABLE,得知VTABLE[2]处为adjust的地点,因此编译器翻译成call circleshape ->VTABLE[2],真正执行时候circleshape为circle范例,因此它将绑定circle 的VTABLE[2]处的函数即circle:: adjust()。 就这样,编译器借助虚拟函数表实 现了动态联编的进程,从而使多态的实现有了大概。因此说虚拟函数表是多态性 的幕后元勋一点也不为过。
五 竣事语
多态性的实现是一个很是复 杂的进程,上面的接头仅仅是针对简朴担任而言,即基类只有一个的环境,对付 多重担任,环境又会有所改变。本文仅是抛砖引玉,但愿有乐趣的伴侣可以一起 探讨。