一、普通多继承时子类的内存布局

class CTestA {
    int m_data;
};

class CTestB : virtual public CTestA {
};

class CTestC : virtual public CTestA {
};

class CTestD : public CTestB, public CTestC {
};

内存布局:

1>class CTestD    size(8):
1>    +---
1> 0    | +--- (base class CTestB)
1> 0    | | +--- (base class CTestA)
1> 0    | | | m_data
1>    | | +---
1>    | +---
1> 4    | +--- (base class CTestC)
1> 4    | | +--- (base class CTestA)
1> 4    | | | m_data
1>    | | +---
1>    | +---
1>    +---

CTestD的大小是8,分别包含了一份CTestA和CTestB,他们分别是4个字节,总共8字节。

1.2 在每个类中添加私有变量

class CTestA {
    int m_data;
};

class CTestB : public CTestA {
    int m_data_b;
};

class CTestC : public CTestA {
    int m_data_c;
};

class CTestD : public CTestB, public CTestC {
    int m_data_d;
};

CTestD的大小为20,其中包含CTestA的8个字节和CTestB的8个字节以及自定义的m_data_d的四个字节:

1>class CTestD    size(20):
1>    +---
1> 0    | +--- (base class CTestB)
1> 0    | | +--- (base class CTestA)
1> 0    | | | m_data
1>    | | +---
1> 4    | | m_data_b
1>    | +---
1> 8    | +--- (base class CTestC)
1> 8    | | +--- (base class CTestA)
1> 8    | | | m_data
1>    | | +---
1>12    | | m_data_c
1>    | +---
1>16    | m_data_d
1>    +---

1.3 总结

多继承,如果没有使用virtual关键字,子类的大小为每个继承的父类的大小之和。其内存分布在变量的最开始部分,先排布父类的内存。

二、使用虚继承

2.1 单个父类虚继承

class CTestA {
    int m_data;
};

class CTestB : virtual public CTestA {
    int m_data_b;
};

class CTestC : public CTestA {
    int m_data_c;
};

class CTestD : public CTestB, public CTestC {
    int m_data_d;
};

内存排布:

1>class CTestD    size(24):
1>    +---
1> 0    | +--- (base class CTestB)
1> 0    | | {vbptr}
1> 4    | | m_data_b
1>    | +---
1> 8    | +--- (base class CTestC)
1> 8    | | +--- (base class CTestA)
1> 8    | | | m_data
1>    | | +---
1>12    | | m_data_c
1>    | +---
1>16    | m_data_d
1>    +---
1>    +--- (virtual base CTestA)
1>20    | m_data
1>    +---
1>

此时CTestD包含了以下几个部分:

  • CTestB: B采用了虚继承,其父类A的m_data不会占用D中的空间,但它内部多了一个vbptr指针,总共占用8字节
  • CTestC: C没有采用虚继承,其内部的m_data也还被D继承,没有vbptr指针,算上C自己的m_data_c共占用8个字节
  • m_data_d: D本身自己的成员变量,占用4字节
  • vitrual base CTestA: 虚继承于CTestA,包含CTestA的数据m_data,占用4个字节

CTestD共占用24个字节。

2.2 父类都使用虚继承

将CTestC也改成虚继承的方式:

class CTestC : virtual public CTestA {
    int m_data_c;
};

此时CTestD的内存分布情况为:

1>class CTestD    size(24):
1>    +---
1> 0    | +--- (base class CTestB)
1> 0    | | {vbptr}
1> 4    | | m_data_b
1>    | +---
1> 8    | +--- (base class CTestC)
1> 8    | | {vbptr}
1>12    | | m_data_c
1>    | +---
1>16    | m_data_d
1>    +---
1>    +--- (virtual base CTestA)
1>20    | m_data
1>    +---
1>

CTestD依旧占用24个字节的内存空间,不过和上面不同的是,D中属于C的8个字节空间的内容变了,其中原本属于A的m_data内存变成了vbptr指针。

2.3 总结

当子类(D)的多个父类(A,B)都继承于同一个基类(A)时,且继承时都添加了virtual关键字(属于虚继承时),子类(D)只会保存一份来自基类(A)的内存。每个父类(A,B)中会添加一个vbptr指针指向公共的基类。

三、vbptr指针

3.1 vbptr的内容

当使用虚继承时,类中会生成vbptr指针,指向公共基类的位置。vbptr中包含两个偏移量,第一个是vbptr指针在当前类中的偏移量,第二个是公共的基类在当前类中的位置。例如上面的CTestD类,其内存布局为:

两个vbtr内容的解析:

  1. CTestB.vbptr: 第一个偏移量=CTestB.vbptr - BTestB = 0,第二个偏移量=CTestA - CTestB = 22。
  2. CTestC.vbptr: 第一个偏移量=CTestC.vbptr - CTestC = 0,第二个偏移量=CTestA - CTestC = 16。

2.2 第一个偏移量的细节

上面的例子中,CTestB和CTestC的vbptr指针的第一个偏移量(以vbptr[0]表示)都是0,它表示的是这个偏移量对于当前类的偏移。因为类B和类C目前只有vbptr的指针,所有的类中,vbptr是在成员变量之前的,所以他们都是0。但是如果在类中加入一个虚函数,使类中产生虚指针,那么这个偏移量就不是0了。

修改类B:

class CTestB : virtual public CTestA {
    int m_data_b;
public:
    CTestB() {
        m_data_b = 2;
    }
    void print_data() {
        cout << m_data_b << endl;
    }
    virtual void hello() {
    }
};

打印出C的内存分布:

1>class CTestD    size(28):
1>    +---
1> 0    | +--- (base class CTestB)
1> 0    | | {vfptr}
1> 4    | | {vbptr}
1> 8    | | m_data_b
1>    | +---
1>12    | +--- (base class CTestC)
1>12    | | {vbptr}
1>16    | | m_data_c
1>    | +---
1>20    | m_data_d
1>    +---
1>    +--- (virtual base CTestA)
1>24    | m_data
1>    +---
1>

可以看到,类B中多了一个vfptr,它指向的是虚函数表的地址,此时vbptr放在第二位,CTestB.vbptr[1]的值就是-4。类B和类C的vbptr的细节:

1>CTestD::$vbtable@CTestB@:
1> 0    | -4
1> 1    | 20 (CTestDd(CTestB+4)CTestA)
1>
1>CTestD::$vbtable@CTestC@:
1> 0    | 0
1> 1    | 12 (CTestDd(CTestC+0)CTestA)

B的第1个偏移变成了-4,而C的没有变化。

最后修改:2019 年 05 月 04 日 05 : 38 PM