C++对象内存模型布局详解

目录

本文主要内容如下:

最后还有一些问题:

一、理解虚函数表

二、对象模型概述

三、继承下的C++对象模型

单继承:

多继承:

一般的多继承(非菱形继承):

菱形继承:

五、虚继承

5.1虚基类表解析:

5.2简单虚继承

5.3虚拟多继承

5.4虚拟菱形继承

六、C++封装带来的布局成本是多大?

七、下面这个空类构成的继承层次中,每个类的大小是多少?


由于本人在查找答案时,发现绝大多数博文都是相同的,并且不全,所以我将各站中写的比较好的博文综合起来,补全了答案,并对一些不必要的内容进行了简化,以下是我参考的一些博文链接:

图说C++对象模型:对象内存布局详解 - melonstreet - 博客园 (cnblogs.com)

图解C++对象模型,看这一篇就够了 - 知乎 (zhihu.com)

C++中类所占内存,父类与子类所占内存大小的关系(详细记忆)_c++中虚函数子类和父类的大小为什么会一样-CSDN博客

C++类的大小计算汇总 - 冯耀耀 - 博客园 (cnblogs.com)

C++中涉及到虚函数成员、静态成员、虚继承、多继承、空类等。

类作为一种类型定义,是没有大小可言的。

类的大小指的是类实例化出的对象的大小。因此,用sizeof对一个类型名操作,得到的是具有该类型实体的大小。

规律综合:

  • 类大小的计算,遵循结构体的对齐规则。
  • 类的大小与数据成员有关,与成员函数和静态成员无关。即普通成员函数,静态成员函数,静态数据成员,静态常量数据成员,均对类的大小没有关系。
  • 虚函数对类的大小有影响,是因为虚函数表指针带来的影响。
  • 虚继承对类的大小有影响,是因为虚基表指针带来的影响。
  • 静态数据成员之所以不计算在类的对象大小内,是因为类的静态数据成员被该类的对象所共享,并不属于具体的哪一个对象,静态数据成员定义在内存的全局区。
  • 空类的大小为1,含有虚函数,虚继承,多继承是特殊情况。

本文主要内容如下:

  1. 虚函数表解析。含有虚函数或其父类含有虚函数的类,编译器都会为其添加一个虚函数表、vptr,先了解虚函数表的构成,有助于对C++对象模型的理解。
  2. 虚基类表解析。虚继承产生虚基类表(vbptr),虚基类表的内容与虚函数表完全不同。
  3. 对象模型的概述:介绍简单对象模型,表格驱动对象模型,以及非继承情况下的C++对象模型。
  4. 继承下的C++对象模型。分析C++类对象在以下情况的内存布局:
  • 单继承:子类单一继承自父类,分析了子类重写父类虚函数、子类定义了新的虚函数情况下子类对象内存布局
  • 多继承:子类继承于多个父类,分析了子类重写父类虚函数、子类定义了新的虚函数情况下子类对象内存布局,同时分析了非虚继承下的菱形继承。
  • 虚继承:分析了单一继承下的虚继承、多重基层下的虚继承、重复继承下的虚继承。

最后还有一些问题:

C++封装带来的布局成本多大?

由空类组成的继承层次中,每个类对象的大小是多大?

一、理解虚函数表

C++中虚函数的作用主要是为了实现多态机制。多态,简单来说是指在继承层次中,父类的指针可以具有多种形态,当它指向某个子类对象时,通过它能够调用到子类的函数,而非父类的函数。这是一种运行期多态,即父类指针唯有在程序运行时才能知道所指的真正类型是什么。而这种决议是通过虚函数表来实现的。

当一个类本身定义了虚函数,或其父类有虚函数时,为了支持多态机制,编译器将为该类添加一个虚函数指针(vptr)。虚函数指针一般都放在内存布局的第一个位置上,这是为了保证在多层继承或多重继承的情况下能以最高效率取到虚函数表。

虚函数表指针指向虚函数表,虚函数表中存储的是一系列虚函数的地址,虚函数地址出现的顺序与类中虚函数声明的顺序一致。取到的虚函数表的地址也即是虚函数表第一个虚函数的地址。

二、对象模型概述

在C++中有两种数据成员(class data members):static 和 nonstatic,以及三种类成员函数(class member function):static、nonstatic 和 virtual。现在我们有一个类Base,包含了上面五种类型的数据或函数:

class Base
{
public:Base(int i) :baseI(i){};int getI(){ return baseI; }static void countI(){};virtual ~Base(){}virtual void print(void){ cout << "Base::print()"; }private:int baseI;static int baseS;
};

类图如下:

在非继承下的C++对象模型下:

nonstatic 数据成员被置于每一个类对象中,而 static 数据成员被置于类对象之外。 static 与 nonstatic 函数也都被放在类对象之外,而对于 virtual 函数,则通过虚函数表 + 虚指针来支持,具体如下:

每个类生成一个表格,成为虚表(virtual table,简称vtbl)。虚表中存放着一堆指针,这些指针指向该类每一个虚函数。虚表中的函数地址将按声明时的顺序排列,不过当子类有多个重载函数时例外。

每个类对象都拥有一个虚表指针(vptr),由编译器为其生成。虚表指针的设定与重置皆由类的复制控制(也就是构造函数,析构函数,赋值操作符)来完成。vptr的位置由编译器决定,传统上它被放在所有显示声明的成员之后,不过现在许多编译器把 vptr 放在一个类对象的最前端。另外,虚函数表的前面设置了一个指向 type_info 的指针,用以支持 RTTI(运行时类型识别)。RTTI 是为多态而生成的信息,包括对象继承关系,对象本身描述等,只有具有虚函数的对象才会生成。

此时 Base 的对象模型如图:

三、继承下的C++对象模型

单继承:

如果我们定义了派生类:

class Derive : public Base
{
public:Derive(int d) :Base(1000),      DeriveI(d){};//overwrite父类虚函数virtual void print(void){ cout << "Drive::Drive_print()" ; }// Derive声明的新的虚函数virtual void Drive_print(){ cout << "Drive::Drive_print()" ; }virtual ~Derive(){}
private:int DeriveI;
};

继承类图如下:

在C++对象模型中,对于一般继承(这个一般是相对于虚继承而言的),若子类重写了父类的虚函数,则子类虚函数将覆盖虚表中对应的父类虚函数(注意子类与父类拥有各自的一个虚函数表);若子类并没有重写父类虚函数,而是声明了自己新的虚函数,则该虚函数地址将扩充到虚函数表最后。而对于虚继承,若子类重写父类虚函数,同样的将覆盖父类子物体中的虚函数表对应位置,而若子类声明了自己新的虚函数,则编译器将为子类增加一个新的虚表指针 vptr,这与一般继承不同。

多继承:

一般的多继承(非菱形继承):

单继承中(一般继承),子类会扩展父类的虚函数表。在多继承中,子类含有多个父类的子对象,该往哪个父类的虚函数表扩展呢?当子类重写了父类的函数,需要覆盖多个父类的虚函数表吗?

  • 子类的虚函数表被放在声明的第一个基类的虚函数表中
  • 重写时,所有基类的 print() 函数都被子类的 print() 函数覆盖
  • 内存布局中,父类按照其声明顺序排列

其中第二点保证了父类指针指向子类对象时,总是能够调用真正的函数。

为了方便查看,我们将代码粘过来:

class Base
{
public:Base(int i) :baseI(i){};virtual ~Base(){}int getI(){ return baseI; }static void countI(){};virtual void print(void){ cout << "Base::print()"; }private:int baseI;static int baseS;
};
class Base_2
{
public:Base_2(int i) :base2I(i){};virtual ~Base_2(){}int getI(){ return base2I; }static void countI(){};virtual void print(void){ cout << "Base_2::print()"; }private:int base2I;static int base2S;
};class Drive_multyBase :public Base, public Base_2
{
public:Drive_multyBase(int d) :Base(1000), Base_2(2000) ,Drive_multyBaseI(d){};virtual void print(void){ cout << "Drive_multyBase::print" ; }virtual void Drive_print(){ cout << "Drive_multyBase::Drive_print" ; }private:int Drive_multyBaseI;
};

继承类图如下:

此时 Drive_multyBase 的对象模型是这样的:

菱形继承:

菱形继承也称为钻石型继承或重复继承,它指的是基类被某个派生类简单重复继承了多次。这样,派生类对象中拥有了多份基类实例(这会带来一些问题),为了方便描述,我们我们不使用上面的代码,而重新写一个重复继承的继承层次:

class B{public:int ib;public:B(int i=1) :ib(i){}virtual void f() { cout << "B::f()" << endl; }virtual void Bf() { cout << "B::Bf()" << endl; }};class B1 : public B{public:int ib1;public:B1(int i = 100 ) :ib1(i) {}virtual void f() { cout << "B1::f()" << endl; }virtual void f1() { cout << "B1::f1()" << endl; }virtual void Bf1() { cout << "B1::Bf1()" << endl; }};class B2 : public B{public:int ib2;public:B2(int i = 1000) :ib2(i) {}virtual void f() { cout << "B2::f()" << endl; }virtual void f2() { cout << "B2::f2()" << endl; }virtual void Bf2() { cout << "B2::Bf2()" << endl; }};class D : public B1, public B2{public:int id;public:D(int i= 10000) :id(i){}virtual void f() { cout << "D::f()" << endl; }virtual void f1() { cout << "D::f1()" << endl; }virtual void f2() { cout << "D::f2()" << endl; }virtual void Df() { cout << "D::Df()" << endl; }};

这时,根据单继承,我们可以分析出B1,B2类继承于B类时的内存布局。又跟据一般多继承,我们可以分析出D类的内存布局,可以得出D类子对象的内存布局如下:

D类对象的内存布局中,图中青色代表b1类子对象实例,黄色代表b2类子对象实例,灰色代表D类子对象实例。从图中可以看到,由于D类间接继承了B类两次,导致D类对象中含有两个B类的数据成员ib,一个属于来源B1类,一个来源B2类。这样不仅增大了空间,更重要的是引起了程序歧义:假如此时我想访问ib,调用的是B1的还是B2的呢?尽管我们可以通过明确指明调用路径以消除二义性,但二义性的潜在性还没有消除,此时我们可以通过虚继承来使D类只拥有一个ib实体。

五、虚继承

虚继承解决了菱形继承中派生类拥有多个间接父类实例的情况。虚继承的派生类的内存布局与普通继承很多不同,主要体现在:

  • 虚继承子类,如果本身定义了新的虚函数,则编译器为其生成一个虚函数指针(vptr)以及一张虚函数表,该vptr位于对象内存最前面。与非虚继承对比,非虚继承则是直接拓展父类虚函数表。
  • 虚继承的子类也单独保留了父类的vptr与虚函数表,这部分内容接与子类内容以一个四字节的0来分界。
  • 虚继承的子类对象中,含有四字节的虚表指针偏移值

5.1虚基类表解析:

在C++对象模型中,虚继承而来的子类会生成一个隐藏的虚基类指针(vbptr),在VSC++中,虚基类表指针总是在虚函数表指针之后,因而,对某个实例来说,如果它有虚基类指针,那么虚基类指针可能在实例的0字节偏移处(该类没有vptr时,vbptr就处于类实例内存布局的最前面,否则vptr处于类实例内存布局的最前面),也可能在类实例的4字节偏移处。

一个类的虚基类指针指向的虚基类表,与虚函数一样,虚基类表也是由多个条目组成,条目中存放的是偏移值,第一个条目存放虚基类表指针(vbptr)所在地址到该类内存首地址的偏移值,由第一段的分析我们知道,这个偏移值为0,(类没有vptr)或者-4(类有虚函数,此时有vptr)。我们通过一张图可以更好的了解:

虚基类表的第二、三...个条目依次为该类的最左虚继承父类,次左虚继承父类...的内存地址相对于虚基类表指针的偏移值。

5.2简单虚继承

//类的内容与前面相同
class B{...}
class B1 : virtual public B

依据我们前面对虚继承的派生类的内存布局的分析,B1类的对象模型应该是这样的:

此时vbptr的第二个条目值是12(假设一个指针占4字节大小),指向基类。

5.3虚拟多继承

class D : virtual public B1, virtual public B2 {
...
}

此时,子类D中的成员放在类内存布局中的最上方,如果子类D中如果有虚函数,那么也会创建一个vptr,并由vbptr指向虚继承的多个虚基类。

5.4虚拟菱形继承

class B{...}
class B1: virtual public  B{...}
class B2: virtual public  B{...}
class D : public B1,public B2{...}

类图如下:

在菱形虚拟继承下,派生类D类对象的对象模型又有不同的构成了,在D类对象的内存构成上,有以下几点:

在D类对象内存中,基类出现的顺序是:先是B1(最左父类),然后是B2(次左父类),最后是B(虚祖父类)

D类对象的数据成员id放在B类前面,两部分数据依旧以0来分隔

编译器没有为D类生成一个它自己的vptr,而是覆盖并扩展了最左父类的虚基类表,与简单继承的对象模型相同。

抄B类的内容放到了D类对象内存布局的最后

六、C++封装带来的布局成本是多大?

在C语言中,“数据”和“处理数据的操作”是分开声明的,也就是说,语言本身并没有支持数据和函数之间的关联性。在C++中,我们通过类来将属性与操作绑定到一起,称为ADT(抽象数据结构)。

由于在C++对象模型中,这些函数属于类而不属于对象,只会为类产生唯一的函数实例,所以封装没有带来任何空间或执行期的效率影响,而在下面两种情况下,C++的封装额外成本才会显示出来:

虚函数机制(virtual function),用以支持执行期绑定,实现多态

虚基类(virtual Base class),虚继承关系产生虚基类,用于在多重继承下保证基类在子类中拥有唯一实例。

不仅如此,类内数据成员的内存布局与C语言的结构体成员内存布局是相同的,C++中处在同一个访问标识符(指public,private,protected)下的声明的数据成员,在内存中必定保证以其声明顺序出现。而处于不同访问标识符声明下的成员则无此规定。

总结:不考虑虚函数与虚继承,当数据都在同一个访问标识符下,C++的类与C语言的结构体在对象大小和内存布局上是一致的,C++的封装并没有带来空间时间上的影响。

七、下面这个空类构成的继承层次中,每个类的大小是多少?


class B{};
class B1 :public virtual  B{};
class B2 :public virtual  B{};
class D : public B1, public B2{};int main()
{B b;B1 b1;B2 b2;D d;cout << "sizeof(b)=" << sizeof(b)<<endl;cout << "sizeof(b1)=" << sizeof(b1) << endl;cout << "sizeof(b2)=" << sizeof(b2) << endl;cout << "sizeof(d)=" << sizeof(d) << endl;getchar();
}

解析(32位情况下):

编译器为空类安插1字节的空间,以便使该类对象在内存得以配置一个地址

b1虚继承于b,编译器为其安插一个4字节的虚基类表指针,此时b1已不为空,编译器不再为其安插1字节的空间。

b2同b1

d含有来自b1与b2两个父类的两个虚基类表指针,大小为8字节

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/722472.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【牛客】VL64 时钟切换

描述 题目描述&#xff1a; 存在两个同步的倍频时钟clk0 clk1,已知clk0是clk1的二倍频&#xff0c;现在要设计一个切换电路&#xff0c;sel选择时候进行切换&#xff0c;要求没有毛刺。 信号示意图&#xff1a; 波形示意图&#xff1a; 输入描述&#xff1a; clk0 clk1为时…

第四十七天| 198.打家劫舍、213.打家劫舍II、337.打家劫舍III

Leetcode 198.打家劫舍 题目链接&#xff1a;198 打家劫舍 题干&#xff1a;你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的现金&#xff0c;影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统&#xff0c;如果两间相邻的房屋在同一晚…

12-Linux部署Zookeeper集群

Linux部署Zookeeper集群 简介 ZooKeeper是一个分布式的&#xff0c;开放源码的分布式应用程序协调服务&#xff0c;是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件&#xff0c;提供的功能包括&#xff1a;配置维护、域名服务、分布式同步、组服务等。…

jmeter 压测数据库

当前版本&#xff1a; jmeter 5.6.3mysql 5.7.39 简介 JMeter 是一个开源的 Java 应用程序&#xff0c;主要用于进行性能测试和负载测试。它支持多种协议&#xff0c;包括但不限于 HTTP、HTTPS、FTP、JDBC 以及各种 Web Services。对于数据库的压力测试可以使用 JDBC 协议与数…

excel导入标准化

excel导入较导出还是复杂一些&#xff0c;一般分为三个步骤.市面上低代码平台可以将常用的操作固化&#xff0c;并且形成收益&#xff0c;这也是挺好的。我将我的一些总结分享到网上也是我自己乐意的。毕竟尊重技术的还是搞技术的自身&#xff0c;一般企业老板并不太关心技术代…

Spring中@import注解终极揭秘!

技术概念 它能干啥 Import注解在Spring框架中主要用于解决模块化和配置管理方面的技术问题&#xff0c;它可以帮助开发者实现以下几个目标&#xff1a; 模块化配置&#xff1a;在大型项目中&#xff0c;通常需要将配置信息分散到多个配置类中&#xff0c;以便更好地组织和管…

FPGA-DDS原理及实现

DDS(Direct Digital Synthesizer)即数字合成器,是一种新型的频率合成技术,具有相对带宽大,频率转换时间短、分辨率高和相位连续性好等优点。较容易实现频率、相位以及幅度的数控调制,广泛应用于通信领域。 相位累加器是由N位加法器与N位寄存器构成,每个时钟周期的上升沿,加法器…

数据中心制冷系统设计与发展

数据中心概要与传统建筑空间相比&#xff0c;数据中心散热密度大&#xff0c;单位面积散热量可达传统办公区域的40倍以上&#xff0c;且越来越呈现集中化、大型化的趋势&#xff1b;同时&#xff0c;设备的安全性需求提高了对内部空调温湿度和洁净度的要求&#xff0c;数据中心…

【Qt】Qwidget的常见属性

目录 一、Qwidget核心属性 二、enable属性 三、geometry属性 四、 WindowFrame的影响 五、windowTitle属性 六、windowIcon属性 七、qrc文件管理资源 八、windowOpacity属性 九、cursor属性 十、font属性 十一、toolTip属性 十二、focusPolicy属性 十三、styleShe…

STM32FreeRTOS-事件组1(STM32Cube高效开发教程)

文章目录 一、事件组的原理和功能1、事件组与队列信号量特点2、事件组存储结构3、事件组运行原理 二、事件组部分函数1、xEventGroupCreate()创建事件组函数2、xEventGroupSetBits&#xff08;&#xff09;事件组置位函数3、xEventGroupSetBitsFromISR&#xff08;&#xff09;…

Sychronized和ReentrantLock锁 面试题

Sychronized和ReentrantLock锁 面试题 前言1、Java死锁如何避免&#xff1f;2、公平锁和⾮公平锁的底层实现&#xff1f;3、ReentrantLock中tryLock()和lock()⽅法的区别&#xff1f;4、Sychronized的偏向锁、轻量级锁、重量级锁&#xff1f;5、谈谈你对AQS的理解&#xff0c;A…

SQL技巧笔记(一):连续3人的连号问题—— LeetCode601.体育馆的人流量

SQL 技巧笔记 前言&#xff1a;我发现大数据招聘岗位上的应聘流程都是需要先进行笔试&#xff0c;其中占比很大的部分是SQL题目&#xff0c;经过一段时间的学习之后&#xff0c;今天开了一个力扣年会员&#xff0c;我觉得我很有必要去多练习笔试题目&#xff0c;这些题目是有技…

代码随想录算法训练营第三十七天 | LeeCode 738. 单调递增的数字

题目链接&#xff1a;738. 单调递增的数字 - 力扣&#xff08;LeetCode&#xff09; class Solution { public:int monotoneIncreasingDigits(int N) {string strNum to_string(N);// flag用来标记赋值9从哪里开始// 设置为这个默认值&#xff0c;为了防止第二个for循环在fla…

Linux - 进程概念

1、冯诺依曼体系结构 我们常见的计算机&#xff0c;如笔记本。我们不常见的计算机&#xff0c;如服务器&#xff0c;大部分都遵守冯诺依曼体系&#xff1b; 截至目前&#xff0c;我们所认识的计算机&#xff0c;都是有一个个的硬件组件组成&#xff1a; 输入单元&#xff1a;…

【JavaEE】_Spring MVC项目使用数组与集合传参

目录 1. 使用数组传参 1.2 传递单个参数 1.3 传递多个名称相同的参数 1.3.1 关于urlencode 2. 使用集合传参 1. 使用数组传参 创建一个Spring MVC项目&#xff0c;其中 .java文件内容如下&#xff1a; package com.example.demo.controller;import com.example.demo.Per…

2.Zookeeper集成springboot操作节点,事件监听,分布式锁实现

1.Springboot项目中添加zookeeper 已经对应的客户端依赖 &#xff0c;pom.xml文件如下 <!-- Zookeeper组件 --><dependency><groupId>org.apache.zookeeper</groupId><artifactId>zookeeper</artifactId><version>3.9.1</version…

【C++】6-8 评委打分 分数 10

6-8 评委打分 分数 10 全屏浏览 切换布局 作者 刘利 单位 惠州学院 某诗歌朗诵比赛&#xff0c;有n位评委给参赛者打分&#xff0c;计算总分时要去除最高分和对低分。 要求&#xff1a;编写名为cmax和cmin的函数分别返回最高分的和最低分元素的引用&#xff0c;带有2个形参…

leetcode面试经典算法题——1

链接&#xff1a;https://leetcode.cn/studyplan/top-interview-150/ 392. 判断子序列 给定字符串 s 和 t &#xff0c;判断 s 是否为 t 的子序列。 字符串的一个子序列是原始字符串删除一些&#xff08;也可以不删除&#xff09;字符而不改变剩余字符相对位置形成的新字符串…

292.【华为OD机试】跳马问题(广度优先搜索(BFS)JavaPythonC++JS实现)

🚀点击这里可直接跳转到本专栏,可查阅顶置最新的华为OD机试宝典~ 本专栏所有题目均包含优质解题思路,高质量解题代码(Java&Python&C++&JS分别实现),详细代码讲解,助你深入学习,深度掌握! 文章目录 一. 题目二.解题思路三.题解代码Python题解代码JAVA题解…

分布式事务(SeataServer)

SeataServer搭建 Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。AT模式是阿里首推的模式,阿里云上有商用版本的GTS(Global Transaction Servi…