C++类继承内存布局(一)

时间:2023-03-09 19:50:23
C++类继承内存布局(一)

转自:http://blog.****.net/jiangyi711/article/details/4890889#

一 类布局

不同的继承方式将导致不同的内存布局

1)C结构

C++基于C,所以C++基本上兼容C。特别地,C++规范在“结构”上使用了和C相同的,简单的内存布局原则:成员变量按其被声明的顺序排列,按具体实现所规定的对齐原则在内存地址上对齐。

struct A {
char c;
int i;
};

C++类继承内存布局(一)

从上图可见,A在内存中占有8个字节,按照声明成员的顺序,前4个字节包含一个字符(实际占用1个字节,3个字节空着,补对齐),后4个字节包含一个整数。A的指针就指向字符开始字节处。

2)有C++特征的结构:

C++本质上是面向对象的语言:包含继承、封装,以及多态

原始的C结构经过改造,成了面向对象世界的基石——类。

除了成员变量外,C++类还可以封装成员函数和其他东西。

C++类实例的大小完全取决于一个类及其基类的成员变量,以及为了实现虚函数和虚继承而引入的隐藏成员变量。成员函数基本上不影响类实例的大小。

struct B {
public:
int bm1;
protected:
int bm2;
private:
int bm3;
static int bsm;
void bf();
static void bsf();
typedef void* bpv;
struct N { };
};

这里B是一个C结构,然而,该结构有一些C++特征:控制成员可见性的public/protected/private关键字、成员函数、静态成员,以及嵌套的类型声明

实际上,只有成员变量才占用类实例的空间 

类中的成员函数存放在代码区,静态函数也存放在代码区,而不是静态区。静态成员函数与一般成员函数的唯一区别就是没有this指针,因此不能访问非静态数据成员

C++类继承内存布局(一)

B中,为何static int bsm不占用内存空间?因为它是静态成员,该数据存放在程序的数据段中,不在类实例中

3)单继承

struct C
{
int c1;
void cf();
}; struct D : C
{
int d1;
void df();
};

C++类继承内存布局(一)

C++类继承内存布局(一)

派生类要保留基类所有的属性和行为,每个派生类的实例都包含了一份完整的基类实例数据

在D中,并不是说基类C的数据一定要放在D的数据之前,只不过这样放的话,能够保证D中的C对象地址,恰好是D对象地址的第一个字节

在这种安排下,有了派生类D的指针,要获得基类C的指针,就不必要计算偏移量了

即在单继承模式下,每个派生类都简单的把自己的成员变量添加到基类的成员变量之后

4)多重继承

struct C {
int c1;
void cf();
}; struct E {
int e1;
void ef();
}; struct F : C , E {
int f1;
void ff();
};

C++类继承内存布局(一)

机构F从C和E多重继承得来,与单继承不同的是,F实例靠内了每个基类的所有数据。

与单继承不同的是,在多重继承下,内嵌的两个基类的对象指针不可能全都与派生类对象指针相同

VC++按照基类的声明顺序,先排列基类实例数据,最后才排列派生类实例数据,派生类数据本身也是按照声明顺序布局的(在有虚函数的情况下,这个规则有所不同)

5)虚继承

 考虑下面这种继承层次:

struct A {};
struct B :A {};
struct C :A {};
struct D :B ,C {};

则在D的实例中,将包含两个A的实例,这两个实例分别来自B和C,这导致了额外的内存开销,并且会造成混乱(对于D,不知道如何区分两个A的实例)

所以出现了虚继承

struct A {};
struct B : virtual A {};
struct C : virtual A {};
struct D : B , C {};

使用虚继承,比单继承和多重继承将有更大的实现开销和调用开销:

在单继承或多重继承下,内嵌的基类实例地址与派生类的实例地址相比,要么地址相同,要么相差一个固定的偏移量

当虚继承时,一般说来,派生类地址和其虚基类地址之间的偏移量是不固定的,因为派生类如果被进一步继承的话,最终派生类会把共享的虚基类实例数据放到一个与上一层派生类不同的偏移量处:

struct G : virtual C {
int g1;
void gf();
};

C++类继承内存布局(一)

vbptr虚基类表指针:

GdGvbptrG:在G中G对象的指针与G的虚基类表指针之间的偏移量,在此可见为0,因为G对象内存布局第一项就是虚基类表指针

GdGvbptrC:在G中C对象的指针与G的虚基类表指针之间的偏移量,在此可见为8

struct H : virtual C {
int h1;
void hf();
};

C++类继承内存布局(一)

struct I : G, H {
int i1;
void _if();
};

C++类继承内存布局(一)

从上面这些图可以看出

在G对象中,内嵌的C基类的数据紧跟在G的数据之后,在H对象中,内嵌的C基类对象的数据紧很在H的数据之后,但在I对象中,内存的布局并非如此

在VC++中,对每个继承自虚基类的类实例,将增加一个隐藏的虚基类表指针成员变量,从而达到间接计算虚基类位置的目的。该变量指向一个全类共享的偏移量表,表中记录了对于该类而言,虚基类表指针与虚基类之间的偏移量

可以得到下列关于VC++虚拟继承下内存布局的结论:

1):首先排列非虚继承的基类实现

2):有虚基类时,为每个基类增加一个隐藏的vbptr指针,除非已经从非虚继承的类那里继承了一个vbptr

3):排列派生类的数据成员

4):在实例最后,排列每个虚基类的一个实例