内存对象模型 是同学们在面试过程中经常会被问到的问题,一堆的 Sizeof 求答案,怎么破?听作者讲完本节,也许你会有个简单的认识,先说对于C++,其内存是如何存放的:
先说说C++的存储区,有4种类型,堆、栈、全局存储区、常量存储区。
堆: 通过new来初始化,delete释放,一般是由程序员来创建和管理;
栈: 栈 是指临时或局部变量的存放位置,一般由操作系统分配和释放,比如我们常说的压栈、出栈;
全局存储区: 全局存储区 是指 程序的 全局变量、静态变量 的存放位置,独立于类对象,程序结束时释放;
常量存储区: 和全局存储区类似,用于常量数据的存放;
class BaseClass; // 前向声明
void main()
{
int i = 16; // 栈
char* str = new char[16]; // 堆
BaseClass b; // 类对象分配到栈
}
class BaseClass
{
public:
int ID;
char* m_strName;
Static int m_nCountry; // 全局存储区
public:
void setID(int _id) { ID=_id; }
static void Show() {……}
};
const int nCountryNum = 224; // 常量存储区
从上面代码可以清晰的看到内存的分布情况,这里需要展开的是,当类对象分配到堆或者栈上的时候,其Sizeof的情况。
那么上面代码中的 sizeof 是多大呢?我们运行的结果是 8,也就是sizeof(int)+sizeof(char*),为什么呢?根据我们前面的描述我们知道,静态变量和函数放在全局区里面,不占用对象存储空间,占用存储空间的只有前两个。
我们再来看下面几种情况:
class BaseClass1
{
};
cout << sizeof(BaseClass1); // 输出为1 - 空类的sizeof为1
class BaseClass2
{
public:
BaseClass2();
virtual ~BaseClass2(); // 虚函数,访问虚函数表
protected:
virtual void init();
};
cout << sizeof(BaseClass2); // 输出为4 - 增加了虚函数表地址,但与虚函数个数无关
class DeriveClass : public BaseClass2
{
public:
int m_nType;
};
cout << sizeof(DeriveClass); // 输出为8 - 父子变量相加
我们来总结一下:
1. 空类的 Sizeof等于1;
2. 带有虚函数的 Sizeof 需要额外计算虚函数表的指针;
3. 继承模式下 父子变量相加,虚函数表也计算在内。
说到这里,基本已经说清楚了,接下来我们来看两个专题,可重入函数 和 菱形继承问题。
** 可重入函数:
当出现多个任务调用同一个函数的时候,如果能保证 每次调用得到一样的结果,我们认为该函数是可重入的,反之,函数是不可重入的,不可重入函数一般也称为不安全函数。
那么导致函数不可重入的因素有哪些呢?
1. 函数内部使用了静态或者全局变量,而这些变量的值有可能每次并不一致;
2. 使用堆导致分配位置不同,或者IO带来的不一致输入;
3. 调用了其他不可重入的函数;
事实上,我们对可重入函数的定位,通常仅仅用于计算,我们给定了input,里面所有用的临时变量都在栈内分配。
可重入函数 与 线程安全 没有必然的联系,可重入只是针对单线程反复调用来讲,线程安全的保证方式在于加锁,即:
1. 可重入的函数一定是线程安全的;
2. 线程安全的函数也不一定是可重入的;
** 菱形继承问题:
菱形继承问题也叫钻石继承问题,描述为 两个类继承自同一父类,同时又有子类同时继承这两个类,图示如下:
通过一段代码来看:
class Base
{
int a;
};
class Parent1 : public Base
{
int b;
};
class Parent2 : public Base
{
int c;
};
class Derived : public Parent1, public Parent2
{
int d;
};
先来看一个Key Problem,编译不过,Base的成员变量在 Derived 里面有几份Copy? 没错,是两份,每个父类保存了一份,这种二义性显然是不允许的。
解决方案1:尽量避免使用双继承,确实这并不是一个清晰的集成体系;
解决方案2:感谢C++给出了一个解决方案,那就是 虚继承,专门针对这个问题给出的(姑且算个补丁吧,出来这摊就废了),我们把上面的类改造一下:
class Base
{
int a;
};
class Parent1 : virtual public Base
{
int b;
};
class Parent2 : virtual public Base
{
int c;
};
class Derived : public Parent1, public Parent2
{
int d;
};
好简单,就这样,Derived 类就只保留一个 a 对象,解决了二义性。
最后再来看,虚继承 的sizeof,与虚函数类似,虚继承添加了一个 虚基类表 指针(4字节)。
很明显 Sizeof(Parent1) = Sizeof(a) + Sizeof(b) + 虚基类指针 = 12
Sizeof(Derived) = Sizeof(a) + Sizeof(b) + Sizeof(c) + Sizeof(d) + 虚基类指针 * 2 = 24
详细的内容描述也可以参考(这篇作者认为写的还是不错):