C++对象模型学习

《深度探索C++对象模型》这本书看了2遍了,第一遍时很多东西懵懵懂懂,似懂非懂,在看时就比较清楚了。想着对比书上的理论,写点代码来验证一下,应该理解会更深刻些。这篇博客中先记录一下第三章的内容-data语意学。所有的代码都是在vs2008中编译。

p84(1):一个空的class内存占用是一个字节,这样的两个空对象就能够在内存中各自分配一个独一无二额地址

class CTst
{ };

p84(2): 如下代码的大小是4个字节(ms vc 环境下)

class CSecond : public virtual CTst
{
};

这个CSecond的大小受3个因素的影响:

  • 语言本身所造成的额外负担 
  • 编译器对于特殊情况所做的优化处理
  • Alignment的限制  

CSecond虚继承自CTst,为了支持virtual base,系统会在CSecond数据内分配一个4字节的内存来保存这种关系virtual base,后面会介绍到。Alignment的限制是系统一般都会对数据区进行取整。当我将CTst的定义改成如下时:

class CTst
{
	char a;	 
};

 CTst的长度是1, CSecond的长度是5, 这时可以看见 针对于这个类,编译器并没用进行Alignment方面的动作。然后我在加一个int的数据成员在CTst中时,此时CTSt的大小是8, CSecond的大小是12。很明显,系统对两个类的内存分配进行对齐的操作。

p88(1): C++对象模型直接把数据存放在一个C++ object中,继承而来的nonstatic 数据也是一样。static的数据成员放在一个全局的数据区中。不过这个class产生多少对象,static的成员只有一份。但是template class的static成员稍有不同。

p89: Argument list中的名称会在他们第一次遇到时被决议出来。

typedef  char TLJW;

class CTst
{
	char a;
	TLJW abc;

	typedef long long TLJW;
};

  上面的代码中,abc的类型是char, 而不是 longlong型。

p92: C++标准规定,同一个access section的数据成员,晚出现的成员在类对象中有较高的地址。不同的access section的数据成员可以自由排列。编译器还可能会生成一些内部使用的成员来支持对象模型,比如vptr。

p98: 通过 char CTst::* abc 可以定义一个指向 CTst的char类型的数据成员的指针(偏移量),&CTst::a会返回a在CTst中的偏移量,如下代码,分别是定义了2个指向数据的指针,一个直接用类成员的地址进行复制,一个用0来复制,可通过反汇编出来的代码看出,系统会将用零赋值的语句转化为用0xffffffff来赋值,这是为了能够使 tst_mem和tst_zero进行比较时结果正确,区分出”指向class第一个member的指针“和“一个指向class的member的指针,没有指向任何member”。

	char CTst::* tst_mem = & CTst::a ;
0041357E  mov         dword ptr [tst_mem],0 
	char CTst::* tst_zero = 0;
00413585  mov         dword ptr [tst_zero],0FFFFFFFFh 

  每一个nonstatic的数据成员的偏移量(offset)可以再编译时就获知。因此存取一个nonstatic类成员和存取一个C struct成员的效率是一样的。

 p99: “从对象存取”和“从指针存取”有什么重大的差异?

class CPoint;

	CPoint origin;
	CPoint* pt = &origin;
	origin.x;
	pt->x;

  当Point是一个derived class,而其继承结构中有一个virtual base class,并且存取的成员(x)是该virtual base class的成员时,问题中的两种存取方式有重大差异。因为我们不能够确定指针pt必然指向哪一种class类型(因此我们就不知道编译时期这个member真正的offset位置)。所以这个操作必须延迟至运行时,经过一个额外的间接导引才能解决。如果使用origin,就不会有这个问题。

p100: 具体继承(相对于虚拟继承)并不会增加空间或者存取时间上的额外负担。

p102: 把一个类分解为2层或者更多层,有可能会为了“表达class体系之抽象化”而膨胀所需要的空间。这个主要是出现在derived class中的base class部分尤其完整原样性。这个主要是子类中父类部分,在内存上要和一个父类的布局相同。

p108: 具有多态的继承会带来空间和存取时间上的负担,主要有:

  • 导入一个virtual table,用来存放他所声明的每一个virtual function的地址。这个表格一般是声明的virtual function的数据, 在加上一个或者2个slot,用来支持 runtime type  identification。
  • 每个类对象都导入一个vptr,提供运行时的链接
  • 加强construct, 使他能够伟vptr设定初值,让他指向class所对应的virtual table。
  • 加强destructor, 使他能消除指向class相关的virtual table的信息。

 写了一个测试用程序,如下:

        class CH
{
public:
int m_h;

virtual void printName()
{
printf("name of CH\n");
}
};

class CI : public CH
{
public:
char m_c;
};

CH _ch;
CI _ci;
_ch.m_h = 12;
_ci.m_c = '2';
_ci.m_h = 11;
CH * _pch = &_ch;
CI *_pci = &_ci;

printf("test of virtual table for single inherit\n");
unsigned long * _pMemory = (unsigned long *)&_ch;
printf("m0:%x, m1:%x, m2:%x, *m0:%x, %x\n", *_pMemory, *(_pMemory+1), *(_pMemory+2), *((unsigned long *)*(_pMemory)), *((unsigned long *)*(_pMemory)+1) );
_pMemory = (unsigned long *)&_ci;
printf("m0:%x, m1:%x, m2:%x, *m0:%x, %x\n", *_pMemory, *(_pMemory+1), *(_pMemory+2), *((unsigned long *)*(_pMemory)), *((unsigned long *)*(_pMemory)+1));

printf("\n");

输出结果是:

test of virtual table for single inherit
m0:41582c, m1:c, m2:cccccccc, *m0:4116b0, 0
m0:415838, m1:b, m2:cccccc32, *m0:4116b0, 0

 这个将CH和CI类对象的内存数据输出出来分别是m0, m1, m2,同时,还把vptr(在对象的开始存放)所指向的内存输出了出来,输出了2个字节,是*m0。 从这个结果可以看出vc中,CH和CI对应虚表不一样,但是他们的表中的第一项都是指向同一个地址,也就是同一个函数。当我在CI中复写printName方法后,输入结果如下:

test of virtual table for single inherit
m0:415838, m1:c, m2:cccccccc, *m0:4116b0, 0
m0:415740, m1:b, m2:cccccc32, *m0:411720, 0

  vptr中的第一项不一样了。

以上是单一继承时的情况,对于多重继承,在以上代码的基础上,在增加如下代码

		class CJ
		{
		public:
			virtual void printAge()
			{
				printf("age from cj\n");
			}
		};

		class CK : public CH, public CJ
		{
		public:
			unsigned m_k;

		};

		CJ _cj;
		CJ * _pcj = &_cj;
		CK _ck;
		_ck.m_h=2; _ck.m_k = 5;
		CK * _pck = &_ck;


		printf("test of virtual table for multiple inherit\n");
		_pMemory = (unsigned long *)&_cj;
		printf("CJ  \tm0:%x, m1:%x, m2:%x, *m0:%x, %x\n", *_pMemory, *(_pMemory+1), *(_pMemory+2), *((unsigned long *)*(_pMemory)), *((unsigned long *)*(_pMemory)+1) );
		_pMemory = (unsigned long *)&_ck;
		printf("CK  \tm0:%x, m1:%x, m2:%x, *m0:%x, %x\n", *_pMemory, *(_pMemory+1), *(_pMemory+2), *((unsigned long *)*(_pMemory)), *((unsigned long *)*(_pMemory)+1));

  输出结果如下:

test of virtual table for single inherit
m0:4157d0, m1:c, m2:cccccccc, *m0:4116b0, 0
m0:4157f0, m1:b, m2:cccccc32, *m0:411720, 0

test of virtual table for multiple inherit
CJ      m0:41583c, m1:cccccccc, m2:cccccccc, *m0:411770, 416654
CK      m0:4158b4, m1:2, m2:41584c, *m0:4116b0, 0

多重继承时,内存的布局是先进行最先继承的(第一个父类)父类的布局,所以子类和该父类有相同的起始地址。然后依次是第二个,第三个父类的内存布局。存取第二个或者之后的父类中的数据成员时,不需要付出额外的代价,数据成员的位置在编译时就已经确定了。

p117: class如果内含一个或者多个virtual base class subobjects,一般的实现方法是类被分割为两部分,一个不变局部和一个共享局部。不变局部的数据,不管后继如何演化,总是拥有固定的offset,所以这一部分数据可以被直接存取。至于共享局部,所表现的就是virtual base class subobject。这一部分的数据,其位置会因为每次的派生操作而有变化,所以他们只可以被间接存取。

通过查看内存的方式,看了下在虚拟继承下,VC的内存布局。比较乱,也没有时间细看,所以就先没看,但是基本的原理了解了一些。将虚拟继承的含义抄在下面。

虚继承的定义: 虚继承是一种机制,类通过虚继承指出它希望共享其虚基类的状态。在虚继承下,对给定虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象。共享的基类子对象称为虚基类(virtual base class)

p129: 虚拟继承的效率令人失望,这主要是由于为了维护虚拟继承的结构,编译器一般会将一些成员决议操作放在运行时来进行间接访问。

p130: &Point::z; 获取到z坐标在class object中的偏移量。 C++要求同一个access level 中的成员的排列次序应该和其声明顺序相同。可以参看p98页的笔记(本文上面)。

原文地址:https://www.cnblogs.com/kwliu/p/2405339.html