zoukankan      html  css  js  c++  java
  • C++类内存分布

    书上类继承相关章节到这里就结束了,这里不妨说下C++内存分布结构,我们来看看编译器是怎么处理类成员内存分布的,特别是在继承、虚函数存在的情况下。

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

    先选择左侧的C/C++->命令行,然后在其他选项这里写上/d1 reportAllClassLayout,它可以看到所有相关类的内存布局,如果写上/d1 reportSingleClassLayoutXXX(XXX为类名),则只会打出指定类XXX的内存布局。近期的VS版本都支持这样配置。

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

    复制代码
    1 class Base
    2 {
    3     int a;
    4     int b;
    5 public:
    6     void CommonFunction();
    7 };
    复制代码

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

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

    从这里可以看到普通类的排布方式,成员变量依据声明的顺序进行排列(类内偏移为0开始),成员函数不占内存空间。

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

    复制代码
    1 class DerivedClass: public Base
    2 {
    3     int c;
    4 public:
    5     void DerivedCommonFunction();
    6 };
    复制代码

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

    可以看到子类继承了父类的成员变量,在内存排布上,先是排布了父类的成员变量,接着排布子类的成员变量,同样,成员函数不占字节。

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

    复制代码
    1 class Base
    2 {
    3     int a;
    4     int b;
    5 public:
    6     void CommonFunction();
    7     void virtual VirtualFunction();
    8 };
    复制代码

    这个内存结构图分成了两个部分,上面是内存分布,下面是虚表,我们逐个看。VS所带编译器是把虚表指针放在了内存的开始处(0地址偏移),然后再是成员变量;下面生成了虚表,紧跟在&Base1_meta后面的0表示,这张虚表对应的虚指针在内存中的分布,下面列出了虚函数,左侧的0是这个虚函数的序号,这里只有一个虚函数,所以只有一项,如果有多个虚函数,会有序号为1,为2的虚函数列出来。

    编译器是在构造函数创建这个虚表指针以及虚表的。

    那么编译器是如何利用虚表指针与虚表来实现多态的呢?是这样的,当创建一个含有虚函数的父类的对象时,编译器在对象构造时将虚表指针指向父类的虚函数;同样,当创建子类的对象时,编译器在构造函数里将虚表指针(子类只有一个虚表指针,它来自父类)指向子类的虚表(这个虚表里面的虚函数入口地址是子类的)。

    所以,如果是调用Base *p = new Derived();生成的是子类的对象,在构造时,子类对象的虚指针指向的是子类的虚表,接着由Derived*到Base*的转换并没有改变虚表指针,所以这时候p->VirtualFunction,实际上是p->vfptr->VirtualFunction,它在构造的时候就已经指向了子类的VirtualFunction,所以调用的是子类的虚函数,这就是多态了。

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

    复制代码
    1 class DerivedClass: public Base
    2 {
    3     int c;
    4 public:
    5     void DerivedCommonFunction();
    6     void virtual VirtualFunction();
    7 };
    复制代码

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

    上半部是内存分布,可以看到,虚表指针被继承了,且仍位于内存排布的起始处,下面是父类的成员变量a和b,最后是子类的成员变量c,注意虚表指针只有一个,子类并没有再生成虚表指针了;下半部的虚表情况与父类是一样的。

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

    复制代码
    1 class DerivedClass1 : public Base
    2 {
    3     int c;
    4 public:
    5     void DerivedCommonFunction();
    6     void virtual VirtualFunction2();
    7 };
    复制代码

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

    还是只有一个虚表指针,但是下方虚表的内容变化了,虚表的0号是父类的VirtualFunction,而1号放的是子类的VirtualFunction2。也就是说,如果定义了DerivedClass的对象,那么在构造时,虚表指针就会指向这个虚表,以后如果调用的是VirtualFunction,那么会从父类中寻找对应的虚函数,如果调用的是VirtualFunction2,那么会从子类中寻找对应的虚函数。

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

    复制代码
    1 class DerivedClass1 : public Base
    2 {
    3     int c;
    4 public:
    5     void DerivedCommonFunction();
    6     void virtual VirtualFunction();
    7     void virtual VirtualFunction2();
    8 };
    复制代码

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

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

    复制代码
     1 class Base
     2 {
     3     int a;
     4     int b;
     5 public:
     6     void CommonFunction();
     7     void virtual VirtualFunction();
     8 };
     9 
    10 
    11 class DerivedClass1: public Base
    12 {
    13     int c;
    14 public:
    15     void DerivedCommonFunction();
    16     void virtual VirtualFunction();
    17 };
    18 
    19 class DerivedClass2 : public Base
    20 {
    21     int d;
    22 public:
    23     void DerivedCommonFunction();
    24     void virtual VirtualFunction();
    25 };
    26 
    27 class DerivedDerivedClass : public DerivedClass1, public DerivedClass2
    28 {
    29     int e;
    30 public:
    31     void DerivedDerivedCommonFunction();
    32     void virtual VirtualFunction();
    33 };
    复制代码

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

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

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

    DerivedClass2的情况是类似于DerivedClass1的。

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

    这里有两份虚表了,分别针对DerivedClass1与DerivedClass2,在&DerivedDericedClass_meta下方的数字是首地址偏移量,靠下面的虚表的那个-16表示指向这个虚表的虚指针的内存偏移,这正是DerivedClass2中的{vfptr}在DerivedDerivedClass的内存偏移。

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

    复制代码
     1 class DerivedClass1: virtual public Base
     2 {
     3     int c;
     4 public:
     5     void DerivedCommonFunction();
     6     void virtual VirtualFunction();
     7 };
     8 
     9 class DerivedClass2 : virtual public Base
    10 {
    11     int d;
    12 public:
    13     void DerivedCommonFunction();
    14     void virtual VirtualFunction();
    15 };
    16 
    17 class DerivedDerivedClass :  public DerivedClass1, public DerivedClass2
    18 {
    19     int e;
    20 public:
    21     void DerivedDerivedCommonFunction();
    22     void virtual VirtualFunction();
    23 };
    复制代码

    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}在内存中的偏移。

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

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

    1. 每个类都有虚指针和虚表;

    2. 如果不是虚继承,那么子类将父类的虚指针继承下来,并指向自身的虚表(发生在对象构造时)。有多少个虚函数,虚表里面的项就会有多少。多重继承时,可能存在多个的基类虚表与虚指针;

    3. 如果是虚继承,那么子类会有两份虚指针,一份指向自己的虚表,另一份指向虚基表,多重继承时虚基表与虚基表指针有且只有一份。

  • 相关阅读:
    Android 开发 深入理解Handler、Looper、Messagequeue 转载
    Android 开发 Handler的基本使用
    Java 学习 注解
    Android 开发 AlarmManager 定时器
    Android 开发 框架系列 百度语音合成
    Android 开发 框架系列 Google的ORM框架 Room
    Android 开发 VectorDrawable 矢量图 (三)矢量图动画
    Android 开发 VectorDrawable 矢量图 (二)了解矢量图属性与绘制
    Android 开发 VectorDrawable 矢量图 (一)了解Android矢量图与获取矢量图
    Android 开发 知晓各种id信息 获取线程ID、activityID、内核ID
  • 原文地址:https://www.cnblogs.com/0xHack/p/9401729.html
Copyright © 2011-2022 走看看