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

1、虚函数表的构造

class A
{
public:
int data; virtual void foo_0(){}
virtual ~A(){}
}; class B : public A
{
public:
virtual void foo_0(){}
virtual void foo_1(){}
};

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

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

2、指针调整和动态绑定

void func(A *pA)
{
pA->foo_0();
}

看看这个函数,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()只是个函数调用,表面上看跟虚函数表并没有什么关系,但它会被编译器改造成下面这个样子:

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

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

B *pB = new B;
func(pB);

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

B *pB = new B;
A *pA = pointer_adjust(pB);
func(pA);

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

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

class A
{
public:
int data; virtual void foo_0(){}
virtual ~A(){}
}; class B
{
public:
int data0; virtual void foo_0(){}
virtual ~B(){}
}; class C : public A, public B
{
public:
virtual void foo_0(){}
virtual void foo_1(){}
};

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

C *pC = new C;
A *pA = pC;
B *pB = pC; pA->foo_0();
pB->foo_0();

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

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

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

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

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

thunk_foo_0:
this -= sizeof(A);
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. struts2 + jquery 开发环境下的ajax构建方法(action写法 + struts.xml配置 + js调用代码)

    1.action写法 public class RegisterAction extends ActionSupport { private InputStream inputStream; /** ...

  2. struts中简单的校验

    Struts中简单的校验 “计应134(实验班) 凌豪” Struts2校验简要说明:struts2中通常情况下,类型转换要在数据校验之前进行.类型转换其实也是基本的服务器端校验,合法数据必然可以通过 ...

  3. 5.java.lang.IndexOutOfBoundsException(数组下标越界异常)

    数组下标越界异常 查看调用的数组或者字符串的下标值是不是超出了数组的范围,一般来说,显示(即直接用常数当下标)调用不太容易出这样的错,但隐式(即用变量表示下标)调用就经常出错了,还有一种情况,是程序中 ...

  4. sketch 跟随鼠标指针移动的特效

    演示地址:http://www.ke01.com/yanshi/sucai/20140830/2/ 下载地址:https://yunpan.cn/cqgWeIYPer8eC  访问密码 672b

  5. 转 批处理 %~dp0的意义

    http://nealcai.iteye.com/blog/1685192 http://blog.csdn.net/caz28/article/details/7448677 http://stac ...

  6. (7) 引用Objective-C class library

    原文 引用Objective-C class library 这个范例是如何在Xamarin.ios中去使用一个我们自行在Xcode中开发的Objective-c Class Library. 主要会 ...

  7. 如何在XML 加入特殊字符内容 如< >

    XML 文件本身包含了一些预定义的保留字符 如< 标记元素的开始符号等 如果要在属性或者元素的值里面包含类似的这些特殊字符 应该如何处理呢 ? 这时候要用到  <![CDATA[] 这个标 ...

  8. select option 下拉多选单选bootstrap插件使用总结

    <select id="example-getting-started" multiple="multiple"> <option value ...

  9. codeigniter使用mongodb/redis

    ci2.x版本,使用mongodb,需要安装pecl-php-mongo扩展(github上很多扩展已不可用,找到个可用版本纪录于此),添加到php.ini中 使用如下 public function ...

  10. 获取第上一个兄弟元素 屏蔽浏览器的差异(PreviousElementSibling)

    //获取element上一个兄弟元素 function getPreviousElementSibling(element){ //能力检测 判断是否支持PreviousElementSibling ...