参考:http://www.yesky.com/385/1785885.shtml http://www.bbsshow.net/dispbbs.asp? boardid=79&id=474 http://dev.yesky.com/448/2211448.shtml http://www.vckbase.com/vckbase/vckbase12/vc/nonctrls/c++mfc_04/1204002.htm
BEN注:
动态联编(迟绑定):一个接口,多种实现.
1、编辑器在编译阶段为每一个包含虚函数的类(及其派生类)创建虚函数表,并在构造生成对象时往对象中插入虚指针指向该表。
vptr -> vTable
2、运行的时候,虚函数的参数表会隐式地加入this指针对象,不同对象调用,this指向不同的对象。
fun (this, ...); this -> ClassInstance;
3、通过指向派生类的基类引用或基类指针,其this指针将指向派生类对象。
this -> BaseInstance_Ptr/Ref -> DeprivedInstance
4、再通过各类的虚函数表加上偏移量调用正确的虚函数。
(... -> DeprivedInstance).vptr -> vTable + offset
以前的想法:当编译器发现类含有虚函数,创建虚函数表vtable#1,同时在类当中插入一个指针成员vptr(指向vtable)。当类进行实例化创建对象时,在栈中产生指向堆区实例的this指针,在堆区自然会有其vptr成员。当使用this指针调用类的成员函数时,发生如下链式调用,this -> vptr -> vtable + offset -> member function。当使用指向派生类对象的基类指针时,会经过动态转换(dynamic_cast)到派生类的this指针。
#1:vtable是一个指针数组,指向该类包括的虚成员函数以及继承下来的虚函数,对非虚成员函数,则在编译时直接指定函数地址,进行静态(先期)联编。
发现了更好的文章:
文章摘要 多态性是C++最主要的特征,多态性的实现得益于C++中的动态联编技术。文章通过对动态联编的关键技术虚拟函数表进行深入的剖析,解析的动态联编的过程极其技术要领。 关键字 多态性 动态联编 VTABLE 虚函数 文章正文 一 从多态性谈动态联编的必要性 在进入主题之前先介绍一下联编的概念。联编就是将模块或者函数合并在一起生成可 执行代码的处理过程,同时对每个模块或者函数调用分配内存地址,并且对外部访问也分配正确的内存地址。按照联编所进行的阶段不同,可分为两种不同的联编方法:静态联编和动态联编。在编译阶段就将函数实现和函数调用关联起来称之为静态联编,静态联编在编译阶段就必须了解所有的函数或模块执行所需要检测的信息,它对函数的选择是基于指向对象的指针(或者引用)的类型。反之在程序执行的时候才进行这种关联称之为动态联编,动态联编对成员函数的选择不是基于指针或者引用,而是基于对象类型,不同的对象类型将做出不同的编译结果。C语言中,所有的联编都是静态联编。C++中一般情况下联编也是静态联编,但是一旦涉及到多态性和虚函数就必须使用动态联编。 多态性是面向对象的核心,它的最主要的思想就是可以采用多种形式的能力,通过一个用户名字或者用户接口完成不同的实现。通常多态性被简单的描述为"一个接口,多个实现。在C++里面具体的表现为通过基类指针访问派生类的函数和方法。 下面我们看一个静态联编的例子,这种静态联编导致了我们不希望的结果。 //1.cpp 1. #include <iostream.h> 2. class shape{ 3. public: 4. void draw(){cout<<"I am shape"<<endl;} 5. void fun(){draw();} 6. }; 7. class circle:public shape{ 8. public: 9. void draw(){cout<<"I am circle"<<endl;} 10. }; 11. main(){ 12. class circle oneshape; 13. oneshape.fun(); 14. }程序的输出结果我们希望是"I am circle",但事实上却输出了"I am shape"的结果,造成这个结果的原因是静态联编。静态联编需要在编译时候就确定函数的实现,但事实上编译器在仅仅知道shape的地址时候无法获取正确的调用函数,它所知道的仅是shape::draw(),最终结果只能是draw操作束缚到shape类上。产生"I am shape"的结果就不足为奇了。 为了能够引起动态联编,我们只需要将需要动态联编的函数声明为虚函数即可。动态联编只对虚函数起作用。我们在通过基类而且只有通过基类访问派生类的时候,只要这个基类中直接的或者间接(从上上层继承)的包含虚函数,动态联编将自动唤醒。下面我们将上面的程序稍微改一下。 //2.cpp 1. #include <iostream.h> 2. class shape{ 3. public: 4. virtual void draw(){cout<<"I am shape"<<endl;} 5. void fun(){draw();} 6. }; 7. class circle:public shape{ 8. public: 9. void draw(){cout<<"I am circle"<<endl;} 10. }; 11. main(){ 12. class circle oneshape; 13. fun(&oneshape); 14. }程序执行得到了正确的结果"I am circle"。代码在VC6.0中执行。 到目前为止我们不清楚动态联编的执行机制,但我们可以做个猜测。正如上面所说,对于函数的实际的对象类型不同,联编结果也应该不同。在静态联编中,执行的困难在于无法通过基类知道需要联编的子对象的确切类型。在1.cpp中shape的派生类既可能是circle,也可能是其余的rectangle或者square等等,到底应该静态联编哪一个呢。迷惑正在于此。动态联编在编译的时候应该也是不知道联编的确切对象类型的,(如果知道的话就成了静态联编了),因此它只能通过一定的机制,使得在执行时候能够找到和调用正确的函数体。可以想象,为了达到这个目的,一些相关信息应该封装在对象自身中。这些信息有点象身份证明,标识自己,这样在动态联编的时候,编译器可以根据这些标记找到相应的函数体,"不要跑,就是你了"。 实际上的动态联编过程是什么样的呢。 二 对象类型信息 为了证明我们的猜想,我们用下面的一个程序进行测试,下面的程序将获取普通的类和包含虚函数的类的字节大小。程序代码如下。 //3.cpp 1. #include <iostream.h> 2. class shape_novirtual{ 3. int a; 4. public: 5. void draw(){cout<<"shape_novirtual::draw()"<<endl;} 6. }; 7. class shape_virtual1{ 8. int a; 9. public: 10. virtual void draw(){cout<<"shape_virtual::draw()"<<endl;} 11. }; 12. class shape_virtual2{ 13. int a; 14. public: 15. virtual void draw(){cout<<"shape_virtual2::draw()"<<endl;} 16. virtual void draw1(){cout<<"shape_virtual2::draw1()"<vendl;} 17. }; 18. main(){ 19. cout<<"sizeof(int)"<<sizeof(int)<<endl; 20. cout<<"sizeof(class shape_novirtual):"<<sizeof(shape_novirtual)<<endl; 21. cout<<"sizeof(void*):"<<sizeof(void*)<<endl; 22. cout<<"sizeof(class shape_virtual):"<<sizeof(shape_virtual)<<endl; 23. cout<<"sizeof(class shape_virtual2):"<<sizeof(shape_virtual2)<<endl; 24. }VC6.0中运行结果如下: sizeof(int)4 sizeof(class shape_novirtual):4 sizeof(void*):4 sizeof(class shape_virtual1):8 sizeof(class shape_virtual2):8 Press any key to continue从上面可以看出,没有虚函数的类shape_novirtual的大小为4,正好为int a的大小。而带有虚函数的类shape_virtual1和shape_virtual2的大小除了int a的大小还多出了4格个字节的大小,这个大小正好是void*指针的大小。到现在为止我们基本上可以说带有虚函数的对象自身确实插入了一些指针信息,而且这个指针信息并不随着虚函数的增加而增大。 如果我们将每个类的成员变量int a去掉,VC6.0运行结果就会变成下面的情况。 sizeof(int)4 sizeof(class shape_novirtual):1 sizeof(void*):4 sizeof(class shape_virtual1):4 sizeof(class shape_virtual2):4 Press any key to continue 上面的运行结果应该让人感到例外。既然size(int)为4,现在没有了这个成员变量,类shape_novirtual应该字节大小为0,但事实上C++编译器不允许对象为零长度。试想一个长度为0的对象在内存中怎么存放?怎么获取它的地址?为了避免这种情况,C++强制给这种类插入一个缺省成员,长度为1。如果有自定义的变量,变量将取代这个缺省成员。 三 虚函数表VTABLE //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的地址。整个结果布局如下。 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()方法。如图四。 35. #include <iostream.h> 36. class shape{ 37. public: 38. virtual void draw(){cout<<"shape::draw()"<<endl;} 39. virtual void area(){cout<<"shape::area()"<<endl;} 40. void fun(){draw();area();} 41. }; 42. class circle:public shape{ 43. public: 44. void draw(){cout<<"circle::draw()"<<endl;} 45. void adjust(){cout<<"circle::adjust()"<<endl;} 46. }; 47. main(){ 48. shape* oneshape; 49. oneshape=new circle; 50. oneshape->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()。 就这样,编译器借助虚拟函数表实现了动态联编的过程,从而使多态的实现有了可能。因此说虚拟函数表是多态性的幕后功臣一点也不为过。 |
以前的一篇
第一篇 ~ 阐述
面向对象程序设计的基本观点是用程式来仿真大千世界,这使得它的各种根本特性非常人性化,如封装、继承、多态等等,而虚数就是c++中实现多态性的主将。为了实现多态性,c++编译器也革命性地提供了动态联编(或叫晚捆绑)这一特征。
虚拟函数亦是mfc编程的关键所在,mfc编程主要有两种方法:一是响应各种消息,进行对应的消息处理。二就是重载并改写虚拟函数,来实现自己的某些要求或改变系统的某些默认处理。
虚函数的地位是如此的重要,对它进行穷根究底,力求能知其然并知其所以然 对我们编程能力的提高大有好处。下面且听我道来。
多态性和动态联编的实现过程分析
一、基础略(限于篇幅,请参阅相应的c++书籍):
1、多态性:使用基础类的指针动态调用其派生类中函数的特性。
2、动态联编:在运行阶段,才将函数的调用与对应的函数体进行连接的方式,又叫运行时联编或晚捆绑。
二、过程描述:
1、编译器发现一个类中有虚函数,编译器会立即为此类生成虚拟函数表 vtable(后面有对vtable的分析)。虚拟函数表的各表项为指向对应虚拟函数的指针。
2、编译器在此类中隐含插入一个指针vptr(对vc编译器来说,它插在类的第一个位置上)。
有一个办法可以让你感知这个隐含指针的存在,虽然你不能在类中直接看到它,但你可以比较一下含有虚拟函数时的类的尺寸和没有虚拟函数时的类的尺寸,你能够发现,这个指针确实存在。
class cnovirtualfun
{
private:
long lmember;
public:
long getmembervalue();
}
class chavevirtualfun
{
private:
long lmember;
public:
virtual long getmembervalue();
}
cnovirtualfun obj;
sizeof(obj) -> == 4;
chavevirtualfun obj;
sizeof(obj) -> == 8;
3、在调用此类的构造函数时,在类的构造函数中,编译器会隐含执行vptr与vtable的关联代码,将vptr指向对应的vtable。这就将类与此类的vtable联系了起来。
4、在调用类的构造函数时,指向基础类的指针此时已经变成指向具体的类的this指针,这样依靠此this指针即可得到正确的vtable,从而实现了多态性。在此时才能真正与函数体进行连接,这就是动态联编。
三、vtable 分析:
分析1:虚拟函数表包含此类及其父类的所有虚拟函数的地址。如果它没有重载父类的虚拟函数,vtable中对应表项指向其父类的此函数。反之,指向重载后的此函数。(本文作者注:后面有详细示例)
分析2:虚拟函数被继承后仍旧是虚拟函数,虚拟函数非常严格地按出现的顺序在 vtable 中排序,所以确定的虚拟函数对应 vtable 中一个固定的位置n,n是一个在编译时就确定的常量。所以,使用vptr加上对应的n,就可得到对应函数的入口地址。(本文作者注:所谓的“偏移”)
四、编译器调用虚拟函数的汇编码(参考think in c++):
push funparam ;先将函数参数压栈
push si ;将this指针压栈,以确保在当前类上操作
mov bx,word ptr[si] ;因为vc++编译器将vptr放在类的第一个位置上,所以bx内为vptr
call word ptr[bx+n] ;调用虚拟函数。n = 所调用的虚拟函数在对应 vtable 中的位置
纯虚函数:
一、引入原因:
1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual returntype function()= 0;),则编译器要求在派生类中必须予以重载以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。
二、纯虚函数实质:
1、类中含有纯虚函数则它的vtable表不完全,有一个空位,所以,不能生成对象(编译器绝对不允许有调用一个不存在函数的可能)。在它的派生类中,除非重载这个函数,否则,此派生类的vtable表亦不完整,亦不能生成对象,即它也成为一个纯虚基类。
虚函数与构造、析构函数:
1、构造函数本身不能是虚拟函数;并且虚机制在构造函数中不起作用(在构造函数中的虚拟函数只会调用它的本地版本)。
想一想,在基类构造函数中使用虚机制,则可能会调用到子类,此时子类尚未生成,有何后果!?。
2、析构函数本身常常要求是虚拟函数;但虚机制在析构函数中不起作用。
若类中使用了虚拟函数,析构函数一定要是虚拟函数,比如使用虚拟机制调用delete,没有虚拟的析构函数,怎能保证delete的是你希望delete的对象。
虚机制也不能在析构函数中生效,因为可能会引起调用已经被delete掉的类的虚拟函数的问题。
对象切片:
向上映射(子类被映射到父类)的时候,会发生子类的vtable 完全变成父类的vtable的情况。这就是对象切片。
原因:向上映射的时候,接口会变窄,而编译器绝对不允许有调用一个不存在函数的可能,所以,子类中新派生的虚拟函数的入口在vtable中会被强行“切”掉,从而出现上述情况。
虚拟函数使用的缺点
优点讲了一大堆,现在谈一下缺点,虚函数最主要的缺点是执行效率较低,看一看虚拟函数引发的多态性的实现过程,你就能体会到其中的原因。
第二篇 ~ 虚函数表示例
基类如果把一个函数声明为虚的(virtual),就表明继承类可以覆盖(override)这个函数(从而表现不同的行为,呈现出多态性)。
对于每一个有虚函数的具体类(或者继承类),可认为有一个与之关联的虚函数表(v-table)。v-table表中的每一项(slot)中存储的是适当的函数指针。C++编译器在编译时刻创建了所有必需的虚函数表。并且,每个虚函数表中的项都已经填充了恰当的值(指向了正确的函数入口)。
例如,下面有三个类:Base,Derived,Derived2。
class Base
{
virtual void f1() {}
virtual void f2() {}
virtual void f3() {}
}
Base类的虚函数表如下。
slot1 – Base::f1()
slot2 – Base::f2()
slot3 – Base::f3()
class Derived: public Base
{
//void f1() {}
void f2() {}
virtual void f3() {}
virtual void f4() {}
virtual void f5() {}
}
Derived类的虚函数表中追加了两项(前面的部分必须与Base类虚函数表完全一致,包括
表项的数量和位置次序),如下。
slot1 – Base::f1()
slot2 – Derived::f2()
slot3 – Derived::f3()
slot4 – Derived::f4()
slot5 – Derived::f5()
注意,我有意在Derived类中不覆盖f1(),这样的话表项1中存储的仍然是指向Base::f1(
)的函数指针。假如在Derived类中覆盖f1()的话,那么表项1将指向Derived::f1()。
class Derived2: public Derived
{
void f3() {}
void f4() {}
void f5() {}
virtual void f6() {}
virtual void f7() {}
}
Derived2类的虚函数表中又增加了两项,如下。
slot1 – Base::f1()
slot2 – Derived::f2()
slot3 – Derived2::f3()
slot4 – Derived2::f4()
slot5 – Derived2::f5()
slot6 – Derived2::f6()
slot7 – Derived2::f7()
现在描述一下对于虚函数的调用。其实已经可以看出,对于虚函数是间接调用的,因为它是通过虚函数表进行的。设想有一个对象指针a,调用a的某个函数f(),如果f()不是虚函数,那么是直接调用的(在确定没有歧义的情况下,没有必要采用间接调用技巧,使效率降低)。如果是虚函数,那么必须是通过虚函数表中的某个项(具体哪个表项是编译阶段完全确定了的),发起调用。为什么不去直接调用呢?因为编译器不能确定这个a指向的对象是Base的实例,还是Derived的实例?要知道就算一个Derived的实例,也可以通过下塑造型(downcast)被视为一个Base实例。 而在Derived类中,函数f()完全可能被覆盖了(从而虚函数表中的相应表项被改写,指向Derived::f())。所以为了产生正确的调用,表现正确的行为,必须通过虚函数表间接调用才行。整个过程有些像变魔术,但也正体现了多态的魅力。
第三篇 ~ 补遗与深化