C++类内存分布 - 转载自Jerry19880126 - 博客园 的文章

在上面这篇文章的基础上做了些整理。

主要讨论了C++类对象的内存分布结构。

来看看编译器是怎么处理类成员内存分布的,特别是在继承虚函数存在的情况下。

原文地址:http://www.cnblogs.com/jerry19880126/p/3616999.html


0、准备

工欲善其事,必先利其器,我们先用好Visual Studio工具,像下面这样一步一步来:

选择左侧的C/C++->命令行,在其他选项这里写上

  • /d1 reportAllClassLayout 它可以看到所有相关类的内存布局,
  • 如果写上/d1 reportSingleClassLayoutXXX(XXX为类名),则只会打出指定类XXX的内存布局。

1、普通类

下面可以定义一个类,像下面这样:

class Base{
int a;
int b;
public:
void CommonFunction();
};

编译后,可以看到输出框里面有这样的排布:

这里不想花精力在内存对齐因素上,所以成员变量都设为int型。

从这里可以看到普通类的排布方式:

  • 成员变量依据声明的顺序进行排列(类内偏移为0开始),成员函数不占内存空间。

2、普通继承

再看下继承,往后面添加如下代码:

class DerivedClass: public Base{
int c;
public:
void DerivedCommonFunction();
};

编译,然后看到如下的内存分布(父类的内存分布不变,这里只讨论子类成员变量的内存分布):

可以看到子类继承了父类的成员变量

在内存排布上:

-先是排布了 父类的成员变量 ,接着排布 子类的成员变量,同样,成员函数不占字节。


3、包含虚函数的类

下面给基类加上虚函数,暂时注释掉DerivedClass,看一下这时的内存排布:

class Base{
int a;
int b;
public:
void CommonFunction();
virtual void VirtualFunction();
};



这个内存结构图分成了两个部分:上面是内存分布,下面是虚表

  1. 内存分布

    VS带的编译器把虚函数表指针放在了内存的开始处(地址偏移为0),然后再是成员变量;
  2. 虚函数表

    紧跟在&Base1_meta后面的0表示,这张虚表对应的虚指针在内存中的分布;

    下面列出了虚函数,左侧的0是这个虚函数 的序号,这里只有一个虚函数,所以只有一项;

    如果有多个虚函数,会有序号为1、2...的虚函数列出来。
  • 编译器是在调用构造函数时创建这个虚表指针以及虚表的。

4、包含虚函数的继承

下面加上子类,并在子类中添加虚函数,像下面这样:

class DerivedClass: public Base{
int c;
public:
void DerivedCommonFunction();
void virtual VirtualFunction();
};

可以看到子类内存的排布如下:

  • 上半部是内存分布,可以看到,

    虚表指针被继承了,且仍位于内存排布的起始处,

    下面是父类的成员变量a和b,最后是子类的成员变量c,

    注意虚表指针只有一个,子类并没有再生成虚表指针了;
  • 下半部的虚表情况与父类(3、包含虚函数的类)是一样的。

5、多态实现

编译器是如何利用虚表指针与虚表来实现多态的呢?

  • 当创建一个含有虚函数的父类的对象时,编译器在对象构造时将虚表指针指向父类的虚函数表;
  • 当创建子类的对象时,编译器在构造函数里将虚表指针(子类只有一个虚表指针,它来自父类)指向子类的虚表(这个虚表里面的虚函数入口地址是子类的)。

所以,如果是调用

Base *p = new Derived();
  1. 生成的是子类的对象,在构造时,子类对象的虚指针指向的是子类的虚表,
  2. 接着由Derived*Base*隐式类型转换并没有改变虚表指针
  3. 若调用p->VirtualFunction(),实际上是p->vfptr->VirtualFunction()

    它在构造的时候就已经指向了子类的VirtualFunction(),所以调用的是子类的虚函数。

这就多态了。


6.1、变化1

我们把子类换个代码,像这样:

class DerivedClass1 : public Base{
int c;
public:
void DerivedCommonFunction();
virtual void VirtualFunction2();
};

注意到这时我们并没有覆盖父类的虚方法,而是重声明了一个新的子类虚方法,内存分布如下:

还是只有一个虚表指针,但是下方虚表的内容变化了,

  • 虚表的0号是父类的VirtualFunction(),而1号放的是子类的 VirtualFunction2()

也就是说,如果定义了DerivedClass的对象(或者指向它的指针或引用),那么在构造时,虚表指针就会指向这个虚表;

如果调用的是VirtualFunction(),那么会从父类中寻找对应的虚函数(!子类没有覆盖父类的函数);

如果调用的是VirtualFunction2(),那么会从子类中寻找对应的虚函数。

????子类没有重载父类的虚函数是一件毫无意义的事情


6.2、变化2

我们再改造一下子类,像这样:

class DerivedClass1 : public Base{
int c;
public:
void DerivedCommonFunction();
virtual void VirtualFunction();
virtual void VirtualFunction2();
};

我们既覆盖父类的虚函数,也有新添的虚函数,那么可以料想的到,是下面的这种内存分布:


7、多重继承

下面来讨论多重继承,代码如下:

class Base{
int a;
int b;
public:
void CommonFunction();
virtual void VirtualFunction();
}; class DerivedClass1: public Base{
int c;
public:
void DerivedCommonFunction();
virtual void VirtualFunction();
}; class DerivedClass2 : public Base{
int d;
public:
void DerivedCommonFunction();
virtual void VirtualFunction();
}; class DerivedDerivedClass : public DerivedClass1, public DerivedClass2{
int e;
public:
void DerivedDerivedCommonFunction();
virtual void VirtualFunction();
};

内存分布从父类到子类,依次如下:



  1. Base中有一个虚表指针,地址偏移为0


  2. DerivedClass1继承了Base,内存排布是先父类后子类。


  3. DerivedClass2的情况是类似于DerivedClass1的。


  4. 下面我们重点看看这个类DerivedDerivedClass,
  • 由外向内看,它并列地排布着继承而来的两个父类DerivedClass1与 DerivedClass2,还有自身的成员变量e。
  • DerivedClass1包含了它的成员变量c,以及Base,Base有一个0地址偏移的虚表指针,然后是成员变量a和b;
  • DerivedClass2的内存排布类似于DerivedClass1,注意到DerivedClass2里面竟然也有一份 Base。



    -这里有两份虚表了,分别针对DerivedClass1DerivedClass2

    &DerivedDericedClass_meta下方的数字0是首地址偏移量,

    靠下面的虚表的那个-16表示指向这个虚表的虚指针的内存偏移,这正是DerivedClass2中的{vfptr}DerivedDerivedClass的内存偏移。

    这里的描述和这篇文章的描述有些出入。

8、虚继承

如果采用虚继承,像下面这样:

class DerivedClass1: virtual public Base{
int c;
public:
void DerivedCommonFunction();
void virtual VirtualFunction();
};
class DerivedClass2 : virtual public Base{
int d;
public:
void DerivedCommonFunction();
void virtual VirtualFunction();
};
class DerivedDerivedClass : public DerivedClass1, public DerivedClass2{
int e;
public:
void DerivedDerivedCommonFunction();
void virtual VirtualFunction();
};


  • Base类没有变化,但往下看:


  • DerivedClass1 就已经有变化了,原来是先排虚表指针与Base成员变量,vfptr位于0地址偏移处;但现在有两个虚表指针了,一个是vbptr,另一个是vfptr

    vbptr是这个DerivedClass1对应的虚表指针,它指向DerivedClass1的虚表vbtable,另一个vfptr是虚基类表对应的虚指针,它指向vftable

下面列出了两张虚表,第一张表是vbptr指向的表,8表示{vbptr}{vfptr}的偏移;

第二张表是vfptr指向的表,-8指明了这张表所对应的虚指针位于内存的偏移量。

DerivedClass2的内存分布类似于DerivedClass1,同样会有两个虚指针,分别指向两张虚表(第二张是虚基类表)。

下面来仔细看一下DerivedDerivedClass的内存分布,这里面有三个虚指针了,但base却只有一份。第一张虚表是内含DerivedClass1的,20表示它的虚指针{vbptr}离虚基表指针{vfptr}的距离,

第二张虚表是内含DerivedClass2 的,12表示它的虚指针{vbptr}离虚基表指针{vfptr}的距离,

最后一张表是虚基表,-20指明了它对应的虚指针{vfptr}在内存中的偏移。

虚继承的作用是减少了对基类的重复,代价是增加了虚表指针的负担(更多的虚表指针)。


9、总结

下面总结一下(当基类有虚函数时):

  1. 每个类都有虚指针和虚表;
  2. 如果不是虚继承,那么子类将父类的虚指针继承下来,并指向自身的虚表(发生在对象构造时)。

    有多少个虚函数,虚表里面的项就会有多少。

    多重继承时,可能存在多个的基类虚表与虚指针;
  3. 如果是虚继承,那么子类会有两份虚指针,一份指向自己的虚表,另一份指向虚基表,

    多重继承时虚基表与虚基表指针有且只有一份。

【转】C++类-内存分布的更多相关文章

  1. 记录:C++类内存分布(虚继承与虚函数)

    工具:VS2013 先说一下VS环境下查看类内存分布的方法: 先选择左侧的C/C++->命令行,然后在其他选项这里写上/d1 reportAllClassLayout,它可以看到所有相关类的内存 ...

  2. 【C++ Primer | 15】C++类内存分布

    C++类内存分布 书上类继承相关章节到这里就结束了,这里不妨说下C++内存分布结构,我们来看看编译器是怎么处理类成员内存分布的,特别是在继承.虚函数存在的情况下. 下面可以定义一个类,像下面这样: c ...

  3. 【转】C++类内存分布

    C++类内存分布  https://www.cnblogs.com/jerry19880126/p/3616999.html 书上类继承相关章节到这里就结束了,这里不妨说下C++内存分布结构,我们来看 ...

  4. C++类内存分布

    http://www.cnblogs.com/jerry19880126/p/3616999.html#undefined 书上类继承相关章节到这里就结束了,这里不妨说下C++内存分布结构,我们来看看 ...

  5. 转载:C++类内存分布

    本文转自:http://www.cnblogs.com/jerry19880126/p/3616999.html,原文写的非常好,从中学到了虚继承的概念,也学会了用VS查看内存分布. 说下C++内存分 ...

  6. c++类内存分布解析

    首先使用Visual Studio工具查看类的内存分布,如下: 先选择左侧的C/C++->命令行,然后在其他选项这里写上/d1 reportAllClassLayout,它可以看到所有相关类的内 ...

  7. 使用Visual Studio查看C++类内存分布

    书上类继承相关章节到这里就结束了,这里不妨说下C++内存分布结构,我们来看看编译器是怎么处理类成员内存分布的,特别是在继承.虚函数存在的情况下. 工欲善其事,必先利其器,我们先用好Visual Stu ...

  8. C++浅析——继承类内存分布和虚析构函数

    继承类研究 1. Code 1.1 Cbase, CTEST为基类,CTest2为其继承类,并重新申明了基类中的同名变量 class CBase { public: int Data; CBase() ...

  9. C++ 类的内存分布

    C++类内存分布 转自:http://www.cnblogs.com/jerry19880126/p/3616999.html   先写下总结,通过总结下面的例子,你就会明白总结了. 下面总结一下: ...

随机推荐

  1. HDOJ 1427(dfs) 速算24点

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1427 思路分析: 题目要求判断是否存在一种运算组合使得4个数的计算结果为24,因为搜索的层次为3层,不 ...

  2. DOC下编译和运行带有包的java类文件

    前言: 带有包名的java类在DOC下编译可以成功,但是运行出错  错误: 找不到或无法加载主类 com.soanl.socket.MyServer D盘temp文件下有个Hello.java文件,包 ...

  3. B - 楼下水题(扩展欧几里德)

    B - 楼下水题 Time Limit:1000MS     Memory Limit:262144KB     64bit IO Format:%I64d & %I64u Submit St ...

  4. 关于CDC在非控件类中的使用

    在非CStatic的派生类中,由于进行图形的绘制的话,我们需要对该类传入一个CDC以便于绘画.这是因为非CStatic等控件类无法自己产生onPaint这类的消息,因此需要借传入的CDC进行回执,然后 ...

  5. 拿出来分享了!VIP珍藏!!!全网最齐全的 DEDECMS模板 全盘下载地址列表!没有你找不到的!

    拿出来分享了!VIP珍藏!!!全网最齐全的 DEDECMS模板 网盘地址!没有你找不到的! 模板类型最齐全: ----------------------优美的走起!------------ 一:DE ...

  6. Lamd表达式

    1. 普通绑定: public void button1_Click(object sender, EventArgs e) { MessageBox.Show("ok"); } ...

  7. 在SQL 中生成JSON数据

    这段时间接手一个数据操作记录的功能,刚拿到手上的时候打算用EF做,后来经过仔细考虑最后还是觉定放弃,最后思考再三决定: 1.以模块为单位分表.列固定(其实可以所有的操作记录都放到同一个表,但是考虑到数 ...

  8. CocoaPods对于不同Target引入不同的第三方库Podfile的写法

    有的时候我们需要建立多个Target来完成不同的测试环境的区分,而多个Target之间可能会有第三方库的不同引用,如果我们在使用CocoaPods管理我们的第三方库的时候,我们就需要思考我们需要如何实 ...

  9. Failed to fetch URL http://dl-ssl.google.com/android/repository/addons_list-2.xml, reason: Connectio (andriod sdk manager) http://dl-ssl.google.com/android上不去解决方案

    Fetching https://dl-ssl.google.com/android/repository/addons_list-2.xml  Fetched Add-ons List succes ...

  10. r语言之给定的概率密度函数生成随机数

    假设概率密度函数为: 思路: 首先产生-1到1之间的均匀分布随机数x,和0到1之间的均匀分布随机数y. 如果y<f(x),则x是符合该概率密度的随机数,否则,重复上述操作. 用r语言生成100个 ...