• c++ 对象模型



    title: object-mode-layout
    date: 2022/09/12
    tags:

    • c++对象模型
      categories: c++对象模型

    c++ 对象模型

    1. 内存5区分布

    在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。

    栈,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

    堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。

    自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。

    全局/静态存储区,全局变量和静态变量被分配到同一块内存中。

    常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改

    image-20220911165339379

    😜提个问题: 为什么栈地址从高到低生长,堆从低到高

    网络答案: 这个问题与虚拟地址空间的分配规则有关,每一个可执行C程序,从低地址到高地址依次是:text,data,bss,堆,栈,环境参数变量;其中堆和栈之间有很大的地址空间空闲着,在需要分配空间的时候,堆向上涨,栈往下涨。

    **这样设计可以使得堆和栈能够充分利用空闲的地址空间。**如果栈向上涨的话,我们就必须得指定栈和堆的一个严格分界线,但这个分界线怎么确定呢?平均分?但是有的程序使用的堆空间比较多,而有的程序使用的栈空间比较多。所以就可能出现这种情况:一个程序因为栈溢出而崩溃的时候,其实它还有大量闲置的堆空间呢,但是我们却无法使用这些闲置的堆空间。所以呢,最好的办法就是让堆和栈一个向上涨,一个向下涨,这样它们就可以最大程度地共用这块剩余的地址空间,达到利用率的最大化!!

    image-20220911165831291

    个人见解: 堆是人new出来的, 符合用户习惯,低地址对应低位数据。

    2. c++对象模型

    2.1 对象模型布局

    举个例子,根据如下的BaseLayout类,进行简单分析

    class BaseLayout {
    public:
    	BaseLayout() {};
    	virtual ~BaseLayout() {};
    	void nonStaticFunc() {};
    	static void staticFunc() {};
    	virtual void nonStaticVirFunc() {};
    
    protected:
    	long long nonStaticX_;
    	short nonStaticY_;
    	static int staticMem_;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    如下图所示,class 对象一般有成员变量/静态成员变量,成员函数和静态成员函数,虚函数5种常见的组件。

    image-20220911173754322

    从上图可以看出, 可以得出的结论:

    1. 无虚函数的类对象,真正属于它自身的其实就只有成员变量,其他的都不是属于它自身。类似现在的共享经济,单个类对象对它们只有使用权,但没有所有权,各个类对象共享。
    2. 有虚函数场景,虚函数其实也不属于它自己, 类对象只有一个可怜巴巴的指向虚函数表的指针而已,当调用其中的虚函数时, 类对象需要拿着这跟无敌金针菇找到对应的虚函数表

    看个例子: 请问下述代码运行结果是啥?

    class Base
    {
    public:
    	Base() {};
    	~Base() {};
    	void nullPtrCall()
    	{
    		cout << "nullPtrCall()" << endl;
    	}
    
    	virtual void f()
    	{
    		cout << "Base::f()" << endl;
    	}
    };
    
    int main()
    {
    	Base* p = nullptr;
    	p->nullPtrCall();  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    正常输出, 请参考函数语义学相关知识

    回答图中的问题:

    1. 问题1:函数存放在代码区 .text 位置;静态成员函数staticMem_ 存放在静态数据区域,虚函数表去哪儿了?存放在.rodata数据段(常量区)
    2. 上述数据大小 X86 vs2022 环境是24. 请大家思考一下为什么,下节做出回答。

    2.2 对象大小计算

    上章节末尾直接回答了BaseLayout的对象占用内存大小。现在来详细分析一下为什么是24而不是其他值?

    在c/c++程序开发中,有一个字节对齐的概念,详细的自行百度。如下图,属于BaseLayout对象的就三个东东,虚函数指针,成员变量nonStaticX_nonStaticY_。 由于字节对齐的原因。总共占据了24个字节。

    image-20220912113825361

    👹 再问个问题? 如下图所示, 下面四个类A1/B1/A2/B2 的大小是多少?

    image-20220912113809373

    A1/B1: 24/24 为什么B1是24呀? 8字节对齐, 虚表指针 + Y_ = 6 + 填充两个字节 = 8 + Y_ = 16? 为啥是16🥱(参考结论)

    A2/B2: 24/16 B2 符合预期为16

    虚表指针内存对齐可以参考:虚表指针字节对齐

    结论:1. 类对象的大小受虚函数,类成员声明顺序, 字节对齐影响

    2 .像虚基类表指针和虚函数表指针这些类里面必要的时候会出现的“隐藏成员变量”它们的对齐规则可以总结为一句话:

    隐藏成员的加入不能影响在其后的成员的对齐

    2.3 空对象的大小

    结论: 空类的大小为1. 理由:如果一个对象为0的话也就不存在,不存在也就无法寻址,编译器为了找到它就必须给它一个大小

    3. 继承体系下的对象模型

    3.1 单继承

    直接上代码: 请问下面B3, B4 的大小是多少?

    class BaseLayoutA3 {
    public:
    	int a3;
    	short nonStaticY_;
    };
    
    class BaseLayoutB3 : public  BaseLayoutA3 {
    public:
    	short x;
    	int nonStaticX_;
    };
    
    class BaseLayoutA4 {
    public:
    	// int a3;
    	short nonStaticY_;
    };
    
    class BaseLayoutB4 : public  BaseLayoutA3 {
    public:
    	short x;
    	int nonStaticX_;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    直接上图: B3的结构类似于B3{ struct A3; x, X_}, A3 进行独立的字节对齐,大小为16;

    image-20220912224913470

    B4 的结构体为8, 这个又是怎么回事呢? 难道上述的规则不生效了么?不应该是下述结构,sizeof出来为12么?

    image-20220912225002362

    惯性思维,有时候人就是做聪明, 当我sizeof(A4)的时候,发现其大小为2, 默认程序四字节为最小单位对齐只是我的主观臆断罢了。上图解释为啥是8

    image-20220912225047135

    3.2 多继承与多重继承

    都遵循上述单继承的规则,这里就不过多阐述了。

    image-20220912225122944

    4. 继承体系下的对象模型(多态)

    4.1 单继承

    class Base
    {
    public:
    	Base() {};
    	~Base() {};
    
    	virtual void f()
    	{
    		cout << "Base::f()" << endl;
    	}
    	
    	virtual void g()
    	{
    		cout << "Base::g()" << endl;
    	}
    	
    	virtual void h()
    	{
    		cout << "Base::h()" << endl;
    	};
    };
    
    class Derive : public Base {
    public:
    	virtual void g()
    	{
    		cout << "Derive::g()" << endl;
    	}
    	int x = 0;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    image-20220912225148270

    首先Base的结构如上图所示,类对象只保存了一个指向虚函数表的指针,并且虚函数表里按照虚函数声明顺序保存虚函数的地址,再虚函数表的最前面有一个type_info的存在,用来做运行时类型识别(RTTI),实现多态。 这里不过多介绍。

    下面的debug图也可以佐证。base的大小为4。

    image-20220912225207416

    type_info的存在可以在上图中找到痕迹,我单独再截了一下图,如图所示,虚函数表大小为4, 但是我们只有3个虚函数。

    image-20220912225239157

    再看子类的虚函数表, 从上图中可以看出, 子类如果 重写了父类的某一个函数,就在虚函数表中原位替换掉父类被重写的函数地址。为什么要原位替换?

    因为实现多态,根据下标索引,不原位替换,下标变了函数调用也就变了。

    4.2 多继承

    还是一样的, 废话不多说, 直接上代码:

    class Base1
    {
    public:
    	Base1() {};
    	
    	virtual void f()
    	{
    		cout << "Base::f()" << endl;
    	}
    };
    
    class Base2
    {
    public:
    	Base2() {};
    
    	virtual void h()
    	{
    		cout << "Base::h()" << endl;
    	};
    };
    
    class MulDerive : public Base1, public Base2 {
    	virtual void g()
    	{
    		cout << "MulDerive::g()" << endl;
    	};
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    类MulDerive 继承自继承自Base1,Base2, 子类没有重写父类。类的布局如下: 子类的虚函数存放在继承定义的第一个父类虚函数表中。

    image-20220912225257739

    image-20220912225315652

    结论:每一个带有虚函数的父类,在子类都有一个指向虚函数表的指针

    4.3 多重继承

    上代码:在4.1 节的代码基础上进行继承

    class DeriveSon : public Derive {
    public:
    	virtual void h()
    	{
    		cout << "DeriveSon::h()" << endl;
    	}
    
    	virtual void xx()
    	{
    		cout << "DeriveSon::xx()" << endl;
    	}
    	int x = 0;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    类DeriveSon 继承自Derive, 类Derive 继承自Base。类Derive 重写了父类的g(), 类DeriveSon重写了父类的h()函数。

    image-20220912225334294

    类DeriveSon的虚函数表应该有4个,为啥xx()不见了?直接操作指针打印吧,

    	DeriveSon* son = new DeriveSon();       //派生类指针,其实用基类指针指向派生类也一样Base *d = new Derive();  	
    	long* pvptr = (long*)son;         //指向对象d的指针转成long *型,大家注意,目前d对象里只有虚函数表指针
    	long* vptr = (long*)(*pvptr);   //(*pvptr)表示pvptr指向的对象,也就是Derive对象本身。这个对象4字节,这个4字节是虚函数表地址
    	
    
    	typedef void(*Func)(void);
    	Func f = (Func)vptr[0]; //f,g,h就是 函数指针变量  vptr[0]指向第一个虚函数{project4.exe!Base::f(void)}	
    	Func g = (Func)vptr[1]; //vptr[1]指向第二个虚函数{project4.exe!Derive::g(void)}
    	Func h = (Func)vptr[2]; //vptr[2]指向第三个虚函数{project4.exe!Base::h(void)}
    	Func i = (Func)vptr[3]; //vptr[3]  void xx()
    	f();
    	g();
    	h();
    	i();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    image-20220912225347335

    通过printf大法,可以看到,直接debug监视没有显示出来而已。

    5. 本文总结

    1. 空对象也要占据一个字节的大小
    2. 类对象的大小受变量声明顺序,虚函数,字节对齐三大要素影响,继承体系下还要受到"基类对象的完整性保护"规则影响
    3. 类对象只拥有指向虚函数表的指针,不拥有虚函数表,只能间接寻址调用,虚函数表在全局常量区,为只读区,在编译时就已经确定
    4. 继承体系(有虚函数)下,父类和子类的虚函数表各自拥有,不是共享的。
      • 如果子类没有重写与新增定义虚函数,则是两张内容相同的表(从下表0开始算)
      • 如果子类重写了父类虚函数,则在虚函数表中原为替换,保证虚函数表的中的虚函数索引不变。
      • 多继承模式下, 子类拥有多个各个父类的指向虚函数表的指针,并且自身独有的虚函数存放在第一个父类的虚函数表末尾

    5. 本文总结

    1. 空对象也要占据一个字节的大小
    2. 类对象的大小受变量声明顺序,虚函数,字节对齐三大要素影响,继承体系下还要受到"基类对象的完整性保护"规则影响
    3. 类对象只拥有指向虚函数表的指针,不拥有虚函数表,只能间接寻址调用,虚函数表在全局常量区,为只读区,在编译时就已经确定
    4. 继承体系(有虚函数)下,父类和子类的虚函数表各自拥有,不是共享的。
      • 如果子类没有重写与新增定义虚函数,则是两张内容相同的表(从下表0开始算)
      • 如果子类重写了父类虚函数,则在虚函数表中原为替换,保证虚函数表的中的虚函数索引不变。
      • 多继承模式下, 子类拥有多个各个父类的指向虚函数表的指针,并且自身独有的虚函数存放在第一个父类的虚函数表末尾
  • 相关阅读:
    JAVASE——局部变量和全局变量
    TypeScript开启
    ubuntu 18.04安装教程(详细有效)
    诊断DLL——Visual Studio安装与dll使用
    python- excel 创建/写入/删sheet+花式遍历
    扫雷(蓝桥杯)
    猿创征文|AnimeGANv2 照片动漫化:如何基于 PyTorch 和神经网络给 GirlFriend 制作漫画风头像?
    Git基础(一)——Git
    芯片SoC设计你了解吗?
    二叉树题目:路径总和 II
  • 原文地址:https://blog.csdn.net/u013300049/article/details/126834085