c++ 对象模型

c++ 对象模型,第1张


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

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

文章目录
  • c++ 对象模型
    • 1. 内存5区分布
    • 2. c++对象模型
      • 2.1 对象模型布局
      • 2.2 对象大小计算
      • 2.3 空对象的大小
    • 3. 继承体系下的对象模型
      • 3.1 单继承
      • 3.2 多继承与多重继承
    • 4. 继承体系下的对象模型(多态)
      • 4.1 单继承
      • 4.2 多继承
      • 4.3 多重继承
    • 5. 本文总结
    • 5. 本文总结

c++ 对象模型 1. 内存5区分布

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

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

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

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

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

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

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

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

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

个人见解: 堆是人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_;
};

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

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

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

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

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

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

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_;
};

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

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

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

3.2 多继承与多重继承

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

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;
};

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

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

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

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

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

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;
	};
};

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

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

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;
};

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

类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();

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

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

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/2889315.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-09-14
下一篇 2022-09-14

发表评论

登录后才能评论

评论列表(0条)

保存