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字节

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

如何创建一个VUE3项目并使用Element UI插件

1.确保已经安装了Node&#xff1a; win R 打开控制面板&#xff0c;输入“node -v”回车。出现版本号信息&#xff0c;则安装成功&#xff0c;否则请移步安装。 Node.js安装及环境配置&#xff08;简单易懂&#xff01;&#xff09;_building: c:\program files\nodejs\node…

基于SSM的学科竞赛管理系统。Javaee项目。ssm项目。

演示视频&#xff1a; 基于SSM的学科竞赛管理系统。Javaee项目。ssm项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff0c;通过Spring SpringMvcMybatisVueLayuiElemen…

我的第②个出海工具站 - 2024年50个出海工具站计划

为了大家更好的使用各种出海工具。我上线了一版 出海工具导航 站点&#xff0c;经常使用的可以收藏下&#xff0c;我文内使用的网站都集成在了这里&#xff0c;非常使用。 随着AIGC的到来&#xff0c;2024年到了海外工具回暖的一年。今年计划上线50款出海工具站计划&#xff0c…

嵌入式工程师函数变量,常用的命名规则(参考学习)

很多工程师不注重平时编码习惯&#xff0c;比如命名规则&#xff0c;一会大写、一会小写&#xff0c;一会中文拼音&#xff0c;一会下划线等&#xff0c;导致自己写的代码自己都看不懂了。 今天就来分享一点关于软件代码常见的几种命名规则。 匈牙利命名法 匈牙利命名法广泛应…

西安雁塔未来人工智能计算中心算力成本分析

先看一例旧闻&#xff1a;西部“最强大脑”落户雁塔——30亿亿次超算能力助力创新之城建设 其中提到一期算力为 300PFLOPS FP16&#xff08;每秒30亿亿次半精度浮点计算&#xff09;&#xff0c;项目总投资约为19亿元。 这个算力是什么概念呢&#xff1f; 我们以深度学习训练中…

使用sunshine和moonlight实现远程游戏串流

过年回家想要打游戏&#xff0c;但是苦于家里没有电脑&#xff0c;又没办法把电脑搬回去&#xff0c;于是想到了使用串流的方式。 实现串流的软件有多种&#xff1a; moonlight。因为仅实现了 NVIDIA 的游戏串流协议&#xff0c;所以只支持 N 卡。Steam Link。支持 steam 的游…

第五十回 插翅虎枷打白秀英 美髯公误失小衙内-mayfly-go:web 版 linux、数据库等管理平台

晁盖宋江和吴用到山下迎接雷横上山&#xff0c;宋江邀请雷横入伙&#xff0c;雷横以母亲年事已高为由拒绝了。 雷横回到郓城&#xff0c;听李小二说从东京新来了个表演的叫白秀英&#xff0c;吹拉弹唱跳&#xff0c;样样精通&#xff0c;于是雷横和李小二一起到戏院去看演出。…

什么是AJAX?它的运用场景有哪些?

文章目录 前言一、什么是AJAX二、AJAX原理是什么三、为什么需要AJAX四、AJAX的使用五、AJAX的应用场景 前言 AJAX 即 Asynchronous Javascript And XML&#xff08;异步JavaScript和XML&#xff09;&#xff0c;是指一种创建交互式网页应用的网页开发技术。 AJAX 是一种用于创…

首尔之春在线资源最新电影1080p高清

打开下面这个链接就可以看到 首尔之春在线资源最新电影1080p高清 如果链接打不开&#xff0c;就复制下面的网址到浏览器打开 https://www.zhufaka.cn/liebiao/A09504AE3BF8BD06 用阿里云盘下载&#xff0c;下载完成之后&#xff0c;用迅雷播放 首尔之春在线资源最新电影10…

NVMe管理命令为何不用SGL?-2

在IO数据传输中&#xff0c;是否选择SGL可以根据自身场景的需要。SGL提供的是一种高效且灵活的方式来描述非连续的内存区域&#xff0c;这对于现代高性能存储系统至关重要&#xff0c;尤其是在处理大数据块或者随机小I/O操作时具有明显优势&#xff1a; 高效的数据传输&#xf…

【OpenGL编程手册09】颜色和光照

目录标题 一、说明二、物理概念三、OpenGL处理办法四、创建一个光照场景 一、说明 在前面的教程中我们已经简要提到过该如何在OpenGL中使用颜色(Color)&#xff0c;但是我们至今所接触到的都是很浅层的知识。本节我们将会更深入地讨论什么是颜色&#xff0c;并且还会为接下来的…

Minio容器化部署并整合SpringBoot

1、启动minio容器 docker run -p 9000:9000 -p 9090:9090 --name minio -d --restartalways -e MINIO_ACCESS_KEYminio -e MINIO_SECRET_KEYminio -v /usr/local/minio/data:/data -v /usr/local/minio/config:/root/.minio minio/minio server /data --console-addr…

文件操作与IO(3) 文件内容的读写——数据流

目录 一、流的概念 二、字节流代码演示 1、InputStream read方法 第一个没有参数的版本&#xff1a; 第二个带有byte数组的版本&#xff1a; 第三个版本 搭配Scanner的使用 2、OutputStream write方法 第一个版本&#xff1a; 第二个写入整个数组版本&#xff1a; …

16 PyTorch 神经网络基础【李沐动手学深度学习v2】

要想直观地了解块是如何工作的&#xff0c;最简单的方法就是自己实现一个。 在实现我们自定义块之前&#xff0c;我们简要总结一下每个块必须提供的基本功能。 将输入数据作为其前向传播函数的参数。 通过前向传播函数来生成输出。请注意&#xff0c;输出的形状可能与输入的形…

python一张大图找小图的个数

python一张大图找小图的个数 一、背景 有时候我们在浏览网站时&#xff0c;发现都是前端搞出来的一张张图&#xff0c;我们只能用盯住屏幕的小眼睛看着&#xff0c;很累的统计&#xff0c;这个是我在项目中发现没办法统计&#xff0c;网上的教程很多&#xff0c;都不成功&…

构建信息蓝图:概念模型与E-R图的技术解析

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua&#xff0c;在这里我会分享我的知识和经验。&#x…

C# 中 Interpreter 用于解释执行代码的工具

在 C# 中&#xff0c;Interpreter 是一个用于解释执行代码的工具&#xff0c;它提供了一种在运行时动态解释和执行 C# 代码的方式。Interpreter 类位于 Microsoft.CodeAnalysis.CSharp.Scripting 命名空间中&#xff0c;它允许你通过编写代码字符串来执行 C# 代码。 下面是一些…

毫秒生成的时间戳如何转化成东八区具体时间

假设现在有一个时间是1709101071419L 后端代码实现 Java代码&#xff08;东八区时间&#xff09; 在Java代码中&#xff0c;我们将时区从UTC调整为东八区&#xff08;UTC8&#xff09;&#xff1a; import java.time.Instant; import java.time.ZoneId; import java.time.Z…

pytest 教程

1. 安装pytest 目前我使用的python版本是3.10.8 pip install pytest命令会安装下面的包&#xff1a; exceptiongroup-1.2.0-py3-none-any.whl iniconfig-2.0.0-py3-none-any.whl packaging-23.2-py3-none-any.whl pluggy-1.4.0-py3-none-any.whl pytest-8.0.2-py3-none-any.…

CSS3新特性

简介 继CSS2之后&#xff0c;CSS3增加了很多新的特性&#xff0c;虽然W3C仍在规范中&#xff0c;但是很多新的CSS3属性已经在很多现代浏览器中得到了支持。 CSS3边框 在CSS3中&#xff0c;可以创建圆角边框&#xff0c;添加边框阴影&#xff0c;设置边框图片&#xff0c;利用…