




已阅读5页,还剩10页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
基类,派生类, 虚基类,虚函数表 ,内存分配和布局 Author: renkic声明:此文档大部分来自互联网,本人只是借用加以研究,以节省时间。但没有加链接,如有不妥,请告知, 谢谢。 1、 基本的类继承关系 内存分配时,是在于基类对象不同的内存地址处,按基类的成员变量类型,开辟一个同样的类型空间,但注意开辟后派生对象的空间,不是复制基类的成员的值,而是仅仅开辟那种成员类型的空间,未初始化时,里面存在的数是不确定的。1:派生类创建对象后调用了基类的构造函数,不是创建派生类对象的同时也创建了基类的对象。而是创建派生类对象时调用基类的构造函数对派生类中所继承自基类的部分初始化。2:派生类对象在内存中包含基类的私有变量,只是不能直接访问。3:从代码共享的角度看,组合类对象和派生类对象没有什么本质的不同,不同的是它们所表达的事物之间的逻辑关系:组合类对象一般表示“整体-部分”关系,而派生类对象一般表达“一般-具体”关系。 然后派生类自己定义的成员变量是排在继承的A类成员下面,如果派生类定义的变量名与基类相同,则此变量覆盖掉继承的基类同名变量,注意,覆盖不是删除,也 就是派生类中继承自基类的成员变量依然存在,而且值也不发生变化。如果想用此继承自基类的成员变量,则要加: , 在成员函数中访问时,直接用base:i,即可,用派生类的对象a访问时,如果此继承自基类的成员变量是对象可访问的(Public类型),则用 a.base:i访问之。#include stdafx.h#include using namespace std;class Apublic:long a;class B: public Apublic:long b;void set(A * d, int idx)didx.a = 2;int _tmain(int argc, _TCHAR* argv)B data4;A array3;for(int i=0; i4; i+)datai.a = 1;datai.b = 1;set(data, i);for(int i=0; i4; i+)coutdatai.adatai.b;coutendl; for(int i=0; i3; i+)arrayi.a = 5;coutarrayi.aendl;system(pause);return 0;Vs2008中的对象布局:可以看出, 类A的对象和类B的对象各自有自己的存储空间。基类对象在派生类中作为子对象形式存在。实际在内存中的存放形式:0x0012FEE2 cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc .0x0012FEF8 cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc 03 00 .0x0012FF0E 00 00 cc cc cc cc cc cc cc cc 04 00 00 00 cc cc cc cc cc cc cc cc .0x0012FF24 04 00 00 00 cc cc cc cc cc cc cc cc 05 00 00 00 05 00 00 00 05 00 .0x0012FF3A 00 00 cc cc cc cc cc cc cc cc 02 00 00 00 02 00 00 00 02 00 00 00 .0x0012FF50 02 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 cc cc .0x0012FF66 cc cc b8 ff 12 00 b8 1a 41 00 01 00 00 00 e8 5e 39 00 58 74 39 00 .A.9.Xt9.上图斜体加粗部分,最上边的三个数字时for循环开辟的i的空间, 下边三个5是A类的对象, 后边2,1都是B类的对象。又如:#include stdafx.h#include using namespace std;class basepublic: int i; int j;class sub:public base public: int i; int j; void p() coutbase:iendl; coutbase:jendl; ; int _tmain(int argc, _TCHAR* argv)base b;sub s;b.i=1;b.j=2;s.i=3;s.j=4;s.p(); / 输出两个不确定的数,coutb.iendl; / 1coutb.jendl; / 2couts.iendl; / 3couts.jendl; / 4couts.base:iendl;couts.base:jendl;system(pause);return 0;输出:从s.p()中,也可以看出,没有显示得调用base的成员变量(即s.base:i,s.base:j)进行赋值,那么这时,在子类中的类A的子对象的成员相当于没有赋初值,这样输出是不确定的值。这也说明了类A和类B不会共用继承过来的变量的存储空间。子类继承的基类的成员,只是在另一个内存空间内开辟一个这种类型的成员变量,它的值并不是基类的值,编译器只是负责把这一部分空间类型设置为与基类的类型相同。对象空间布局:2、虚基类的情形(注意:下面的例子如果不声明为虚基类,则会由于对象的二义性,即有多个基类Base的拷贝,导致编译错误。/ test.cpp : 定义控制台应用程序的入口点。/#include stdafx.h#include using namespace std;class Basepublic:int a;class Base1: virtual public Basepublic:int a1;class Base2: virtual public Basepublic:int a2;class Derived: public Base1, public Base2public:int d;int _tmain(int argc, _TCHAR* argv)Derived drd;drd.a = 1;drd.a1 = 2;drd.a2 = 3;drd.d = 4;Base b;b.a = 10;Base1 base1;base1.a = 11;base1.a1 = 11;Base2 base2;base2.a = 22;base2.a2 = 22;cout drd.a=%ddrd.a1drd.a1=%ddrd.a1drd.a2=%ddrd.a2drd.d=%ddrd.dendl;coutb.a=%d drd.Base1:a=%d drd.Base2:a=%d drd:Base1(Base2):Base:a=%d b.adrd.Base1:adrd.Base2:adrd.Base1:Base:aa;上面说过,dD中的int a不是继承自B的,也不是继承自C的,那么这个B中的pb-a又会怎么知道指向的是dD内存中的第六项呢?那就是指针vb_ptr的妙用了。原理如下:(其实g+3.4.3的实现更加复杂,我不知道是出于什么考虑,而我这里只说原理,所以把过程和内容简单化了)首先,vb_ptr指向一个整数的地址,里面放的整数是那个int a的距离dD开始处的位移(在这里vb_ptr指向的地址里面放的是20,以字节为单位)。编译器是这样做的:首先,找到vb_ptr(这个不用找,因为在g+中,vb_ptr就是B*中的第一项,呵呵),然后取得vb_ptr指向的地址的内容(这个例子是20),最后把这个内容与指针pb相加,就得到pb-a的地址了。所以说这种时候,用指针转换多了两个中间层才能找到基类的成员,而且是运行期间。由此也可以推知dD中的vb_ptr和vc_ptr的内容都是一样的,都是指向同一个地址,该地址就放20(在本例中)如下的语句呢:A *pa = &dD;pa-a = 4;这个语句不用转换了,因为编译器在编译期间就知道他把A中的成员插在dD中的那个地方了(在本例中是末尾),所以这个语句中的运行效率和dD.a是一样的(至少也是差不多的)这就是虚基类实现的基本原理。注意的是:那些指针的位置和基类成员在派生类成员中的内存布局是不确定的,也就是说标准里面没有规定int a必须要放在最后,只不过g+编译器的实现而已。c+标准大概只规定了这套机制的原理,至于具体的实现,比如各成员的排放顺序和优化,由各个编译器厂商自己定Sizeof#include stdafx.h#include using namespace std;struct A;class Bpublic:int g(void);struct Cstruct C *ptr;int _tmain(int argc, _TCHAR* argv)struct A a;B b;printf(sizeof(A)=%dn, sizeof(A); /1printf(sizeof(B)=%dn, sizeof(B); /1 printf(sizeof(C)=%dn, sizeof(C); / 4system(pause);return 0;实际上,sizeof计算对象的大小也是转换成对对象类型的计算,也就是说,同种类型的不同对象其sizeof值都是一致的。这里,对象可以进一步延伸至 表达式,即sizeof可以对一个表达式求值,编译器根据表达式的最终结果类型来确定大小,一般不会对表达式进行计算。如:sizeof( 2 );/ 2的类型为int,所以等价于 sizeof( int );sizeof( 2 + 3.14 ); / 3.14的类型为double,2也会被提升成double类型,所以等价于 sizeof( double );sizeof也可以对一个函数调用求值,其结果是函数返回类型的大小,函数并不会被调用,我们来看一个完整的例子:char foo()printf(foo() has been called.n);return a;int main()size_t sz = sizeof( foo() ); / foo() 的返回值类型为char,所以sz = sizeof(char ),foo()并不会被调用printf(sizeof( foo() ) = %dn, sz);C99标准规定,函数、不能确定类型的表达式以及位域(bit-field)成员不能被计算sizeof值,即下面这些写法都是错误 的:sizeof( foo );/ errorvoid foo2() sizeof( foo2() );/ errorstruct Sunsigned int f1 : 1;unsigned int f2 : 5;unsigned int f3 : 12;sizeof( S.f1 );/ error3. sizeof的常量性sizeof的计算发生在编译时刻,所以它可以被当作常量表达式使用,如:char ary sizeof( int ) * 10 ; / ok最新的C99标准规定sizeof也可以在运行时刻进行计算,如下面的程序在Dev-C+中可以正确执行:int n;n = 10; / n动态赋值char aryn; / C99也支持数组的动态定义printf(%dn, sizeof(ary); / ok. 输出10但在没有完全实现C99标准的编译器中就行不通了,上面的代码在VC6中就通不过编译。所以我们最好还是认为sizeof是在编译期执行的,这样 不会带来错误,让程序的可移植性强些。4. 基本数据类型的sizeof这里的基本数据类型指short、int、long、float、double这样的简单内置数据类型,由于它们都是和系统相 关的,所以在不同的系统下取值可能不同,这务必引起我们的注意,尽量不要在这方面给自己程序的移植造成麻烦。一般的,在32位编译环境 中,sizeof(int)的取值为4。5. 指针变量的sizeof学过数据结构的你应该知道指针是一个很重要的概念,它记录了另一个对象的地址。既然是来存放地址的,那么它当然等于计算机内部地址总线的宽度。所以在32位计算机中,一个指针变量的返回值必定是4(注意结果是以字节为单位),可以预计,在将来的64位系统中指针变量的sizeof结 果为8。char* pc = abc;int* pi;string* ps;char* ppc = &pc;void (*pf)();/ 函数指针sizeof( pc ); / 结果为4sizeof( pi ); / 结果为4sizeof( ps ); / 结果为4sizeof( ppc ); / 结果为4sizeof( pf );/ 结果为4指针变量的sizeof值与指针所指的对象没有任何关系,正是由于所有的指针变量所占内存大小相等,所以MFC消息处理函数使用两个参数 WPARAM、LPARAM就能传递各种复杂的消息结构(使用指向结构体的指针)。6. 数组的sizeof。数组的sizeof值等于数组所占用的内存字节数,如:char a1 = abc;int a23;sizeof( a1 ); / 结果为4,字符末尾还存在一个NULL终止符sizeof( a2 ); / 结果为3*4=12(依赖于int)一些朋友刚开始时把sizeof当作了求数组元素的个数,现在,你应该知道这是不对的,那么应该怎么求数组元素的个数 呢Easy,通常有下面两种写法:int c1 = sizeof( a1 ) / sizeof( char ); / 总长度/单个元素的长度int c2 = sizeof( a1 ) / sizeof( a10 ); / 总长度/第一个元素的长度写到这里,提一问,下面的c3,c4值应该是多少呢void foo3(char a33)int c3 = sizeof( a3 ); / c3 = void foo4(char a4)int c4 = sizeof( a4 ); / c4 = 也许当你试图回答c4的值时已经意识到c3答错了,是的,c3!=3。这里函数参数a3已不再是数组类型,而是蜕变成指针,相当于char* a3,为什么仔细想想就不难明白,我们调用函数foo1时,程序会在栈上分配一个大小为3的数组吗不会!数组是“传址”的,调用者只需将实参的地址传递过 去,所以a3自然为指针类型(char*),c3的值也就为4。7. 结构体的sizeof这是初学者问得最多的一个问题,所以这里有必要多费点笔墨。让我们先看一个结构体:struct S1char c;int i;问sizeof(s1)等于多少聪明的你开始思考了,char占1个字节,int占4个字节,那么加起来就应该是5。是这样吗你在你机器上试过了吗也许你是对的,但很可能你是错的!VC6中按默认设置得到的结果为8。Why为什么受伤的总是我请不要沮丧,我们来好好琢磨一下sizeof的定义 sizeof的结果等于对象或者类型所占的内存字节数,好吧,那就让我们来看看S1的内存分配情况:S1 s1 = a, 0xFFFFFFFF ;定义上面的变量后,加上断点,运行程序,观察s1所在的内存,你发现了什么以我的VC6.0为例,s1的地址为0x0012FF78,其数据内容如 下:0012FF78: 61 CC CC CC FF FF FF FF发现了什么怎么中间夹杂了3个字节的CC看看MSDN上的说明:When applied to a structure type or variable, sizeof returns the actual size, which may include padding bytes inserted for alignment.原来如此,这就是传说中的字节对齐啊!一个重要的话题出现了。为什么需要字节对齐计算机组成原理教导我们这样有助于加快计算机的取数 速度,否则就得多花指令周期了。为此,编译器默认会对结构体进行处理(实际上其它地方的数据变量也是如此),让宽度为2的基本数据类型(short等)都 位于能被2整除的地址上,让宽度为4的基本数据类型(int等)都位于能被4整除的地址上,以此类推。这样,两个数中间就可能需要加入填充字节,所以整个 结构体的sizeof值就增长了。让我们交换一下S1中char与int的位置:struct S2int i;char c;看看sizeof(S2)的结果为多少,怎么还是8再看看内存,原来成员c后面仍然有3个填充字节,这又是为什么啊别着急,下面总结规律:字节对齐的细节和编译器实现相关,但一般而言,满足三个准则:1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。对于上面的准则,有几点需要说明:1) 前面不是说结构体成员的地址是其大小的整数倍,怎么又说到偏移量了呢?因为有了第1点存在,所以我们就可以只考虑成员的偏移量,这样思考起来简单。想想为什 么。结构体某个成员相对于结构体首地址的偏移量可以通过宏offsetof()来获得,这个宏也在stddef.h中定义,如下:#define offsetof(s,m) (size_t)&(s *)0)-m)例如,想要获得S2中c的偏移量,方法为size_t pos = offsetof(S2, c);/ pos等于42) 基本类型是指前面提到的像char、short、int、float、double这样的内置数据类型,这里所说的“数据宽度”就是指其sizeof的大小。由于结构体的成员可以是复合类型,比如另外一个结构体,所以在寻找最宽基本类型成员时,应当包括复合类型成员的子成员,而不是把复合成员看成是一个整体。但在确定复合类型成员的偏移位置时则是将复合类型作为整体看待。这里叙述起来有点拗口,思考起来也有点挠头,还是让我们看看例子吧(具体数值仍以 VC6为例,以后不再说明):struct S3char c1;S1 s;char c2;S1的最宽简单成员的类型为int,S3在考虑最宽简单类型成员时是将S1“打散”看的,所以S3的最宽简单类型为int,这样,通过S3定义的 变量,其存储空间首地址需要被4整除,整个sizeof(S3)的值也应该被4整除。c1的偏移量为0,s的偏移量呢这时s是一个整体,它作为结构体变量 也满足前面三个准则,所以其大小为8,偏移量为4,c1与s之间便需要3个填充字节,而c2与s之间就不需要了,所以c2的偏移量为12,算上c2的大小 为13,13是不能被4整除的,这样末尾还得补上3个填充字节。最后得到sizeof(S3)的值为16。通过上面的叙述,我们可以得到一个公式:结构体 的大小等于最后一个成员的偏移量加上其大小再加上末尾的填充字节数目,即:sizeof( struct ) = offsetof( last item ) + sizeof( last item ) + sizeof( trailing padding )到这里,朋友们应该对结构体的sizeof有了一个全新的认识,但不要高兴得太早,有一个影响sizeof的重要参量还未被提及,那便是编译器的 pack指令。它是用来调整结构体对齐方式的,不同编译器名称和用法略有不同,VC6中通过#pragma pack实现,也可以直接修改/Zp编译开关。#pragma pack的基本用法为:#pragma pack( n ),n为字节对齐数,其取值为1、2、4、8、16,默认是8,如果这个值比结构体成员的sizeof值小,那么该成员的偏移量应该以此值为准,即是说, 结构体成员的偏移量应该取二者的最小值,公式如下:offsetof( item ) = min( n, sizeof( item ) )再看示例:#pragma pack(push) / 将当前pack设置压栈保存#pragma pack(2)/ 必须在结构体定义之前使用struct S1char c;int i;struct S3char c1;S1 s;char c2;#pragma pack(pop) / 恢复先前的pack设置计算sizeof(S1)时,min(2, sizeof(i)的值为2,所以i的偏移量为2,加上sizeof(i)等于6,能够被2整除,所以整个S1的大小为6。同样,对于 sizeof(S3),s的偏移量为2,c2的偏移量为8,加上sizeof(c2)等于9,不能被2整除,添加一个填充字节,所以sizeof(S3) 等于10。现在,朋友们可以轻松的出一口气了,:)还有一点要注意,“空结构体”(不含数据成员)的大小不为0,而是1。试想一个“不占空间”的变量如何被取地址、两个不同的“空结构体”变量又如何得以区分呢?于是,“空结构体”变量也得被存储,这样编译器也就只能为其分配一个字节的空间用于占位了。如 下:struct S5 ;
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 考试政策与试题解析执业医师考试试题及答案
- 数字经济创业项目跨境电商有限合伙人合作协议
- 海外留学生住宿家庭安全与教育服务合同
- 卫生政策与制度相关试题及答案
- 对外汉语综合课教案设计
- 2025年叉车工安全培训试题及答案
- 财产债务划分协议书
- 老婆拿着婚前协议书
- 不定式作定语的语法解析与设计应用
- 企业艺术培训学校员工技能提升计划
- 工程师评审代办合同协议
- (二模)2025年深圳市高三年级第二次调研考试物理试卷(含标准答案)
- 物品置换合同协议
- 心力衰竭试题及答案
- 公安治安管理培训
- 平面向量及其应用 章末题型归纳总结(基础篇)(10大题型)原卷版-2024-2025学年高一数学(人教A版必修第二册)
- 债权管理制度
- 运动营养学知到课后答案智慧树章节测试答案2025年春黑龙江冰雪体育职业学院
- 【基于改进杜邦分析法的中国东方航空公司财务分析(数据图表论文)13000字】
- 2025高级插花花艺师核心备考试题库及答案(浓缩300题)
- 光伏发电站施工规范完整版2025年
评论
0/150
提交评论