C++对象模型(内存布局)

最近学习老王的对象模型,记录下学习历程和心得吧:

1.普通类对象成员布局

在这里插入图片描述

先不讨论有虚函数的情况,我们先说基本概念:
普通类成员布局中的 普通成员变量跟着对象走(放在堆或者栈区取决于对象生成的方式);
静态(全局)变量编译期间已经确认地址跟着类走,有内存的放在数据段,未初始化的放在BSS;
普通成员函数(包括静态)都是跟着类走的,编译期间确认地址代码内容置于代码段;
声明:以下情况均说的是Visaul Studio 2017 32位 MSVC编译器的情况。

**

2.普通继承情况下的类对象成员布局

**
在这里插入图片描述

这里也先不讨论有虚函数的情况,子类对象包含父类所有的数据成员,其对象成员分布比较简单如下:
在这里插入图片描述

这里先申明一个概念:子类对象构造时,先调用父类的函数即先构造父类子对象再构造子类子对象,组成该子类对象的对象模型;析构则正好相反。

3.类中带虚函数的情况

当类中存在至少一个虚函数的情况下,编译器在类对象的头部会安插一个4字节的虚函数表指针vptr,并在调用构造函数的时候赋予该指针的内存指向(虚函数表vtbl):
在这里插入图片描述

其类对象的成员布局如下:
在这里插入图片描述

虚函数表指针vptr指向虚函数表vtbl,虚函数表vbtl内部存放了类的虚函数表实际静态地址(编译时已经确认);
我们可以通过如下代码来取得虚函数表的内容,:
在这里插入图片描述
如上图,首地址4个字节指向的为虚函数表指针vptr,vptr指向的为虚函数表,取得的item即为虚函数,可通过函数指针强转调用既可以像普通成员函数一样调用(仅限不需要访问成员变量的时候,以上写法会丢失this指针信息,后续会说明);
----------------------------------------以上即为基本概念----------------------------------------

4.带虚函数类对象继承的情况

由于使用虚函数的情况基本用于有继承关系的情况,所以直接看但继承情况下有虚函数的类对象成员布局;
在这里插入图片描述
其子类对象的内存布局如下:
在这里插入图片描述
此时,父类和子类的虚函数表指向虚函数地址副本不同,具体调用的虚函数取决于类指针指向的动态创建的对象,产生了多态;
在这里插入图片描述

5.带虚函数的多继承情况

在这里插入图片描述
其对象布局如下:
在这里插入图片描述
此时会产生两个vptr,子类对象与Base1类子对象共用一个vptr1(继承自Base1基类,并在子类对象构造时重新被赋值),指向虚函数表vtbl1,vptr2为跳过sizeof(Base1子对象) 8个字节的Base2类子对象的首地址,指向虚函数表vtbl2:详细布局如下:
在这里插入图片描述

6.继承情况下this指针的调整

我们都知道this指针即指向类对象本身,即对象首地址,加上类成员变量的偏移值即可以访问成员变量;
也应该知道成员函数,包括虚函数访问对象内存时候都会附带this指针(编译器视角):
其实成员函数也好,静态函数,全局函数也罢,都是编译时确定地址指向,区别是成员函数会附带this指针作为隐藏参数,加上类对象成员相对于首地址的偏移来访问类对象数据成员;
在这里插入图片描述
有趣的是,在不需要this指针的成员函数中,如:
在这里插入图片描述

我们甚至不需要生成对象,即不需要对象就可以调用,因为编译期间就确定了地址,即使不需要确定this指针也能正常访问:
在这里插入图片描述
为了方便理解,这种写法我们可以理解为用空指针调用了默认构造函数生成了临时对象进行成员函数调用,只不过this指针没有确认,当访问数据成员比如虚函数表指针的时候会出现异常。
在这里插入图片描述
this指针即指向虽然指向类对象本身,但如下两种情况this指针是会动态调整的
在有继承的情况下为了正确访问基类子对象的数据成员,this指针需要动态地做出调整:
在这里插入图片描述
如上,单一继承的情况下,如果Base1类不带虚函数,而Derived类带虚函数,那么编译器只会为Derived类生成虚函数表指针,
其类对象布局如下:
在这里插入图片描述

单继承情况下,this指针指向对象首地址及vptr,我们都知道构造子类对象,父类对象会先构造,构造时打印出Base1类的this指针;
结论是Base1类子对象的this指针比子对象的首地址下偏了4个字节,即Base1类的this指针往下偏移了4个字节。
如果Base1类带了虚函数,那么子对象的vptr与父类子对象的vptr其实是共用的,Base1父类子对象也会有一份vptr,此时this指针不需要做出调整

回到多继承的情况:
在这里插入图片描述
其子类对象的内存布局如下:
在这里插入图片描述
试想当这么种情况,当Base2类的对象指针指向Derived类对象:
在这里插入图片描述

打印Base2类的成员偏移
在这里插入图片描述
会发现m_b2i仅仅偏移了4个字节,说明Base2类子对象的this指针并不是new出来这个24字节的首地址,而是指向了Base2类子对象的首地址,并返回给了Base2类指针p2,跳过了vptr1和m_bi的内存(Base1类子对象),即(编译器视角,这里其实是偷懒写法):
在这里插入图片描述
在编译器看来this指针是必须要做出调整的,才能正确访问Derived类对象包含的Base2类子对象成员以及对象本身包含的数据成员。

7.为什么虚析构须加上

接着上述多继承的情况,当城市去删除new Derived()这段内存时,应为p2的指向为Base2类子对象的首地址,直接delete p2,Base1类子对象的内存并没有得到释放,是会引起内存泄漏异常的:
在这里插入图片描述
如果强转成Derived
类型,delete是正常的,内存窗口显示Derived *d也往前t跳了8个字节,说明编译器已经将this指针动态调回了申请的对象首地址,才能正确释放内存
这样虽然可以正确释放内存,但是太不方便;
所以如果Base2类的析构函数是虚函数的话,由于p2实际指向的是一个Derived对象,执行delete p2的时,由于C++多态设计,那么肯定会查虚函数表去调用Derived类的析构函数,C++编译器很智能,如果基类有虚析构而Derived类里没有,则会为Derived派生类合成一个虚析构并且安插(跳转,有的话)析构Base1和Base2基类对象的汇编代码,保证所有的对象内存被正确释放。
所以C++有条不成文的规定,就是析构函数必须为虚函数,因为难免你编写的类会被别人继承的情况,除非不考虑继承和多态的使用。

8.虚基类虚继承内存布局

考虑这么一种情况,A1,A2子类同时继承Grand爷爷类,C1孙子类继承A1的同时也继承A2,如下图:
在这里插入图片描述
用孙子类对象调用爷爷类对象m_grand;
在这里插入图片描述
编译器会提示对C1::m_grand访问不明确,产生了二义性,因为C1有两条途径都能访问到Grand内存,一份副本保存在A1里,一份在A2里,如下图:
在这里插入图片描述

虽然可以通过加类限定符来明确二义性的所指:
在这里插入图片描述

但是Grand类还是被继承了两次,并且子类中有各自的副本,显得多余并且增加了开销;
为此,引入了虚基类虚继承的概念,如下三层结构(三层继承关系才可能存在虚基类虚继承的情况)
在这里插入图片描述

对应代码:
在这里插入图片描述
加入虚继承后,访问爷爷类的类对象数据成员不再会出现二义性,但要注意的是如果Grand类带参构造,孙子类C1就必须对其的构造负责,在其初始化列表里初始化Grand类,而不是放在传统继承关系的子类中:
在这里插入图片描述
先考虑虚基类单虚继承的内存布局:
在这里插入图片描述
A1类对象"A1 a"的内存布局如下:
在这里插入图片描述
此时编译器会为A1类的对象成员安插一个vbptr虚基类表指针,指向虚基类表vbtbl;
三层完成模型如下:
在这里插入图片描述
下面说下虚基类表vbtbl里面的内容:
一般,虚基类表为8字节,4字节为一个单位,如果有多个虚基类(class A1: virtual public Grand1,virtual public Grand2…),每过一个,虚基类表的长度加4个字节;
虚基类表5~8个字节存放的是虚基类表指针vbptr这个成员变量的首地址和虚基类Grand子对象首地址之间的偏移量:
在这里插入图片描述
在本例中,为0x00000008,即跳过8个字节就能访问到Grand类子对象的数据成员,而不需要额外产生一个Grand类的对象副本
虚基类表14个字节存放的是子类对象首地址与其虚基类成员变量vbptr地址之间的偏移,因为本例对象首地址等于vbptr,所以14字节的内容自然是0x00000000;
考虑这种情况:
在这里插入图片描述

A1子类正常继承Grand,虚继承Grand2,那么A1类的对象模型如下:
在这里插入图片描述
虚基类表1~4个字节存放的是子类对象首地址与其虚基类成员变量vbptr地址之间的偏移,因为Grand基类子对象的存在,vbptr需要从虚函数表拿到与对象首地址的偏移值才能正常访问正常继承的Grand类数据成员。
三层结构的话,可以类推:
在这里插入图片描述
孙子类C1的对象包含两个vbptr,分别指向两张虚函数表,虚函数表里14字节分别记录了vbptr自身与对象首地址的偏移,58字节里记录了vbptr于虚基类子对象首地址的偏移量,C++编译器正式靠着这种机制确保程序员内存访问的正确性,多态性,效率性。
以一图总括上述内容:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值