title: object-mode-layout
date: 2022/09/12
tags:
在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
栈,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
全局/静态存储区,全局变量和静态变量被分配到同一块内存中。
常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改
😜提个问题: 为什么栈地址从高到低生长,堆从低到高?
网络答案: 这个问题与虚拟地址空间的分配规则有关,每一个可执行C程序,从低地址到高地址依次是:text,data,bss,堆,栈,环境参数变量;其中堆和栈之间有很大的地址空间空闲着,在需要分配空间的时候,堆向上涨,栈往下涨。
**这样设计可以使得堆和栈能够充分利用空闲的地址空间。**如果栈向上涨的话,我们就必须得指定栈和堆的一个严格分界线,但这个分界线怎么确定呢?平均分?但是有的程序使用的堆空间比较多,而有的程序使用的栈空间比较多。所以就可能出现这种情况:一个程序因为栈溢出而崩溃的时候,其实它还有大量闲置的堆空间呢,但是我们却无法使用这些闲置的堆空间。所以呢,最好的办法就是让堆和栈一个向上涨,一个向下涨,这样它们就可以最大程度地共用这块剩余的地址空间,达到利用率的最大化!!
个人见解: 堆是人new出来的, 符合用户习惯,低地址对应低位数据。
举个例子,根据如下的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种常见的组件。
从上图可以看出, 可以得出的结论:
看个例子: 请问下述代码运行结果是啥?
class Base
{
public:
Base() {};
~Base() {};
void nullPtrCall()
{
cout << "nullPtrCall()" << endl;
}
virtual void f()
{
cout << "Base::f()" << endl;
}
};
int main()
{
Base* p = nullptr;
p->nullPtrCall();
}
正常输出, 请参考函数语义学相关知识
回答图中的问题:
staticMem_
存放在静态数据区域,虚函数表去哪儿了?存放在.rodata数据段(常量区)上章节末尾直接回答了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 .像虚基类表指针和虚函数表指针这些类里面必要的时候会出现的“隐藏成员变量”它们的对齐规则可以总结为一句话:
隐藏成员的加入不能影响在其后的成员的对齐。
结论: 空类的大小为1. 理由:如果一个对象为0的话也就不存在,不存在也就无法寻址,编译器为了找到它就必须给它一个大小
直接上代码: 请问下面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
都遵循上述单继承的规则,这里就不过多阐述了。
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个虚函数。
再看子类的虚函数表, 从上图中可以看出, 子类如果 重写了父类的某一个函数,就在虚函数表中原位替换掉父类被重写的函数地址。为什么要原位替换?
因为实现多态,根据下标索引,不原位替换,下标变了函数调用也就变了。
还是一样的, 废话不多说, 直接上代码:
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.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监视没有显示出来而已。