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页的笔记(本文上面)。