9-1LectureNotesonObject-OrientedTechnology(Programming&Design)(Fall2013,BachelorofComputerScience)DuanshihongOffice:Room1203A,InformationBuilding9-2主题讨论-对象模型类对象实例化抽象、封装在面向对象语言里定义数据抽象的基本机制是类,在类里可以定义数据和子程序成员(称为方法)加上了封装,布局成本增加了多少?9-3主题讨论-对象模型1、C结构(struct)structA{charc;inti;};说明:从上图可见,A类型数据在内存中占有8个字节,按照声明成员的顺序,前4个字节包含一个字符(实际占用1个字节,3个字节空着,补对齐),后4个字节包含一个整数。A的指针就指向字符开始字节处。9-4主题讨论-对象模型1、C结构(struct)-成员变量structA{charc;inti;};A*pc;pc-c;//*(pc+dCc);说明:pc是指向A的指针。访问A的成员变量c,只需要在pc上加上固定的偏移量dCc(在C中,C指针地址与其c成员变量之间的偏移量值),再获取该指针的内容即可。9-5主题讨论-对象模型2.有C++特征的C结构structB{public:intbm1;protected:intbm2;private:intbm3;staticintbsm;voidbf();staticvoidbsf();typedefvoid*bpv;structN{};};说明:C++类实例的大小完全取决于一个类及其基类的成员变量!成员函数基本上不影响类实例的大小。B中,为何staticintbsm不占用内存空间?因为它是静态成员,该数据存放在程序的数据段中,不在类实例中。9-6主题讨论-对象模型3.单继承structC{intc1;voidcf();};structD:C{intd1;voiddf();};说明:每个派生类的实例都包含了一份完整的基类实例数据。在D中,并不是说基类C的数据一定要放在D的数据之前,只不过这样放的话,能够保证D中的C对象地址,恰好是D对象地址的第一个字节。几乎所有知名的C++厂商都采用这种内存安排。在单继承类层次下,每一个新的派生类都简单地把自己的成员变量添加到基类的成员变量之后。看看上图,C对象指针和D对象指针指向同一地址。9-7主题讨论-对象模型3.单继承D*pd;pd-c1;//*(pd+dDC+dCc1);//*(pd+dDc1);pd-d1;//*(pd+dDd1);说明:D从C单继承,pd为指向D的指针。•当访问基类成员c1时,计算步骤本来应该为“pd+dDC+dCc1”,即为先计算D对象和C对象之间的偏移,再在此基础上加上C对象指针与成员变量c1之间的偏移量。然而,由于dDC恒定为0,所以直接计算C对象地址与c1之间的偏移就可以了。•当访问派生类成员d1时,直接计算偏移量。9-8主题讨论-对象模型4.多重继承structC{intc1;voidcf();};structE{inte1;voidef();};structF:C,E{intf1;voidff();};Ff;//(void*)&f==(void*)(C*)&f;//(void*)&f(void*)(E*)&f;说明:可以看到F中内嵌的E对象,其指针与F指针并不相同。因此强制转换时,这个偏移量会造成少量的调用开销。9-9主题讨论-对象模型4.多重继承F*pf;pf-c1;//*(pf+dFC+dCc1);//*(pf+dFc1);pf-e1;//*(pf+dFE+dEe1);//*(pf+dFe1);pf-f1;//*(pf+dFf1);说明:F继承自C和E,pf是指向F对象的指针。访问C类成员c1时,F对象与内嵌C对象的相对偏移为0,可以直接计算F和c1的偏移;访问E类成员e1时,F对象与内嵌E对象的相对偏移是一个常数,F和e1之间的偏移计算也可以被简化;访问F自己的成员f1时,直接计算偏移量。9-10主题讨论-对象模型5.虚继承structC{intc1;voidcf();};structG:virtualC{intg1;voidgf();};structH:virtualC{inth1;voidhf();};structI:G,H{inti1;void_if();};9-11主题讨论-对象模型5.虚继承VC++实现的内存布局中,G对象实例中G对象和C对象之间的偏移,不同于I对象实例中G对象和C对象之间的偏移。当使用指针访问虚基类成员变量时,由于指针可以是指向派生类实例的基类指针,所以,编译器不能根据声明的指针类型计算偏移,而必须找到另一种间接的方法,从派生类指针计算虚基类的位置。在VC++中,对每个继承自虚基类的类实例,将增加一个隐藏的“虚基类表指针”(vbptr)成员变量,从而达到间接计算虚基类位置的目的。该变量指向一个全类共享的偏移量表,表中项目记录了对于该类而言,“虚基类表指针”与虚基类之间的偏移量。在VC++中,G拥有一个隐藏的“虚基类表指针”成员,指向一个虚基类表,该表的第二项是GdGvbptrC。(在G中,虚基类对象C的地址与G的“虚基类表指针”之间的偏移量(当对于所有的派生类来说偏移量不变时,省略“d”前的前缀)。比如,在32位平台上,GdGvptrC是8个字节。同样,在I实例中的G对象实例也有“虚基类表指针”,不过该指针指向一个适用于“G处于I之中”的虚基类表,表中一项为IdGvbptrC,值为20。9-12主题讨论-对象模型5.虚继承当类有虚基类时,访问非虚基类的成员仍然是计算固定偏移量的问题。然而,访问虚基类的成员变量,开销就增大了,因为必须经过如下步骤才能获得成员变量的地址:获取“虚基类表指针”;获取虚基类表中某一表项的内容;把内容中指出的偏移量加到“虚基类表指针”的地址上。I*pi;pi-c1;//*(pi+IdGvbptr+(*(pi+IdGvbptr))[1]+dCc1);pi-g1;//*(pi+dIG+dGg1);//*(pi+dIg1);pi-h1;//*(pi+dIH+dHh1);//*(pi+dIh1);pi-i1;//*(pi+dIi1);Ii;i.c1;//*(&i+IdIC+dCc1);//*(&i+IdIc1);9-13主题讨论-对象模型5.虚继承I继承自G和H,G和H的虚基类是C,pi是指向I对象的指针访问虚基类C的成员c1时,IdGvbptr是“在I中,I对象指针与G的“虚基类表指针”之间的偏移”*(pi+dIGvbptr)是虚基类表的开始地址*(pi+dIGvbptr)[1]是虚基类表的第二项的内容(在I对象中,G对象的“虚基类表指针”与虚基类之间的偏移),dCc1是C对象指针与成员变量c1之间的偏移访问非虚基类G的成员g1时,直接计算偏移量;访问非虚基类H的成员h1时,直接计算偏移量;访问自身成员i1时,直接使用偏移量;当声明了一个对象实例,用点“.”操作符访问虚基类成员c1时,由于编译时就完全知道对象的布局情况,所以可以直接计算偏移量。9-14VC++虚继承下内存布局的结论首先排列非虚继承的基类实例;有虚基类时,为每个基类增加一个隐藏的vbptr,除非已经从非虚继承的类那里继承了一个vbptr;排列派生类的新数据成员;在实例最后,排列每个虚基类的一个实例。该布局安排使得虚基类的位置随着派生类的不同而“浮动不定”,但是,非虚基类因此也就凑在一起,彼此的偏移量固定不变。9-15虚成员函数表和运行期绑定vtable:支持运行时查询。系统可以将某一函数名绑定到虚函数表中的特定入口地址。classB{public:virtualvoidm1();virtualvoidm2();};voidD:publicB{//…}{public:virtualvoidm1();};虚成员函数入口地址示例虚成员函数入口地址示例B::m10x7723D::m10x99a7B::m20x23b4D::m20x23b49-16面向对象程序设计小结类和对象在C++中有类的成员包括:两种数据成员:static和nonstatic三种成员函数static、nonstatic和virtual。C++对象模型对内存空间和存取时间做了优化。nonstatic的数据成员被置于类对象之内,而static数据成员被置于类对象之外。static和nonstatic成员函数被放在类对象之外。而virtual函数是由类对象的一个指向vtbl(虚函数表)的指针vptr来进行支持。而vptr的设定和重置由类的构造函数、析构函数以及copyassignment运算符自动完成。9-17面向对象程序设计小结继承和派生-动态约束fooF,*q;barB,*s;q=&F;q-m();q=&B;q-m();9-18面向对象程序设计小结继承和派生-向下强制fooF,*q;barB,*s;问题:s=q?如果q指向的是基类的对象,赋值是无意义的;*q没有bar类的新的数据成员和虚表地址。如果q指向的是派生的对象,赋值是有意义的;bar*s=dynamic_castbar*(q);运行时类型识辨(RunTimeTypeIdentification,RTTI)类对象中有类的标识符9-33C++对象模型说明C++中对象的内存布局:C++有两种类成员数据类型:static,nonstatic有三种类成员函数类型:static,nonstatic,virtual一个classobject需要的内存包括以下三部分:1.其nonstaticdatamembers的总和大小;2.加上任何由于alignment(边界调整)的需求而填补上去的空间;3.加上为了支持virtual而由内部产生的任何额外负担。C++在布局以及存取时间上的主要的额外负担是有virtual引起的,包括:1.virtualfunction机制--用以支持一个有效率的“执行期绑定”。2.virtualbaseclass--用以实现“多次出现在继承体系中的baseclass,有一个单一而被共享的实体”。(virtual就是共享的意思)