一、普通多继承时子类的内存布局
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内容的解析:
CTestB.vbptr
: 第一个偏移量=CTestB.vbptr - BTestB
= 0,第二个偏移量=CTestA - CTestB
= 22。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的没有变化。
此处评论已关闭