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

主要从三个方面来讲:

  1 单一继承

  2 多重继承

  3 虚拟继承

1 单一继承

(1)派生类完全拥有基类的内存布局,并保证其完整性。

派生类可以看作是完整的基类的Object再加上派生类自己的Object。如果基类中没有虚成员函数,那么派生类与具有相同功能的非派生类将不带来任何性能上的差异。另外,一定要保证基类的完整性。实际内存布局由编译器自己决定,VS里,把虚指针放在最前边,接着是基类的Object,最后是派生类自己的object。举个栗子:

class A
{
  int b;
  char c;
};
class A1 :public A
{
    char a;
};
int main()
{
  cout << sizeof(A) << " " << sizeof(A1) << endl;
  return 0;
}

输出是什么?

答案:

8 12

A类的话,一个int,一个char,5B,内存对齐一下,8B。A1的话,一个int,两个char,内存对齐一下,也是8B。不对吗?

我说了,要保证基类对象的完整性。那么一定要保证A1类前面的几个字节一定要与A类完全一样。也就是说,A类作为内存补齐的3个字节也是要出现在A1里面的。也就是说,A类是这样的:int(4B)+char(1B)+padding(3B)=8B,A1类:int(4B)+char(1B)+padding(3B)+char(1B)+padding(3B)=12B。

(2)虚指针怎么处理?

还是视编译器而定,VS是永远把vptr放在对象的最前边。如果基类中含有虚函数,那么处理情况与上边一样。可是,如果基类中没有虚函数而派生类有的话,那么如果把vptr放在派生类的前边的话,将会导致派生类中基类成分并不在最前边。这将带来什么问题呢?举栗:假设A不含虚,而A1含。

A *pA;
A1 obj_A1;
pA=&obj_A1;

如果A1完全包含A并且A位于A1的最前边,那么编译器只需要把&obj_A1直接赋给pA就可以了。如果不是呢?编译器就需要把&obj_A1+sizeof(vptr)赋给pA了。

2 多重继承

说结论:VS的内存布局是按照声明顺序排列内存。再举个栗子:

class point2d
{
public:
  virtual ~point2d(){};
  float x;
  float y;
};
class point3d :public point2d
{
  ~point3d(){};
  float z;
};
class vertex
{
public:
  virtual ~vertex(){};
  vertex* next;
};
class vertex3d :public point3d, public vertex
{
  float bulabula;
};

int _tmain(int argc, _TCHAR* argv[])
{
  cout << sizeof(point2d) << " " << sizeof(point3d) << " " << sizeof(vertex) << " " << sizeof(vertex3d) << endl;
  return 0;
}

输出: 12 16 8 24。

内存布局:

point2d: vptr(4)+x(4)+y(4)=12B

point3d: vptr+x+y+z=16B

vertex: vptr+next=8B

vertex3d: vptr+x+y+z+vptr+next+bulabula=28B

为什么需要多个虚指针?请往下看。

3 虚拟继承

(1)为什么要有“虚继承”这样的机制?

简单讲,虚继承是为也防止“diamond”继承所带来的问题。也就是类A1、A2都继承于A,类B又同时继承于A1、A2。这样一来,类B中就有两份类A的成员了,这样的程序无法通过编译。我们改成这样的形式:

class A
{
public:
  int a;
  virtual ~A();
  virtual void fun(){cout<<"A"<<endl;}
};
class A1 :public virtual A
{
public:
  int a1;
  virtual void fun(){cout<<"A1"<<endl;}
};
class A2 :public virtual A
{
public:
  int a2;
  virtual void fun(){cout<<"A2"<<endl;}
}; 

class B :public A1,public A2 {
public:
  int b;
  virtual void fun(){cout<<"B"<<endl;}
  virtual void funB(){};
};

这样就能防止这样的事情发生。

(2)虚拟继承与普通继承的区别:

普通继承使得派生类每继承一个基类便拥有一份基类的成员。而虚拟继承会把通过虚拟继承的那一部分,放在对象的最后。从而使得只拥有一份基类中的成员。虚拟对象的偏移量被保存在Derived类的vtbl的this指向的上一个slot。比较难理解。下面我给你个栗子。

(3)虚拟继承的内存布局:

每个派生类会把其不变部分放在前面,共享部分放在后面。

上面四个类的大小是怎样的呢?

int _tmain(int argc, _TCHAR* argv[])
{
  cout << sizeof(A) << " " << sizeof(A1) << " " << sizeof(A2) << " " << sizeof(B) << endl;
  return 0;
}

输出:8 16 16 28

内存布局:

    A: vptr+a=8B

    A1: vptr+a1+vptrA+a=16B

    A2: vptr+a2+vptrA+a=16B

    A3: vptr+a1+vptrA2+a2+b+vptrA+a=28B

上个草图:

那究竟为什么需要多个虚指针?将对象内存布局和虚表结构搞清楚之后,答案是不是呼之欲出呢?

是的,因为这样可以保证在将子类指针/引用转换成基类指针时编译器可以直接根据对像的内存布局进行偏移,从而使得指向的第一个内容为虚指针,进而实现多态(根据静态类型执行相应动作)。

以上就是小编为大家带来的浅谈C++中派生类对象的内存布局全部内容了,希望大家多多支持~

以上是小编为您精心准备的的内容,在的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索派生类对象
派生类的内嵌对象、派生类对象、派生类的对象、基类定义派生类对象、基类对象和派生类对象,以便于您获取更多的相关知识。

时间: 2024-09-30 21:44:34

浅谈C++中派生类对象的内存布局_C 语言的相关文章

解读C++编程中派生类的构成和创建_C 语言

C++派生类的构成 派生类中的成员包括从基类继承过来的成员和自己增加的成员两大部分.从基类继承的成员体现了派生类从基类继承而获得的共性,而新增加的成员体现了派生类的个性.正是这些新增加的成员体现了派生类与基类的不同,体现了不同派生类之间的区别. 在基类中包括数据成员和成员函数 (或称数据与方法)两部分,派生类分为两大部分:一部分是从基类继承来的成员,另一部分是在声明派生类时增加的部分.每一部分均分别包括数据成员和成员函数. 实际上,并不是把基类的成员和派生类自己增加的成员简单地加在一起就成为派生

浅谈C++中的构造函数分类及调用规则_C 语言

构造函数的分类这里简单地将C++中的构造函数分一下类,直接看下面的代码表达,说明在注释中: #include <iostream> using namespace std; class Text { public: Text() // 无参数构造函数 { m_a = 0; m_b = 0; cout << "无参数构造函数" << endl; } Text(int a) // 有参数构造函数 { m_a = a; m_b = 0; cout <

浅析内存对齐与ANSI C中struct型数据的内存布局_C 语言

这些问题或许对不少朋友来说还有点模糊,那么本文就试着探究它们背后的秘密. 首先,至少有一点可以肯定,那就是ANSI C保证结构体中各字段在内存中出现的位置是随它们的声明顺序依次递增的,并且第一个字段的首地址等于整个结构体实例的首地址.比如有这样一个结构体: 复制代码 代码如下:   struct vector{int x,y,z;} s;  int *p,*q,*r;  struct vector *ps;  p = &s.x;  q = &s.y;  r = &s.z;  ps

浅谈js中StringBuffer类的实现方法及使用_javascript技巧

如下所示: <strong>JAVA中有一个StringBuffer类,js中却没有下面来自己实现一个简单的js的StringBuffer类.</strong> //创建一个StringBuffer类 ,此类有两个方法:一个是append方法一个是toString方法 function StringBuffer() { this.__strings__ = []; }; StringBuffer.prototype.append = function(str) { this.__s

浅谈java中对集合对象list的几种循环访问_java

java中对集合对象list的几种循环访问的总结如下  1 经典的for循环  public static void main(String[] args) { List<String> list = new ArrayList(); list.add("123"); list.add("java"); list.add("j2ee"); System.out.println("=========经典的for循环======

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

今天重温C++的知识,当看到虚基类这点的时候,那时候也没有太过追究,就是知道虚基类是消除了类继承之间的二义性问题而已,可是很是好奇,它是怎么消除的,内存布局是怎么分配的呢?于是就深入研究了一下,具体的原理如下所示: 在C++中,obj是一个类的对象,p是指向obj的指针,该类里面有个数据成员mem,请问obj.mem和p->mem在实现和效率上有什么不同. 答案是:只有一种情况下才有重大差异,该情况必须满足以下3个条件: (1).obj 是一个虚拟继承的派生类的对象 (2).mem是从虚拟基类派

浅谈JavaScript中的String对象常用方法

这篇文章主要介绍了JavaScript中的String对象常用方法,非常简单实用,有需要的小伙伴参考下 String对象提供的方法用于处理字符串及字符. 常用的一些方法: charAt(index):返回字符串中index处的字符. indexOf(searchValue,[fromIndex]):该方法在字符串中寻找第一次出现的searchValue.如果给定了fromIndex,则从字符串内该位置开始搜索,当searchValue找到后,返回该串第一个字符的位置. lastIndexOf(s

浅谈C#中ToString()和Convert.ToString()的区别_C#教程

浅谈ToString()和Convert.ToString()方法的区别 一.一般用法说明 ToString()是Object的扩展方法,所以都有ToString()方法;而Convert.ToString(param)(其中param参数的数据类型可以是各种基本数据类型,也可以是bool或object类对象. 二.ToString()和Convert.ToString()的区别 一般情况下,这两种方法都可以通用,但是当返回的数据类型中有可能出现null值时如果调用ToString方法了,就会返

解析C++中派生的概念以及派生类成员的访问属性_C 语言

C++继承与派生的概念.什么是继承和派生 在C++中可重用性是通过继承(inheritance)这一机制来实现的.因此,继承是C++的一个重要组成部分. 前面介绍了类,一个类中包含了若干数据成员和成员函数.在不同的类中,数据成员和成员函数是不相同的.但有时两个类的内容基本相同或有一部分相同,例如巳声明了学生基本数据的类Student: class Student { public: void display( ) //对成员函数display的定义 { cout<<"num: &qu