多态是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. C语言学习——C程序的运行机理

    预处理: #include<xxx> 尖括号表示库文件:#include"xxx" 双引号表示自己写的文件. #include后面的文件格式允许多种,但若要是" ...

  2. python日志记录-logging模块

    1.logging模块日志级别 使用logging模块简单示例: >>>import logging >>>logging.debug("this's a ...

  3. 黑马程序员-- C语言变量作用域与代码块

    这里通过变量作用域的两种错误用法来介绍一下C语言变量作用域 其次对代码块的使用进行了简单说明: #include <stdio.h> 1.变量的作用域(作用范围) 变量定义的那一行开始,直 ...

  4. MarkWord

    MarkWord - 可发布博客的 Markdown编辑器 代码开源 1 /// <summary> 2 /// 同步呈现 3 /// </summary> 4 /// < ...

  5. Red Hat Enterprise Linux Release Dates

    Red Hat Enterprise Linux Release Dates UpdatedMay 10 2016 at 10:57 PM - English The tables below lis ...

  6. ThinkPHP中使用ajax接收json数据的方法

    本文实例讲述了ThinkPHP中使用ajax接收json数据的方法.分享给大家供大家参考.具体分析如下: 这里通过ThinkPHP+jquery实现ajax,扩展了下,写了个查询,前台代码如下: 首先 ...

  7. Javascript面向对象之创建对象

    面向对象的语言具有一个共同的标志,那就是具有“类”的概念,但是在javascript中没有类的概念,在js中将对象定义为“无序属性的集合,其属性可以包含基本值,对象或者函数”,即其将对象看作是一组名值 ...

  8. linux win7双系统

    真恨我自己啊,刚在linux下写了这个博客,因为没有分类,添加了个linux分类.按了F5刷没了.靠,哪里有心情复述啊 一直想装直接装linux系统,现在实现他,以后也要跟上linux的笔记,不然都对 ...

  9. erlang supervisor说明

    Supervisor Behaviour是一个用来实现一个supervisor进程来监控其他子进程的模块 子进程可以是另一个supervisor,也可以是一个worker进程. worker进程一般使 ...

  10. android 设置头像以及裁剪功能

    在android的开发过程中,经常遇到设置用户头像以及裁剪图像大小的功能.昨天我遇到了设置用户头像的功能,开始不知道怎么搞,在技术群里问也没人回 答,就研究了微信用户设置头像的功能,了解到用户设置图像 ...