【高级】C++中虚函数机制的实现原理
多态是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++中虚函数机制的实现原理的更多相关文章
- C++中虚函数功能的实现机制
要理解C++中虚函数是如何工作的,需要回答四个问题. 1. 什么是虚函数. 虚函数由于必须是在类中声明的函数,因此又称为虚方法.所有以virtual修饰符开始的成员函数都成为虚方法.此时注意是vir ...
- C++中对C的扩展学习新增内容———面向对象(继承)函数扩展性及虚函数机制
1.c语言中的多态,动态绑定和静态绑定 void do_speak(void(*speak)()) { speak(); } void pig_speak() { cout << &quo ...
- 匹夫细说C#:从园友留言到动手实现C#虚函数机制
前言 上一篇文章匹夫通过CIL代码简析了一下C#函数调用的话题.虽然点击进来的童鞋并不如匹夫预料的那么多,但也还是有一些挺有质量的来自园友的回复.这不,就有一个园友提出了这样一个代码,这段代码如果被编 ...
- 关于C++与Java中虚函数问题的读书笔记
之前一直用C++编程,对虚函数还是一些较为肤浅的理解.可近期由于某些原因搞了下Java,发现有些知识点不熟,于是站在先驱巨人的肩上谈谈C++与Java中虚函数问题. Java中的虚函数 以下是段别人的 ...
- c++中虚函数和多态性
1.直接看下列代码: #include <iostream> using namespace std; class base{ public: void who(){ cout<&l ...
- [C/C++] 虚函数机制
转自:c++ 虚函数的实现机制:笔记 1.c++实现多态的方法 其实很多人都知道,虚函数在c++中的实现机制就是用虚表和虚指针,但是具体是怎样的呢?从more effecive c++其中一篇文章里面 ...
- 浅谈C++虚函数机制
0.前言 在后端面试中语言特性的掌握直接决定面试成败,C++语言一直在增加很多新特性来提高使用者的便利性,但是每种特性都有复杂的背后实现,充分理解实现原理和设计原因,才能更好地掌握这种新特性. 只要出 ...
- C++中虚函数的作用和虚函数的工作原理
1 C++中虚函数的作用和多态 虚函数: 实现类的多态性 关键字:虚函数:虚函数的作用:多态性:多态公有继承:动态联编 C++中的虚函数的作用主要是实现了多态的机制.基类定义虚函数,子类可以重写该函数 ...
- C++中虚函数的作用浅析
虚函数联系到多态,多态联系到继承.所以本文中都是在继承层次上做文章.没了继承,什么都没得谈. 下面是对C++的虚函数这玩意儿的理解. 一, 什么是虚函数(如果不知道虚函数为何物,但有急切的想知道,那你 ...
随机推荐
- BOOST_PP_INC_I(x)实现
这个比较有意思,# define BOOST_PP_INC_I(x) BOOST_PP_INC_ ## x 连接在一起以后,然后定义为x+1 实现了inc功能,不过最多也就到255 # /* Copy ...
- 常用的ASCII码对照表
- 管理 sendmail 的邮件队列
邮件队列是存储 sendmail 命令传送的邮件消息数据和控制文件的目录.缺省情况下,邮件队列是 /var/spool/mqueue. 邮件消息可能由于很多原因而排入队列. 例如: sendmail ...
- hdu 5726 GCD 倍增+ 二分
题目链接 给n个数, 定义一个运算f[l,r] = gcd(al, al+1,....ar). 然后给你m个询问, 每次询问给出l, r. 求出f[l, r]的值以及有多少对l', r' 使得f[l, ...
- OpenCV学习 4:摄像头视频读写与边缘检测
原创文章,欢迎转载,转载请注明出处 想实现人脸识别,车辆识别,车牌识别.一般我们可不是读硬盘里面的视频文件的,都是直接从摄像头读取视频流然后直接识别的.所以读取摄像头的视频流这是基础...OpenCV ...
- SQL Server 日志截断
截断事务日志是逻辑操作,只是把日志的一部分标记为‘不再需要’所以可以重用这个空间,截断不是物理操作,不会减少磁盘上文件的大小, 要减小物理大小必定要进行收缩. ----------- 有时就算是备份都 ...
- android 环境搭建 windows, linux
android环境也搭建了很多次了,linux下window下.在这里记录下,以后再搭建设置变量啥的就直接看自己的博客就好了.电子挡笔记有时候也不方便 1.下载材料 概述:用的是比较简单的方式搭建环境 ...
- git忽略特殊文件
忽略特殊文件 有些时候,你必须把某些文件放到Git工作目录中,但又不能提交它们,比如保存了数据库密码的配置文件啦,等等,每次git status都会显示Untracked files ...,有强迫症 ...
- substr,substring,slice 的区别
javascript中的三个函数substr,substring,slice都可以用来提取字符串的某一部分(函数名称都是小写,不要写成subStr,subString又或者Substring,记住js ...
- poj1563---蜗牛爬井
#include <stdio.h> #include <stdlib.h> int main() { int dayTh; float Udis,currentHeight, ...