一文详解C++类的内存布局和虚函数底层实现机制

2021-02-02 19:15

阅读:779

标签:对象   类对象   constant   mamicode   就会   vfp   alt   contain   amp   

目录
  • VS2015 查看内存布局
  • 1 空类
  • 2 普通类
  • 3 虚函数的类
  • 4 不含虚函数的普通继承
    • 4.1 单继承的类
    • 4.2 两个继承类
    • 4.3 菱形继承类
  • 5 含虚函数的普通继承
    • 5.1.1 单继承,子类不含虚函数
    • 5.1.2 单继承,子类含虚函数
    • 5.2 多个继承
    • 5.3 菱形继承
      • 从上面的菱形继承,我们发现,菱形继承存在二义性,而且内存布局有重复,空间消耗大,因此需要虚继承。
  • 6 虚继承
    • 6.1.1 基类不含虚函数,子类也不含虚函数的单虚继承
    • 6.1.2 基类含有虚函数,子类不含虚函数的单虚继承
    • 6.1.3 基类含有虚函数,子类也含有虚函数的单虚继承
    • 6.2.多继承
    • 6.3.1 不含虚函数的菱形继承
    • 6.3.1 基类含虚函数,中间类不含单独的虚函数的菱形继承
    • 6.3.1 基类含虚函数,中间类含单独虚函数,但自己不含虚函数的菱形继承
    • 6.3.2 基类含虚函数,中间类含单独虚函数,但自己含虚函数的菱形继承
  • 总结

VS2015 查看内存布局

1 打开VS,创建一个项目, 项目 --> 属性--> C/C++ --> 命令行
2 添加可选项 /d1reportSingleClassLayout
3 运行程序
4 调出输出窗口:Alt+12,下拉输出来源,选择生成顺序。
技术图片

1 空类

class C
{
public:
	C() { cout 

内存布局:占据1个字节。

1>  class C	size(1):
1>  	+---
1>  	+---
1>

为什么空类大小为1字节:因为c++有规定,任何不同的对象都应该有不同的地址。空类虽然没有内容,但是仍旧需要分配地址,表明有这个对象的存在,另外所占字节大小与编译器有关。 另外说明添加了构造和析构函数,说明类的非虚函数不占用类对象的内存(函数编译后形成二进制文件放在内存中的代码段区)

2 普通类

class A
{
public:
	int a;
	char b;
	void func() { cout 

内存布局:占据8个字节,(四字节对齐,所以需要八个字节),如下所示:

1>  class A	size(8):
1>  	+---
1>   0	| a
1>   4	| b
1>    	|  (size=3)
1>  	+---

3 虚函数的类

class A
{
public:
	int a;
	char b;
	virtual void Afunc1() { cout 

内存布局:占据12个字节,多了一个虚函数指针,如下所示: 虚函数指针指向一张虚表,虚表里面存储了虚函数Afunc1(),Afunc2()的入口地址。

1>  class A	size(12):
1>  	+---
1>   0	| {vfptr}
1>   4	| a
1>   8	| b
1>    	|  (size=3)
1>  	+---

虚表

1>  A::$vftable@:
1>  	| &A_meta
1>  	|  0
1>   0	| &A::Afunc1
1>   1	| &A::Afunc2

4 不含虚函数的普通继承

4.1 单继承的类

B->A

class A
{
public:
	int a;
	char b;
};

class B:public A
{
public:
	int _c;
};

子类B的内存布局,先是继承基类A的属性,然后再自己的属性。

1>  class A	size(8):
1>  	+---
1>   0	| a
1>   4	| b
1>    	|  (size=3)
1>  	+---
1>
1>  class B	size(12):
1>  	+---
1>   0	| +--- (base class A)
1>   0	| | a
1>   4	| | b
1>    	| |  (size=3)
1>  	| +---
1>   8	| _c
1>  	+---
1>

4.2 两个继承类

C->A,B->A

C 同上B

1>  class C	size(12):
1>  	+---
1>   0	| +--- (base class A)
1>   0	| | a
1>   4	| | b
1>    	| |  (size=3)
1>  	| +---
1>   8	| _c
1>    	|  (size=3)
1>  	+---

4.3 菱形继承类

D->C,D->B, C->A,B->A

ABC同上,我们看到D类的内存大小不是简单的a,b,_b,_c,d所占的20个字节,而是28字节,爷爷类的ab,保存了两份,因此访问ab时会存在二义性,因此有两个解决方案:1 指定所属作用域,说明是B类的还是C类的。2采用虚继承。另外内存布局遵循前面的规则:继承的属性放在最前面,自己多的属性放在最后面。

1>  class D	size(28):
1>  	+---
1>   0	| +--- (base class B)
1>   0	| | +--- (base class A)
1>   0	| | | a
1>   4	| | | b
1>    	| | |  (size=3)
1>  	| | +---
1>   8	| | _b
1>  	| +---
1>  12	| +--- (base class C)
1>  12	| | +--- (base class A)
1>  12	| | | a
1>  16	| | | b
1>    	| | |  (size=3)
1>  	| | +---
1>  20	| | _c
1>    	| |  (size=3)
1>  	| +---
1>  24	| d
1>  	+---

5 含虚函数的普通继承

5.1.1 单继承,子类不含虚函数

class A
{
public:
	int a;
	char b;
	void func(){ cout 
1>  class A	size(12):
1>  	+---
1>   0	| {vfptr}
1>   4	| a
1>   8	| b
1>    	|  (size=3)
1>  	+---

//A类的虚表:存的A的虚函数地址
1>  A::$vftable@:
1>  	| &A_meta
1>  	|  0
1>   0	| &A::Afunc1
1>
1>  A::Afunc1 this adjustor: 0
1>
1>  class std::is_error_code_enum	size(1):
1>  	+---
1>   0	| +--- (base class std::integral_constant)
1>  	| +---
1>  	+---
1>
1>  class B	size(16):
1>  	+---
1>   0	| +--- (base class A)
1>   0	| | {vfptr}
1>   4	| | a
1>   8	| | b
1>    	| |  (size=3)
1>  	| +---
1>  12	| _b
1>  	+---

//B类的虚表:存的是B类的虚函数地址,因此同一个函数Afunc1,由于虚表的因故,地址不同,实现了多态性。
1>  B::$vftable@:
1>  	| &B_meta
1>  	|  0
1>   0	| &B::Afunc1

到此我们可以发现,虚函数指针的位置总是放在最前面的,设想如果B也有自己的虚函数,内存布局又是怎么样的呢?

5.1.2 单继承,子类含虚函数

class A
{
public:
	int a;
	char b;
	void func(){ cout 

A类的内存布局同上,但是B类的内存布局改变了,看一下和你想象的一样吗? 仅在虚函数表里面也增加了Bfunc()这个虚函数的入口地址。

1>  class B	size(16):
1>  	+---
1>   0	| +--- (base class A)
1>   0	| | {vfptr}
1>   4	| | a
1>   8	| | b
1>    	| |  (size=3)
1>  	| +---
1>  12	| _b
1>  	+---

//B类的虚表
1>  B::$vftable@:
1>  	| &B_meta
1>  	|  0
1>   0	| &B::Afunc1
1>   1	| &B::Bfunc

5.2 多个继承

C->A,B->A

C的内存布局类似B;

5.3 菱形继承

D->C,D->B, C->A,B->A

ABC的内存布局同上,但是D的内存布局又是怎么样的呢?先想象一下。

class A
{
public:
	int a;
	char b;
	void func(){ cout 

内存布局:相比没有虚函数的内存布局,仅仅多了两个虚函数指针。

1>  class D	size(36):
1>  	+---
1>   0	| +--- (base class B)
1>   0	| | +--- (base class A)
1>   0	| | | {vfptr}
1>   4	| | | a
1>   8	| | | b
1>    	| | |  (size=3)
1>  	| | +---
1>  12	| | _b
1>  	| +---
1>  16	| +--- (base class C)
1>  16	| | +--- (base class A)
1>  16	| | | {vfptr}
1>  20	| | | a
1>  24	| | | b
1>    	| | |  (size=3)
1>  	| | +---
1>  28	| | _c
1>    	| |  (size=3)
1>  	| +---
1>  32	| d
1>  	+---

虚函数表有两张,而且根据继承的顺序,表的虚函数地址也不一样,请仔细品。

1>  D::$vftable@B@:
1>  	| &D_meta
1>  	|  0
1>   0	| &D::Afunc1
1>   1	| &B::Bfunc
1>   2	| &D::Dfunc
1>
1>  D::$vftable@C@:
1>  	| -16
1>   0	| &thunk: this-=16; goto D::Afunc1
1>   1	| &C::Cfunc

从上面的菱形继承,我们发现,菱形继承存在二义性,而且内存布局有重复,空间消耗大,因此需要虚继承。

6 虚继承

6.1.1 基类不含虚函数,子类也不含虚函数的单虚继承

class A
{
public:
	int a;
	char b;
	void func(){ cout 

B的内存布局:1 多了一个虚基指针,放在了最前面,而且B的成员也放在了最前面,基类被放到了最后面,和非虚继承的内存布局形式完全相反。

1>  class B	size(16):
1>  	+---
1>   0	| {vbptr}
1>   4	| _b
1>  	+---
1>  	+--- (virtual base A)
1>   8	| a
1>  12	| b
1>    	|  (size=3)
1>  	+---
1>

虚基表:虚基表里面存放了虚基类的入口地址: 从B类地址开始偏移8个字节就是虚基类的地址,结合内存布局看,容易理解。

1>  B::$vbtable@:
1>   0	| 0
1>   1	| 8 (Bd(B+0)A)

6.1.2 基类含有虚函数,子类不含虚函数的单虚继承

可以先想象一下,含有虚函数之后,内存布局会是怎样?根据虚函数的特性,会有一个虚函数指针和虚函数表,那虚函数指针又是放在内存的哪个地方呢?

class A
{
public:
	int a;
	char b;
	void func(){ cout 

没什么变化,仅仅是基类增加了一个虚函数指针,而继承类没有虚函数指针。

1>  class B	size(20):
1>  	+---
1>   0	| {vbptr}//虚基指针
1>   4	| _b
1>  	+---
1>  	+--- (virtual base A)
1>   8	| {vfptr}//虚函数指针
1>  12	| a
1>  16	| b
1>    	|  (size=3)
1>  	+---

//虚基表
1>  B::$vbtable@:
1>   0	| 0
1>   1	| 8 (Bd(B+0)A)

//虚函数表
1>  B::$vftable@:
1>  	| -8
1>   0	| &B::Afunc1
1>

6.1.3 基类含有虚函数,子类也含有虚函数的单虚继承

再子类也含有虚函数之后,内存布局又会是怎样?会像非虚继承那样,直接将自己的虚函数放入继承自基类的虚函数指针指向的虚函数表吗?

class A
{
public:
	int a;
	char b;
	void func(){ cout 

恐怕就不是了。

内存布局,此时子类拥有了自己独立的虚函数指针,并且放在了起始地址,有用两张虚表。

1>  class B	size(24):
1>  	+---
1>   0	| {vfptr}
1>   4	| {vbptr}
1>   8	| _b
1>  	+---
1>  	+--- (virtual base A)
1>  12	| {vfptr}
1>  16	| a
1>  20	| b
1>    	|  (size=3)
1>  	+---

//存放B类独立的虚函数的虚函数表1
1>  B::$vftable@B@:
1>  	| &B_meta
1>  	|  0
1>   0	| &B::Bfunc
//虚基表
1>  B::$vbtable@:
1>   0	| -4
1>   1	| 8 (Bd(B+4)A)

//存放B类继承父类虚函数的虚函数表2
1>  B::$vftable@A@:
1>  	| -12
1>   0	| &B::Afunc1
1>

6.2.多继承

C->A,B->A

C的内存布局同单继承的B

6.3.1 不含虚函数的菱形继承

从上面虚继承内存布局形式来看,子类的属性会放在最前面,然后是继承类的属性,那么三层的菱形继承,内存布局会是先D,再B,C,最后A吗?

class A
{
public:
	int a;
	char b;
	void func(){ cout 

D的内存布局:em和想象的有一点相反,A类的属性被放在了BC之前。但是只保留了一份,不再是无虚继承时的两份,节约了空间,而且访问a,b不再二义性了, 而且存在三个虚基指针,显然也指向了三张虚基表

1>  class D	size(32):
1>  	+---
1>   0	| {vbptr}
1>   4	| d
1>  	+---
1>  	+--- (virtual base A)
1>   8	| a
1>  12	| b
1>    	|  (size=3)
1>  	+---
1>  	+--- (virtual base B)
1>  16	| {vbptr}
1>  20	| _b
1>  	+---
1>  	+--- (virtual base C)
1>  24	| {vbptr}
1>  28	| _c
1>    	|  (size=3)
1>  	+--

虚基表,基类内存布局更容易懂哦

//D的虚基指针
1>  D::$vbtable@D@:
1>   0	| 0
1>   1	| 8 (Dd(D+0)A) //基类A的起始地址是D类起始偏移为8的地址
1>   2	| 16 (Dd(D+0)B)//基类B的起始地址是D类起始偏移为16的地址
1>   3	| 24 (Dd(D+0)C)//基类C的起始地址是D类起始偏移为24的地址

//B的虚基指针
1>  D::$vbtable@B@:
1>   0	| 0
1>   1	| -8 (Dd(B+0)A)//基类A的起始地址是B的起始偏移为-8的地址,16-8=8

//C的虚基指针
1>  D::$vbtable@C@:
1>   0	| 
1>   1	| -16 (Dd(C+0)A)//基类A的起始地址是C的起始偏移为-16的地址,24-16=8

6.3.1 基类含虚函数,中间类不含单独的虚函数的菱形继承

我们又可以合理推断了,根据6.1.2,如果自己没有单独的虚函数,自己是不存在单独虚函数指针的,直接继承了父类的虚函数指针,重写了所指向的虚函数表。因此内存布局中仍然只有一个虚函数指针,存放于基类A的地方。

class A
{
public:
	int a;
	char b;
	void func(){ cout 

内存布局

1>  class D	size(36):
1>  	+---
1>   0	| {vbptr}
1>   4	| d
1>  	+---
1>  	+--- (virtual base A)
1>   8	| {vfptr}
1>  12	| a
1>  16	| b
1>    	|  (size=3)
1>  	+---
1>  	+--- (virtual base B)
1>  20	| {vbptr}
1>  24	| _b
1>  	+---
1>  	+--- (virtual base C)
1>  28	| {vbptr}
1>  32	| _c
1>    	|  (size=3)
1>  	+---

虚基表和虚函数表

1>  D::$vbtable@D@:
1>   0	| 0
1>   1	| 8 (Dd(D+0)A)
1>   2	| 20 (Dd(D+0)B)
1>   3	| 28 (Dd(D+0)C)
1>
1>  D::$vftable@:
1>  	| -8
1>   0	| &D::Afunc1
1>
1>  D::$vbtable@B@:
1>   0	| 0
1>   1	| -12 (Dd(B+0)A)
1>
1>  D::$vbtable@C@:
1>   0	| 0
1>   1	| -20 (Dd(C+0)A)

6.3.1 基类含虚函数,中间类含单独虚函数,但自己不含虚函数的菱形继承

相信到这里,就很容易想到,D类的内存布局只会增加B,C类的虚函数指针,增加8个字节。

class A
{
public:
	int a;
	char b;
	void func(){ cout 

内存布局

1>  class D	size(44):
1>  	+---
1>   0	| {vbptr}
1>   4	| d
1>  	+---
1>  	+--- (virtual base A)
1>   8	| {vfptr}
1>  12	| a
1>  16	| b
1>    	|  (size=3)
1>  	+---
1>  	+--- (virtual base B)
1>  20	| {vfptr}
1>  24	| {vbptr}
1>  28	| _b
1>  	+---
1>  	+--- (virtual base C)
1>  32	| {vfptr}
1>  36	| {vbptr}
1>  40	| _c
1>    	|  (size=3)
1>  	+---

虚基表和虚函数表

1>  D::$vbtable@D@:
1>   0	| 0             //说明此虚基指针距离D的起始地址 的距离,为0,说明D类不含自己的虚函数
1>   1	| 8 (Dd(D+0)A)  //说明此虚基指针距离A的起始地址 的距离,为8
1>   2	| 20 (Dd(D+0)B)
1>   3	| 32 (Dd(D+0)C)
1>
1>  D::$vftable@A@:
1>  	| -8            //说明来自A的虚函数距离D的起始地址 的距离,为-8
1>   0	| &D::Afunc1    //说明此虚基指针指向的函数入口地址是D类重写的Afunc1
1>
1>  D::$vftable@B@:
1>  	| -20
1>   0	| &D::Bfunc
1>
1>  D::$vbtable@B@:
1>   0	| -4            //说明B类的虚基指针距离B的起始地址 的距离,为-4,说明有虚函数指针。
1>   1	| -16 (Dd(B+4)A)//说明B类的虚基指针距离继承的A的起始地址 的距离,为-16
1>
1>  D::$vftable@C@:
1>  	| -32
1>   0	| &D::Cfunc
1>
1>  D::$vbtable@C@:
1>   0	| -4
1>   1	| -28 (Dd(C+4)A)

6.3.2 基类含虚函数,中间类含单独虚函数,但自己含虚函数的菱形继承

这就不用多说了把,又会增加一个虚函数指针,增加4字节。

内存布局

1>  class D	size(48):
1>  	+---
1>   0	| {vfptr}
1>   4	| {vbptr}
1>   8	| d
1>  	+---
1>  	+--- (virtual base A)
1>  12	| {vfptr}
1>  16	| a
1>  20	| b
1>    	|  (size=3)
1>  	+---
1>  	+--- (virtual base B)
1>  24	| {vfptr}
1>  28	| {vbptr}
1>  32	| _b
1>  	+---
1>  	+--- (virtual base C)
1>  36	| {vfptr}
1>  40	| {vbptr}
1>  44	| _c
1>    	|  (size=3)
1>  	+---

虚函数表和虚基表

1>  D::$vftable@:
1>  	| &D_meta
1>  	|  0
1>   0	| &D::Dfunc
1>
1>  D::$vbtable@D@:
1>   0	| -4
1>   1	| 8 (Dd(D+4)A)
1>   2	| 20 (Dd(D+4)B)
1>   3	| 32 (Dd(D+4)C)
1>
1>  D::$vftable@A@:
1>  	| -12
1>   0	| &D::Afunc1
1>
1>  D::$vftable@B@:
1>  	| -24
1>   0	| &D::Bfunc
1>
1>  D::$vbtable@B@:
1>   0	| -4
1>   1	| -16 (Dd(B+4)A)
1>
1>  D::$vftable@C@:
1>  	| -36
1>   0	| &D::Cfunc
1>
1>  D::$vbtable@C@:
1>   0	| -4
1>   1	| -28 (Dd(C+4)A)

总结

虚基表

采用虚继承,继承类就会多一个虚基指针,指向虚基表,虚基表里面存放了基类的入口地址

虚基指针所指向的虚基表的内容:

  1. 虚基指针的第一条内容表示的是该虚基指针距离所在的子对象的首地址的偏移
  2. 虚基指针的第二条内容表示的是该虚基指针距离虚基类子对象的首地址的偏移

虚函数表

采用虚函数,继承类就会多一个虚函数指针,指向虚函数表,虚函数表里面存放了基类的入口地址
1. 每个基类都有自己的虚函数表
2. 派生类如果有自己的虚函数,会被加入到第一个虚函数表之中
3. 内存布局中,其基类的布局按照基类被声明时的顺序进行排列
4. 派生类会覆盖基类的虚函数,只有第一个虚函数表中存放的是真实的被覆盖的函数的地址;其它的虚函数表中存放的并不是真实的对应的虚函数的地址,而只是一条跳转指令

一文详解C++类的内存布局和虚函数底层实现机制

标签:对象   类对象   constant   mamicode   就会   vfp   alt   contain   amp   

原文地址:https://www.cnblogs.com/Alexkk/p/12808082.html


评论


亲,登录后才可以留言!