多态是C++中的一个重要特性,而虚函数却是实现多态的基石。所谓多态,就是基类的引用或者指针可以根据其实际指向的子类类型而表现出不同的功能。这篇文章讨论这种功能的实现原理,注意这里并不以某个具体的编译器为参照。

1、虚函数表的构造

  1. class A
  2. {
  3. public:
  4. int data;
  5.  
  6. virtual void foo_0(){}
  7. virtual ~A(){}
  8. };
  9.  
  10. class B : public A
  11. {
  12. public:
  13. virtual void foo_0(){}
  14. virtual void foo_1(){}
  15. };

编译器会为存在虚函数的类生成一个虚函数表,并且会在该类中安插一个新成员:指向相应虚函数表的指针,简称vptr,接着会在该类的构造函数中插入初始化vptr的代码,使vptr指向自己的虚函数表。例如,上面的A类和B类分别对应于一个虚函数表,其结构如下:

需要注意的是,一个继承链中相同的虚函数在各个类的虚函数表中应该具有相同的索引,这是实现虚函数的根本,如上面的foo_0都放在索引0的位置上,析构函数都放在索引为1的位置上。

2、指针调整和动态绑定

  1. void func(A *pA)
  2. {
  3. pA->foo_0();
  4. }

看看这个函数,pA可以指向A类对象也可以指向B类对象,那编译器知道pA->foo_0()应该调用哪一个类中的foo_0()吗?答案是不知道,因为只有到运行时才知晓pA具体指向A还是B的对象;不过编译器通过虚函数表机制总可以调用到正确的foo_0()函数,即如果pA指向A类型的对象,那它就调用A中的foo_0(),若pA指向B类型的对象,那就调用B中的foo_0(),这种机制称作动态绑定;不过pA->foo_0()只是个函数调用,表面上看跟虚函数表并没有什么关系,但它会被编译器改造成下面这个样子:

  1. (*pA->vptr[0])(pA);

vptr是编译器安插的指向虚函数表的指针成员,另外传递了当前对象的指针到虚函数中。这样改造之后,就能实现动态绑定了,因为类A和类B中的foo_0()都被存放在各自虚函数表索引0处。
现在假设有这样的调用:

  1. B *pB = new B;
  2. func(pB);

因为func需要的是一个A类型的指针,而传进去的是B*,所以编译器首先需要进行指针调整,像下面这样:

  1. B *pB = new B;
  2. A *pA = pointer_adjust(pB);
  3. func(pA);

其语义是使得传递到func()中的指针确实指向一个A类型的对象,或者子类中的A类成份;其原因是,在func()中可能使用pA访问A类中的数据成员,如data或者vptr成员;另一方面,如果在func()中调用虚函数,传递到相应虚函数的对象指针(this)又需要指向实际的对象,所以可能再次调整指针,对于前面虚函数调用的改造,即:(*pA->vptr[0])(pA),在单继承下可以工作得很好,因为pA总是可以指到正确的位置上,不论传递进去的是A类型的指针还是B类型的指针,但是对于多继承和虚拟继承,情况就不一样了。详见下一节。

3、多重继承下虚函数调用时的this指针调整

  1. class A
  2. {
  3. public:
  4. int data;
  5.  
  6. virtual void foo_0(){}
  7. virtual ~A(){}
  8. };
  9.  
  10. class B
  11. {
  12. public:
  13. int data0;
  14.  
  15. virtual void foo_0(){}
  16. virtual ~B(){}
  17. };
  18.  
  19. class C : public A, public B
  20. {
  21. public:
  22. virtual void foo_0(){}
  23. virtual void foo_1(){}
  24. };

现在继承结构改成上面这样子,然后有下面的虚函数调用:

  1. C *pC = new C;
  2. A *pA = pC;
  3. B *pB = pC;
  4.  
  5. pA->foo_0();
  6. pB->foo_0();

如果按照第2节所讲的虚函数调用改造方法,它们会改造成下面这样:

  1. (*pA->vptr[0])(pA) .... (1)
  2. (*pB->vptr[0])(pB) .... (2)

对于(1)没有问题,因为pA和pC都指向C的首部,(2)则不然,因为类B处在继承声明中第二的位置上,那么pB会指向C的中部,也就是离首部有一个偏移,所以必须要调整。Bjarne的解决方法是,将虚函数表扩大,使得每个条目是虚函数指针以及相应this指针偏移的聚合。然后对于虚函数调用,像下面这样改造:

  1. (*pA->vptr[0].faddr)(pA+pA->vptr[0].offset) .... (1)
  2. (*pB->vptr[0].faddr)(pB+pB->vptr[0].offset) .... (2)

不过这样对于不需要调整this指针的类也需要背负着更大的虚函数表空间和相应的时间开销,而且在大多数情况不需要调整,毕竟单继承用得更多。更有效率的解决方法是利用thunk,thunk技术是由高德纳(knuth)发明的,thunk就是一小段汇编代码,功能是调整this指针,然后跳转到相应的虚函数中执行,比如通过pB调用foo_0()的thunk像下面这样:

  1. thunk_foo_0:
  2. this -= sizeof(A);
  3. C::foo_0(this)

这样对于需要调整this指针的虚函数,虚函数表中存放的是相应的thunk地址,而对于不需要调整this指针的虚函数,只需存放该函数本身的地址,就没有额外的时间和空间开销,微软的C++编译器就用到了thunk。虚拟继承时的处理跟多继承差不多,就不重复描述了。

【高级】C++中虚函数机制的实现原理的更多相关文章

  1. C++中虚函数功能的实现机制

    要理解C++中虚函数是如何工作的,需要回答四个问题. 1.  什么是虚函数. 虚函数由于必须是在类中声明的函数,因此又称为虚方法.所有以virtual修饰符开始的成员函数都成为虚方法.此时注意是vir ...

  2. C++中对C的扩展学习新增内容———面向对象(继承)函数扩展性及虚函数机制

    1.c语言中的多态,动态绑定和静态绑定 void do_speak(void(*speak)()) { speak(); } void pig_speak() { cout << &quo ...

  3. 匹夫细说C#:从园友留言到动手实现C#虚函数机制

    前言 上一篇文章匹夫通过CIL代码简析了一下C#函数调用的话题.虽然点击进来的童鞋并不如匹夫预料的那么多,但也还是有一些挺有质量的来自园友的回复.这不,就有一个园友提出了这样一个代码,这段代码如果被编 ...

  4. 关于C++与Java中虚函数问题的读书笔记

    之前一直用C++编程,对虚函数还是一些较为肤浅的理解.可近期由于某些原因搞了下Java,发现有些知识点不熟,于是站在先驱巨人的肩上谈谈C++与Java中虚函数问题. Java中的虚函数 以下是段别人的 ...

  5. c++中虚函数和多态性

    1.直接看下列代码: #include <iostream> using namespace std; class base{ public: void who(){ cout<&l ...

  6. [C/C++] 虚函数机制

    转自:c++ 虚函数的实现机制:笔记 1.c++实现多态的方法 其实很多人都知道,虚函数在c++中的实现机制就是用虚表和虚指针,但是具体是怎样的呢?从more effecive c++其中一篇文章里面 ...

  7. 浅谈C++虚函数机制

    0.前言 在后端面试中语言特性的掌握直接决定面试成败,C++语言一直在增加很多新特性来提高使用者的便利性,但是每种特性都有复杂的背后实现,充分理解实现原理和设计原因,才能更好地掌握这种新特性. 只要出 ...

  8. C++中虚函数的作用和虚函数的工作原理

    1 C++中虚函数的作用和多态 虚函数: 实现类的多态性 关键字:虚函数:虚函数的作用:多态性:多态公有继承:动态联编 C++中的虚函数的作用主要是实现了多态的机制.基类定义虚函数,子类可以重写该函数 ...

  9. C++中虚函数的作用浅析

    虚函数联系到多态,多态联系到继承.所以本文中都是在继承层次上做文章.没了继承,什么都没得谈. 下面是对C++的虚函数这玩意儿的理解. 一, 什么是虚函数(如果不知道虚函数为何物,但有急切的想知道,那你 ...

随机推荐

  1. error1

     #include<stdio.h>main(){ int a[10],i,m,n,j;   for(i=3;i<10;i++)    scanf("%d",&a ...

  2. Android R.layout. 找不到已存在的布局文件

    今天写新页面的时候,突然发现R.layout.  无法找到我已经写好的页面,于是顿时就不淡定了. 把R文件翻了一遍  发现也没有.... 然后我就看到了这个. android.R 原来是我错把Andr ...

  3. 我定制的jquery ui主题

    打开网址 http://jqueryui.com/themeroller/,找到Gallery找到Redmond点击edit 将圆角设置成3px,让圆角更低调:将下面的每个Background的背景图 ...

  4. Android应用程序架构之res

    res/drawable 专门存放png.jpg等图标文件.在代码中使用getResources().getDrawable(resourceId)获取该目录下的资源. res/layout 专门存放 ...

  5. 重写javascript浮点运算

    javascript中变量存储时不区分number和float类型,同一按照float存储; javascript使用IEEE 754-2008标准定义的64bit浮点格式存储number,decim ...

  6. ubuntu远程windows服务器

    ubuntu端: sudo apt-get install rdesktop windows端: 需要允许此windows远程访问.我的windows是windows server2012,基本操作: ...

  7. Stream与byte转换

    将 Stream 转成 byte[] /// <summary> /// 将 Stream 转成 byte[] /// </summary> public byte[] Str ...

  8. CentOS下重新安装yum

    1,下载最新的yum-3.2.28.tar.gz并解压 #wget http://yum.baseurl.org/download/3.2/yum-3.2.28.tar.gz#tar xvf yum- ...

  9. Oracle EBS-SQL (BOM-6):检查物料失效但BOM中未失效的数据.sql

    select msi.segment1                   装配件编码 , msi.description                  装配件描述 , msi.item_type ...

  10. Oracle EBS-SQL (SYS-3):sys_人员用户名对应关系查询.sql

    select fu.user_name 用户名,       fu.description 描述,       (select ppf.FULL_NAME          from per_peop ...