C++独孤九剑第一式——拨云见日(对象内存模型)

原创 2016年05月30日 20:13:16

归妹趋无妄,无妄趋同人,同人趋大有。甲转丙,丙转庚,庚转癸。子丑之交,辰巳之交,午未之交。风雷是一变,山泽是一变,水火是一变。乾坤相激,震兑相激,离巽相激······好了,要背心法的同学请课后自行背诵。

        特别说明:本系列并非严格按照《独孤九剑》的剑式进行命名,来学剑招的请自备传送卷轴,恕不报销往返机票。

之所以称之为“拨云见日”,就是要在这招中和大家一起揭开C++对象内存的迷雾,让她在众位少侠面前一览无余(想想心里还真有点小激动呢o(* ̄︶ ̄*)o)。

我们从最基础的开始,一步步直到出任CEO,迎娶白富美(额,有点扯远了)。

环境说明:本机是windows32位机,实验环境为VC6.0

1.   当我们定义了一个空类的时候

Class base{};
Base b = Base();
Base *pb = &b;
cout<<sizeof(Base)<<endl;	//输出为1
cout<<*((char *)pb)<<endl;	//输出一个空字符

也就是说,当一个类为空类的时候,它的大小其实是1个字节(编译器悄悄地给了那个可怜的类一个字节)。为什么要插入这一个字节,个人认为是这样的:类是对象的模板,如果类的大小为0,那么它构造出的对象就相当于无物(大小为0的对象!)。这个时候究竟是有对象还是没有对象恐怕就要上升到哲学的高度了。用区区一个字节的内存,谈笑间就解决了这一难题,岂不妙哉。

2.   非空无继承的时候

class Base
{
Public:	//方便后面的验证
	static int s;	//静态变量不会占用类中的存储空间,它将存在全局区
	char a;
	int i;
	char b;
<span style="white-space:pre">	</span>virtual void sum();	//测试时先实现该函数,否则将报错
};

此时sizeofBase)操作将得到的答案是16,是的并不是61+4+1)。这是由内存对齐(为了更快的与cup进行数据交换)和存储虚函数表指针所引起的(虚函数本身虽然不保存在类中,但是指向虚函数表的指针将被保存,在具体实现多态时会用到)。

其中,nonstatic datamembersclass object中的排列顺序将和其被声明的顺序一样,members的排列只需符合“较晚出现的membersclass object中有较高的地址”这一条即可。也就是说各个members并不一定连续,例如编译器在内存对齐时填补的bytes就会出现在两个连续的members之间。而虚函数表指针被放在对象首部(不同的编译器会有所不同)。关于虚表指针的位置,可用以下语句进行验证:

Base b;
cout<<((long)&(b.a) - (long)&b)<<endl;//该表达式的结果为4,说明成员a前还有不知名的生物存在,毫无疑问,在这种情况下肯定是虚函数表指针无疑了。

此时的内存布局如下左图示:


注:padding表示编译器的填充部分。

ib的声明顺序交换后,该类的对象内存如上右图所示,此时大小为12

3.   单一继承的时候

class Base
{
Public:
	int  i;
	char  c;
<span style="white-space:pre">	</span>virtual void sum();	//测试时先实现该函数,否则将报错
};
class Little : public Base
{
Public:
	int i;
	virtual void out();	//测试时先实现该函数,否则将报错
};

此时sizeofLittle)得到的值为16。这里说明两点:首先,虽然父类和子类都有成员变量i,但他们在不同的内存中,互不干扰;其次,子类也有虚函数,但是子类的对象中并不会新添加一个虚表指针(该虚函数将被添加到父类虚函数表中,由父类部分的虚函数指针统一指向)。可用下式证明:

Little l;
cout<<((long)&(l.i)- (long)&(l.c))<<endl;//值为4,说明他们之间并没有其它成员。

关于第二点,我还将在独孤九剑第三式中详加说明。

下面左图是当前Little对象的内存情况:


该内存布局可由以下语句佐证:

Little l;
cout<<((long)&(l.Base::i) - (long) &l)<<endl;   //值为4
cout<<((long)&(l.i) - (long)&(l.Base::i))<<endl;  //值为8

若把Base中的sum虚函数去掉,则Little对象的内存布局如上右图。此时Little对象没有继承虚表指针,而是由自己新建了一个。

4.    多继承的时候

class Base1
{
public:
	char  *pc;
	virtual void sum1();
};

class Base2
{
public:
	int  ** pi;
	virtual void sum2();
};

class Little : public Base1,public Base2
{
public:
	double  *** pd;
	virtual void out();
};

这次把数据成员都换成指针,为各位看官换换口味。

此时,sizeof(Base1) 和 sizeof(Base2) 都为 8,sizeof(Little)为20。Little的对象内存如下图所示:


说明的主要是三点:1.不管是几级指针,在32位环境下都是占4个字节;2.基类成员在子类对象中的顺序取决于继承时的声明顺序;3.子类对象依然公用父类部分的虚函数指针,没有自己新建一个。

以下语句可用于验证内存分布:

Little l;
cout<<((long)&(l.pc) - (long)&(l))<<endl;//4
cout<<((long)&(l.pi) - (long)&(l))<<endl;//12
cout<<((long)&(l.pd) - (long)&(l))<<endl;//16
cout<<((long)&(l.pi) - (long)&(l.pc))<<endl;//8

5.    多继承 + 虚继承

class  VirtualBase
{
public:
	int vbi;
};

class Base1 : public  virtual  VirtualBase
{
public:
	char *pc;
	virtual void sum1();
};

class Base2 : public  virtual  VirtualBase
{
public:
	int **pi;	
	virtual void sum2();
};

class Little : public  Base1, public  Base2
{
public:
	double ***pd;
	virtual void out();
};

此时,sizeof(Base1) 和 sizeof(Base2) 都为 16,sizeof(Little)为32。假设Base1有实例对象b1,Base2有实例对象b2,则它们的内存如下图所示:


其中__vptr_Base1和__vptr_Base2是指向虚函数表的指针;而__vbptr_Base1和__vbptr_Base2是指向虚基类表的指针。

而此时,Little对象的内存如下图所示:


该布局可用如下语句佐证:

Little  l;
cout<<((long)&(l.vbi) - (long)&(l))<<endl;//28 该成员被放在了最后
cout<<((long)&(l.pc) - (long)&(l))<<endl;//  8
cout<<((long)&(l.pi) - (long)&(l))<<endl;// 20
cout<<((long)&(l.pd) - (long)&(l))<<endl;//  24
cout<<((long)&(l.pi) - (long)&(l.pc))<<endl;//  12

说明:在VC++中,对每个继承自虚基类的类实例,将增加一个隐藏的“虚基类表指针”(vbptr)成员变量,从而达到间接计算虚基类位置的目的。该变量指向一个全类共享的偏移量表,表中项目记录了对于该类而言,“虚基类表指针”与虚基类之间的偏移量”。

下面进一步说明虚基类指针指向的虚基类表的内容。

上面Little类实例的__vbptr_Base1和__vbptr_Base2指针所指向的虚基类表的内存分布如下左图所示:


说明:虚基类表中的第一个索引是自己的内存地址相对于自己存放指向虚基类表的地址的偏移量(为-4,因为中间隔了一个虚函数表指针);第二个索引开始是基类内存相对于它自己指向虚基类表地址的偏移量;最后以0标志虚基类表的结束。

右图是VC6.0的实验结果,__vbptr_Base1所指向的内容紧挨着__vbptr_Base2所指向的内容。很容易验证,__vbptr_Base1的值与__vbptr_Base2的值差了12字节,且有:

*((int *)*((int*)(&l) + 1) - 2)  ==  *((int *)*((int*)(&l) + 4) + 1) == 12

从而证明了右图的正确性(并不一定适用于其它编译器)。

到此第一式就练完了,各位少侠是否觉得神清气爽、格外精神(*^-^*)


















版权声明:本文为博主原创文章,未经博主允许不得转载。

C++独孤九剑第六式——洞若观火(深入对象操作)

在前面的几式中,虽说我们已经比较详细的探讨了对象的种种操作(构造、复制构造、赋值操作符、析构),但是我感觉还是差了一点深度,所以在这一式中进一步加深,直击对象内部的操作过程。真正做到“知其然,且知其所...
  • wzxq123
  • wzxq123
  • 2016年06月18日 17:11
  • 2266

C++独孤九剑第三式——鱼跃于渊(多态机制实现)

鱼跃于渊,过而成龙,变幻万千。 我们都知道面向对象的三大特征: 1.封装        2.继承        3.多态 多态是建立在前面两个特征的基础之上的,可以算得上是面向对象的“终极应用”...
  • wzxq123
  • wzxq123
  • 2016年06月06日 16:45
  • 3238

C++独孤九剑第七式——庖丁解牛(各种重载操作)

庖丁解牛,庖丁指的是编译器,而我们则是提供牛的人,牛就是被重载的操作。当编译器遇到被重载的操作,能够准确地找出合适一个,犹如庖丁解牛,游刃有余。 重载主要分为函数重载和操作符重载。函数重载应该大家都会...
  • wzxq123
  • wzxq123
  • 2016年06月23日 16:59
  • 1905

C++独孤九剑第九式——以静制动(模板编程探索)

在C++编程中,抽象层次最高的应该算是模板了吧。模板是泛型编程的基础。所谓泛型编程,就是以独立于任何特定类型的方式编写代码。把相关的类型也抽象出来,使我们的代码可以适应所需要任何类型!哇塞,想想都觉得...
  • wzxq123
  • wzxq123
  • 2016年06月29日 21:58
  • 2724

netfilter——独孤九剑

一、主函数  init为初始化函数,主要完成表的注册,然后再注册与表相对应的HOOK  //初始化函数为init:  module_init(init);  //init 函数负责注册...

缩短新站被百度收录时间的独孤九剑

缩短新站被百度收录时间的独孤九剑   经常听到一些朋友说新站seo优化收录困难,笔者对此深有体会,毕竟笔者在日常工作中,经常需要把一个新站点优化到首页甚至排名前列的位置,因而拥有了一些小小的...

连接数据库之独孤九剑

一:连接数据库读取数据 九大步骤: 1、获得连接字符串     .\SQLEXPRESS string url=@"Data Source=服务器名;Initial Catalog=数据库名;Us...

第十篇:百花错拳与独孤九剑

第一部分内容写到这里,就已经结束了。在这部分内容里,我们更多的是从庄家的基本职能,赔率和盘口的表象与本质在进行一些探讨。一直在思考,该用怎样的文字来给第一部分内容挽上一个句号,思来想去,就用金庸笔下的...
  • yp1125
  • yp1125
  • 2017年01月06日 15:03
  • 118

互联网思维之“独孤九剑”与19条法则

原标题:互联网思维:3个段子,9大分类,19条法则 课前秀:三个段子 第一个段子:一个毫无餐饮行业经验的人开了一家餐馆,仅两个月时间,就实现了所在商场餐厅坪效第一名...

「优知学院」架构师进阶独孤九剑系列(一):数据结构与算法

“ 架构师进阶系列之独孤九剑,这套秘籍如果你能完整学会,年薪至少60万起,今天是架构师进阶连载开启篇。 完整的架构师进阶步骤 未来我会用1-2个月在实战为大家讲解架构师的九大必备技能。 架构师进阶...
  • crmike
  • crmike
  • 2017年11月28日 10:22
  • 110
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:C++独孤九剑第一式——拨云见日(对象内存模型)
举报原因:
原因补充:

(最多只允许输入30个字)