C++类的内存布局

关于C++中类的内存布局,是比较基础而重要的一个内容。先转一篇老文章,因其使用了Visual Studio的特殊功能而能够写得很简单清晰,不过当时作者用的应该是32机器,故指针大小是4字节而不是8字节。
笔者手头没有Visual Studio,故转载之。也可以按陈皓早年的文章用打印地址的方式分析。不过因为本文的方式非常清晰易懂,故还是想转载下这篇。遇到与64位的不同处,本文会略作说明。
本文对原文略作了一些增删: 增加了前2节,对第3节做了不少的删减合并与结构调整,不过基本保留了原图与代码。

原文地址:https://www.cnblogs.com/jerry19880126/p/3616999.html

1. 成员函数

成员函数的地址,在编译期确定,静态或动态地绑定在对象上。
非虚函数,是静态绑定的,可以通过this指针访问到其在代码段中的地址,故不需要在对象的内存中存储非虚函数的地址;
虚函数,是通过虚表动态绑定的,对象内存中有虚表地址,故也不需要在对象内存中保存虚函数地址。

2. 静态成员

无论是静态的成员变量或成员函数,都是整个类的属性,而不是单个对象特有的,因此不会在类的对象的内存留有它们的空间。

3. 类的内存布局

Visual Studio 中的 Debug 配置的“命令行”属性中,可添加 "/d1 reportAllClassLayout" 或 "/d1 reportSingleClassLayoutXXX", 则会打印所有类的或指定类的布局。
(注: 以下图中分析皆基于32位机器,即指针大小为4字节的情况)

注:关于对象的内存布局,大致各编译器都差不多;但是关于虚表内部的结构,则大约会有一些不同。
 

3.1 无虚函数的基类与继承类

class Base
{
    int a;
    int b;
public:
    void CommonFunction();
};

class DerivedClass: public Base
{
    int c;
public:
    void DerivedCommonFunction();
};

内存布局图如下:

    

成员变量依据声明的顺序进行排列(类内偏移为0开始)。

注:在64位机器上,DerivedClass大小也是12,因为这里最大类型就是int,就按4字节对齐即可。

 

3.2 带虚函数的基类与继承类

class Base
{
    int a;
    int b;
public:
    void CommonFunction();
    void virtual VirtualFunction();
};

class DerivedClass: public Base
{
    int c;
public:
    void DerivedCommonFunction();
    void virtual VirtualFunction();
};

内存布局如下:

   

这个图中就多了一个虚表了。

在类的内存布局中,将虚表指针放在了内存的开始处(0地址偏移)。(这也很好理解,这样编译器最容易实现,找到虚表很容易)

注: 
若是64位机器,则Base大小为16字节,因为虚表指针占8字节,而a和b刚好占8字节能够对齐;
但DerivedClass大小则为24字节,因为多出来的c需按8字节对齐。

下面把子类的代码改一下:继承基类的虚函数,加一个自己的虚函数。

class DerivedClass1 : public Base
{
    int c;
public:
    void DerivedCommonFunction();
    void virtual VirtualFunction2();
};

内存布局如下:

可以看到,在虚表中,现在有2个虚函数了:第1个是Base类的,第2个是自己的;而虚表指针还是只有那一个。

如果把子类改造成重写Base的虚函数,再另加一个自己的虚函数:

class DerivedClass1 : public Base
{
    int c;
public:
    void DerivedCommonFunction();
    void virtual VirtualFunction();
    void virtual VirtualFunction2();
};

内存布局如下:

 

3.3 钻石型普通多重继承

class Base
{
    int a;
    int b;
public:
    void CommonFunction();
    void virtual VirtualFunction();
};


class DerivedClass1: public Base
{
    int c;
public:
    void DerivedCommonFunction();
    void virtual VirtualFunction();
};

class DerivedClass2 : public Base
{
    int d;
public:
    void DerivedCommonFunction();
    void virtual VirtualFunction();
};

class DerivedDerivedClass : public DerivedClass1, public DerivedClass2
{
    int e;
public:
    void DerivedDerivedCommonFunction();
    void virtual VirtualFunction();
};

内存布局如下:

  

孙子类DerivedDerivedClass的内存布局如下:

  

注:

在64位机器上,DerivedClass1和DerivedClass2都是占24字节(按8对齐),因此,DerivedDerivedClass占48字节。

另:最后图中的-16,原文解释是“DerivedClass2中的{vfptr}在DerivedDerivedClass的内存偏移”,笔者猜测这里的意思是:从当前虚表回到对象首地址,offset是多少,那就是负值了。

由上五幅图可见,普通的这种钻石型继承,孙子类会简单地将2个基类排在自己的内存中,并没有做什么变化。

 

3.4 钻石型虚拟继承

class DerivedClass1: virtual public Base
{
    int c;
public:
    void DerivedCommonFunction();
    void virtual VirtualFunction();
};

class DerivedClass2 : virtual public Base
{
    int d;
public:
    void DerivedCommonFunction();
    void virtual VirtualFunction();
};

class DerivedDerivedClass :  public DerivedClass1, public DerivedClass2
{
    int e;
public:
    void DerivedDerivedCommonFunction();
    void virtual VirtualFunction();
};

注意:DerivedDerivedClass在继承2个父类的时候,没有用virtual关键字。

Base类没有变化,DerivedClass1类内存布局如图:

注: 64位机器上,DerivedClass1的size是 8+8+8+8 = 32

DerivedClass2与DerivedClass1类似,就不再列出。

在虚拟继承后,出现了2个虚表指针。内存布局中,先排自己的虚表指针与成员变量;然后再排虚基类的部分。虚基类也有一个虚表,所以也有一个虚表指针。

最后看 DerivedDerivedClass的内存布局:

 

注: 64位机器上,DerivedDerivedClass的大小是48,首先是按8对齐,其次,上图中d和e可以align到一个8字节里,因此刚好是6个8字节,即48字节。

首先排2个父虚基类的内存,最后只需要排一个祖父虚基类的内存。

因为2个父基类都是虚拟继承了祖父基类,因此在孙子这里祖父基类确实只需要一份copy了,但是每个祖先类里面都需要一个虚表指针了,这就是代价。

 

(完)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值