【高级】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++的虚函数这玩意儿的理解. 一, 什么是虚函数(如果不知道虚函数为何物,但有急切的想知道,那你 ...
随机推荐
- Linux Mysql 总结
一:Error Code: . Access denied for user 'root'@'%' to database ① mysql -u root -p 进入到mysql中 ②SELECT h ...
- php随笔1-php图片处理
php图片处理的知识内容 upload_image.php: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN ...
- Java String 和 StringBuffer的区别
1.String类的定义是这样的: public final class String extends Object implements Serializable, Comparable< ...
- Linux----给一个普通用户root权限
问题说明:linux可以通过useradd创建用户.那有没有想过.我们创建的用户怎么样才可以使它得到全部的root权限呢? 解决办法: 1.这是一个可以打80分的办法.就是编辑/etc/sudoers ...
- [原]性能优化之Hibernate缓存讲解、应用和调优
近来坤哥推荐我我们一款性能监控.调优工具--JavaMelody,通过它让我觉得项目优化是看得见摸得着的,优化有了针对性.而无论是对于分布式,还是非分布,缓存是提示性能的有效工具. 数据层是EJB3. ...
- CC++初学者编程教程(1) Visual Stduio2010开发环境搭建
Visual Studio是微软公司推出的开发环境.是目前最流行的Windows平台应用程序开发环境. Visual Studio 2010版本于2010年4月12日上市,其集成开发环境(IDE)的界 ...
- 一个好用的VC DBGRID[图]
本文示例源代码下载 CGridCtrl_demo19_01.zip为演示CGridCtrl的使用 CGridCtrl_demo19_02.zip演示与CMYODBC的配合使用 一.引言 在用vc开发关 ...
- ldap for ruby
Net::LDAP for Ruby (also called net-ldap) implements client access for the Lightweight Directory Acc ...
- Java反射-简单应用
为了程序更好的维护和扩展,在面向对象思维的世界里,首先是面向接口编程,然后我们应该把做什么和怎么做进行分离. 以下我将用一个开晚会的样例来演示一下,终于达到的效果是:工厂+反射+配置文件实现程序的灵活 ...
- 杂记之activity之间的跳转
代码结构图 manifest.xml <?xml version="1.0" encoding="utf-8"?> <manifest xml ...