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

今天重温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++中虚基类在派生类中的内存布局全部内容了,希望大家多多支持我们~

时间: 2016-12-20

浅谈C++中派生类对象的内存布局

主要从三个方面来讲: 1 单一继承 2 多重继承 3 虚拟继承 1 单一继承 (1)派生类完全拥有基类的内存布局,并保证其完整性. 派生类可以看作是完整的基类的Object再加上派生类自己的Object.如果基类中没有虚成员函数,那么派生类与具有相同功能的非派生类将不带来任何性能上的差异.另外,一定要保证基类的完整性.实际内存布局由编译器自己决定,VS里,把虚指针放在最前边,接着是基类的Object,最后是派生类自己的object.举个栗子: class A { int b; char c; }

详解C++中基类与派生类的转换以及虚基类

C++基类与派生类的转换 在公用继承.私有继承和保护继承中,只有公用继承能较好地保留基类的特征,它保留了除构造函数和析构函数以外的基类所有成员,基类的公用或保护成员的访问权限在派生类中全部都按原样保留下来了,在派生类外可以调用基类的公用成员函数访问基类的私有成员.因此,公用派生类具有基类的全部功能,所有基类能够实现的功能, 公用派生类都能实现.而非公用派生类(私有或保护派生类)不能实现基类的全部功能(例如在派生类外不能调用基类的公用成员函数访问基类的私有成员).因此,只有公用派生类才是基类真正的

实例讲解C++编程中的虚函数与虚基类

虚函数 ① #include "stdafx.h" #include <iostream> using namespace std; class B0//基类B0声明 { public: void display(){cout<<"B0::display()"<<endl;}//公有成员函数 }; class B1: public B0//公有派生类B1声明 { public: void display(){cout<<

C++中基类和派生类之间的转换实例教程

本文实例讲解了C++中基类和派生类之间的转换.对于深入理解C++面向对象程序设计有一定的帮助作用.此处需要注意:本文实例讲解内容的前提是派生类继承基类的方式是公有继承,关键字public.具体分析如下: 以下程序为讲解示例: #include<iostream> using namespace std; class A { public: A(int m1, int n1):m(m1), n(n1){} void display(); private: int m; int n; }; voi

C#实现Stream与byte[]之间的转换实例教程

本文以实例形式详细介绍了C#实现Stream与byte[]之间的转换的方法,分享给大家供大家参考之用.具体方法如下: 一.二进制转换成图片 MemoryStream ms = new MemoryStream(bytes); ms.Position = 0; Image img = Image.FromStream(ms); ms.Close(); this.pictureBox1.Image 二.C#中byte[]与string的转换代码 1. System.Text.UnicodeEncod

java中long(Long)与int(Integer)之间的转换方式

我就废话不多说了,大家还是直接看代码吧~ public static void main(String[] args) { // 1.将long型转化为int型,其中int.long是基础类型 long a = 10; int b = (int) a; System.out.println("1.将long型转化为int型:" + b); // 2.将int型转化为long型,其中int.long都是基础类型 int a1 = 10; long b1 = a1; System.out.

C#中将xml文件反序列化为实例时采用基类还是派生类的知识点讨论

基类: using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace DeserializeTest { public class SettingsBase { private string m_fileName; public string FileName { get { return m_fileName; } set { m_fileName = value;

在Java中String和Date、Timestamp之间的转换

一.String与Date(java.util.Date)互转   1.1 String -> Date String dateStr = "// ::"; Date date = new Date(); //注意format的格式要与日期String的格式相匹配 DateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); try { date = sdf.parse(dateStr); Syst

JavaScript对象和字串之间的转换实例探讨

JavaScript 对象定义方式 1. var obj = new Object() 复制代码 代码如下: <!--Add by oscar999--> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <HTML> <HEAD> <TITLE> New Document </TITLE> <META NAME="Auth

Android中使用Camera类编写手机拍照App的实例教程

Camera是Android摄像头硬件的相机类,位于硬件包"android.hardware.Camera"下.它主要用于摄像头捕获图片.启动/停止预览图片.拍照.获取视频帧等,它是设备本地的服务,负责管理设备上的摄像头硬件. Camera既然用于管理设备上的摄像头硬件,那么它也为开发人员提供了相应的方法,并且这些方法大部分都是native的,用C++在底层实现,下面简单介绍一下Camera的一些方法: static Camera open():打开Camera,返回一个Camera实

vue.js整合vux中的上拉加载下拉刷新实例教程

前言 Vux 是基于 Vue 和 Weui 开发的手机端页面 UI 组件库,开发初衷是满足公司的微信端表单需求,因为第三方的调查问卷表单系统在手机上实在比较丑(还是 PC 那一套样式适配了大小而已).于是用 vue 重构了表单组件,后来一发不可收拾把其他常用组件也一并开发了. 相比于 React 还是更喜欢用 Vue ,除了目前社区组件不多,周边构建工具还是比较完善的(作者也特别勤奋). 下面话不多说了,来一看看详细的介绍吧. 先上图 创建项目 使用vue-cli 创建一个vue项目 安装vux