详谈C++中虚基类在派生类中的内存布局

 更新时间:2020年4月25日 17:33  点击:1894

今天重温C++的知识,当看到虚基类这点的时候,那时候也没有太过追究,就是知道虚基类是消除了类继承之间的二义性问题而已,可是很是好奇,它是怎么消除的,内存布局是怎么分配的呢?于是就深入研究了一下,具体的原理如下所示:

在C++中,obj是一个类的对象,p是指向obj的指针,该类里面有个数据成员mem,请问obj.mem和p->mem在实现和效率上有什么不同。

答案是:只有一种情况下才有重大差异,该情况必须满足以下3个条件:

(1)、obj 是一个虚拟继承的派生类的对象

(2)、mem是从虚拟基类派生下来的成员

(3)、p是基类类型的指针

当这种情况下,p->mem会比obj.mem多了两个中间层。(也就是说在这种情况下,p->mem比obj.mem要明显的慢)

WHY?

如果好奇心比较重的话,请往下看 :

1、虚基类的使用,和为多态而实现的虚函数不同,是为了解决多重继承的二义性问题。

举例如下:

class A
{
public:
  int a;
};
class B : virtual public A
{
public:
  int b;
};
class C :virtual public A
{
public:
  int c; 
};
class D : public B, public C
{
public:
  int d;
};

上面这种菱形的继承体系中,如果没有virtual继承,那么D中就有两个A的成员int a;继承下来,使用的时候,就会有很多二义性。而加了virtual继承,在D中就只有A的成员int a;的一份拷贝,该拷贝不是来自B,也不是来自C,而是一份单独的拷贝,那么,编译器是怎么实现的呢??

在回答这个问题之前,先想一下,sizeof(A),sizeof(B),sizeof(C),sizeof(D)是多少?(在32位x86的linux2.6下面,或者在vc2005下面)在linux2.6下面,结果如下:sizeof(A) = 4; sizeof(B) = 12; sizeof(C) = 12; sizeof(D) = 24;sizeof(B)为什么是12呢,那是因为多了一个指针(这一点和虚函数的实现一样),那个指针是干嘛的呢?

那么sizeof(D)为什么是24呢?那是因为除了继承B中的b,C中的c,A中的a,和D自己的成员d之外,还继承了B,C多出来的2个指针(B和C分别有一个)。再强调一遍,D中的int a不是来自B也不是来自C,而是另外的一份从A直接靠过来的成员。

如果声明了D的对象d: D d;

那么d的内存布局如下:

vb_ptr: 继承自B的指针
int b: 继承自B公有成员
vc_ptr:继承自C的指针
int c: 继承自C的共有成员
int d: D自己的公有成员
int a: 继承自A的公有成员
 
那么以下的用法会发生什么事呢?

D dD;
B *pb = &dD;
pb->a;

上面说过,dD中的int a不是继承自B的,也不是继承自C的,那么这个B中的pb->a又会怎么知道指向的是dD内存中的第六项呢?

那就是指针vb_ptr的妙用了。原理如下:(其实g++3.4.3的实现更加复杂,我不知道是出于什么考虑,而我这里只说原理,所以把过程和内容简单化了)

首先,vb_ptr指向一个整数的地址,里面放的整数是那个int a的距离dD开始处的位移(在这里vb_ptr指向的地址里面放的是20,以字节为单位)。编译器是这样做的:

首先,找到vb_ptr(这个不用找,因为在g++中,vb_ptr就是B*中的第一项,呵呵),然后取得vb_ptr指向的地址的内容(这个例子是20),最后把这个内容与指针pb相加,就得到pb->a的地址了。

所以说这种时候,用指针转换多了两个中间层才能找到基类的成员,而且是运行期间。

由此也可以推知dD中的vb_ptr和vc_ptr的内容都是一样的,都是指向同一个地址,该地址就放20(在本例中)

如下的语句呢:

A *pa = &dD;
pa->a = 4;

这个语句不用转换了,因为编译器在编译期间就知道他把A中的成员插在dD中的那个地方了(在本例中是末尾),所以这个语句中的运行效率和dD.a是一样的(至少也是差不多的)

这就是虚基类实现的基本原理。

注意的是:那些指针的位置和基类成员在派生类成员中的内存布局是不确定的,也就是说标准里面没有规定inta必须要放在最后,只不过g++编译器的实现而已。c++标准大概只规定了这套机制的原理,至于具体的实现,比如各成员的排放顺序和优化,由各个编译器厂商自己定~

非虚拟继承:

在派生类对象里,按照继承声明顺序依次分布基类对象,最后是派生类数据成员。

若基类声明了虚函数,则基类对象头部有一个虚函数表指针,然后是基类数据成员。

在基类虚函数表中,依次是基类的虚函数,若某个函数被派生类override,则替换为派生类的函数。

派生类独有的虚函数被加在第一个基类的虚函数表后面。

虚拟继承:

在派生类对象里,按照继承声明顺序依次分布非虚基类对象,然后是派生类数据成员,最后是虚基类对象。

若基类声明了虚函数,则基类对象头部有一个虚函数表指针,然后是基类数据成员。

在基类虚函数表中,依次是基类的虚函数,若某个函数被派生类override,则替换为派生类的函数。

若直接从虚基类派生的类没有非虚父类,且声明了新的虚函数,则该派生类有自己的虚函数表,在该派生类头部;否则派生类独有的虚函数被加在第一个非虚基类的虚函数表后面。

直接从虚基类派生的类内部还有一个虚基类表指针(一个隐藏的“虚基类表指针”成员,指向一个虚基类表),在数据成员之前,非虚基类对象之后(若有的话)。

虚基类表中第一个值是该虚基类表到派生类起始地址的偏移;之后的值依次是该派生类的虚基类到该表位置的地址偏移(虚基类对象的地址与派生类的“虚基类表指针”之间的偏移量)。

对于虚函数表指针和虚基类表指针:

当单继承且非虚继承时:每个含有虚函数的表只有一个虚函数表,所以只需要一个虚表指针即可;

当多继承且非虚继承时:一个子类有几个父类则会有几个虚函数表,所以就有和父类个数相同的虚表指针来标识;

总之,当时非虚继承时,不需要额外增加虚函数表指针。

当虚继承时:无论是单虚继承还是多虚继承,需要有一个虚基类表来记录虚继承关系,所以此时子类需要多一个虚基类表指针;而且只需要一个即可。

当虚继承时可能出现一个类中持有多个虚函数表的情况:无论是单虚继承还是多虚继承,

如果子类没有构造函数和析构函数,且子类中的虚函数都是在父类中出现的虚函数,这个时候不需要增加任何虚表指针;只需要像多继承那个持有父类个数的虚表指针来标识即可;

如果子类中含有构造函数或者析构函数或二者都有,则在子类中只要每出现一个父类中的虚函数则需要增加一个虚函数表指针来标识此类的虚函数表;

无论是否含有构造函数或者虚构函数,只要继承都是虚继承且出现了父类中没有出现的虚函数,则在子类中需要再增加一个徐函数表指针;如果其中有一个是非虚继承,则按照最省空间的原则,不需要增加虚函数表指针,因为这个时候可以和非虚基类共享一个虚函数表指针。

以上就是小编为大家带来的详谈C++中虚基类在派生类中的内存布局全部内容了,希望大家多多支持猪先飞~

[!--infotagslink--]

相关文章

  • php svn操作类

    以前我们开发大型项目时都会用到svn来同步,因为开发产品的人过多,所以我们会利用软件来管理,今天发有一居然可以利用php来管理svn哦,好了看看吧。 代码如下 ...2016-11-25
  • PHP 数据库缓存Memcache操作类

    操作类就是把一些常用的一系列的数据库或相关操作写在一个类中,这样调用时我们只要调用类文件,如果要执行相关操作就直接调用类文件中的方法函数就可以实现了,下面整理了...2016-11-25
  • c++中的system("pause")的作用和含义解析

    这篇文章主要介绍了c++中system("pause")的作用和含义,非常不错,具有参考借鉴价值,需要的朋友参考下吧...2020-04-25
  • 基于BootStrap Metronic开发框架经验小结【八】框架功能总体界面介绍

    这篇文章主要介绍了基于BootStrap Metronic开发框架经验小结【八】框架功能总体界面介绍 的相关资料,需要的朋友可以参考下...2016-05-14
  • JS+CSS实现分类动态选择及移动功能效果代码

    本文实例讲述了JS+CSS实现分类动态选择及移动功能效果代码。分享给大家供大家参考,具体如下:这是一个类似选项卡功能的选择插件,与普通的TAb区别是加入了动画效果,多用于商品类网站,用作商品分类功能,不过其它网站也可以用,...2015-10-21
  • Php文件上传类class.upload.php用法示例

    本文章来人大家介绍一个php文件上传类的使用方法,期望此实例对各位php入门者会有不小帮助哦。 简介 Class.upload.php是用于管理上传文件的php文件上传类, 它可以帮...2016-11-25
  • C# 16 进制字符串转 int的方法

    这篇文章主要介绍了C# 16 进制字符串转 int的方法,非常不错,具有参考借鉴价值,需要的朋友可以参考下...2020-06-25
  • InterlliJ IDEA2020新建java web项目找不到Static Web的解决

    这篇文章主要介绍了InterlliJ IDEA2020新建java web项目找不到Static Web的解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-09-02
  • PHP实现无限级分类(不使用递归)

    无限级分类在开发中经常使用,例如:部门结构、文章分类。无限级分类的难点在于“输出”和“查询”,例如 将文章分类输出为<ul>列表形式; 查找分类A下面所有分类包含的文章。1.实现原理 几种常见的实现方法,各有利弊。其中...2015-10-23
  • PHP实现递归无限级分类

    在一些复杂的系统中,要求对信息栏目进行无限级的分类,以增强系统的灵活性。那么PHP是如何实现无限级分类的呢?我们在本文中使用递归算法并结合mysql数据表实现无限级分类。 递归,简单的说就是一段程序代码的重复调用,当把...2015-10-23
  • C#判断一个字符串是否是数字或者含有某个数字的方法

    这篇文章主要介绍了C#判断一个字符串是否是数字或者含有某个数字的方法,本文给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下...2020-06-25
  • mybatis-plus实体类主键策略有3种(小结)

    这篇文章主要介绍了mybatis-plus实体类主键策略有3种(小结),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-08-27
  • ecshop商品无限级分类代码

    ecshop商品无限级分类代码 function cat_options($spec_cat_id, $arr) { static $cat_options = array(); if (isset($cat_options[$spec_cat_id]))...2016-11-25
  • C#类中static变量用法分析

    这篇文章主要介绍了C#类中static变量用法,实例分析了static变量使用技巧与相关注意事项,需要的朋友可以参考下...2020-06-25
  • C# WinForm快捷键设置技巧

    这篇文章主要给大家介绍C# winform快捷键设置技巧,涉及到C winform快捷键相关知识,对C winform知识感兴趣的朋友可以参考下本篇文章...2020-06-25
  • 详解C#切换窗口

    最近项目不多忙,于是抽点时间巩固下切换窗口问题,感兴趣的朋友跟着小编一起学习吧...2020-06-25
  • PHP 一个完整的分页类(附源码)

    在php中要实现分页比起asp中要简单很多了,我们核心就是直接获取当前页面然后判断每页多少再到数据库中利用limit就可以实现分页查询了,下面我来详细介绍分页类实现程序...2016-11-25
  • 基于Ionic3实现选项卡切换并重新加载echarts

    这篇文章主要介绍了基于Ionic3实现选项卡切换并重新加载echarts,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下...2020-09-24
  • pytorch绘制并显示loss曲线和acc曲线,LeNet5识别图像准确率

    今天小编就为大家分享一篇pytorch绘制并显示loss曲线和acc曲线,LeNet5识别图像准确率,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-05-02
  • c#各种Timer类的区别与用法介绍

    System.Threading.Timer 是一个简单的轻量计时器,它使用回调方法并由线程池线程提供服务。在必须更新用户界面的情况下,建议不要使用该计时器,因为它的回调不在用户界面线程上发生...2020-06-25