



免费预览已结束,剩余1页可下载查看
下载本文档
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
精品论文从内存布局深入剖析继承与多态匡翠芸 武汉科技大学计算机学院,湖北武汉 (430065) e-mail: 摘要:在面向对象程序设计中,使派生类继承基类的虚函数,再定义一个指向派生类对象的基类指针可以轻松实现动态绑定程,那么编译器是如何做到动态绑定以及基类的析构函数为 什么要申明为虚析构函数呢.本文避开普通的调试方法,通过精巧设计实例,安插指针来对面向对象的类的继承与多态的研究, 从内存的角度深入剖析了具有虚函数的基类和派生类对象的内存布局和结构组成,分析动态绑定的实现原理,从而在编写程序的时候出现不必要的错 误,使设计出来的类更合理,避免类拥肿.关键词:多态性;继承性;虚函数;虚函数表;虚拟指针;动态联编;内存布局1. 引言在 c+面向对象程序设计中,继承与多态是 oop 最重要的两个特性。继承的目的是复用, 继承复用包括两方面的复用:抽象(接口)复用和实现(过程)复用。多态的目的是要将抽象复 用及实现复用剥离开来。那么派生类是如何实现继承基类的属性和方法,虚函数是如何实现 多态?庖丁解牛的功力不在其刀的锋利,而在对牛筋骨脉络的理解。一般的研究方法是通过1- 5 -调试从汇编代码去分析,但有其不足:一是不懂汇编的人无法深入了解,二是不能从整体上把握对象继承与多态的内存布局。本文正是从内存布局作为切入点,精心设计 c+代码来 从整体上深入剖析这两大特性,使 oop 人员对继承与多态的筋骨脉络了如指掌,在实际程 序设计中做到心中有数,有的放矢。2. 设计 c+代码分析内存布局2.1 虚拟指针的存在class a1int a; class a2int a; virtual void f1() virtual f2() ; sizeof(a1) = 4; sizeof(a2) = 8;a1 的字节数为 4 在我们的意料之中,而在 a2 中我们增加了两个虚函数,这时的 字节数却为 8.原来编译器对每个包含虚函数的类创建一个虚表2(称为 vtable),此 vtable 由 类的所有对象所共用,同时编译器也会为这个类的每个类对象秘密地嵌入一个变量,即一个指 向自己虚表的指针称为虚拟指针(vptr)正是因为有了 vptr 的存在,所以后者的所占字节为8.2.2 设计实例由上面的分析可知,对于有虚函数的类在实例化(instance)时都有一个 vptr,那么,对象,虚 表的结构是由什么组成,结构如何?编译器是怎么实现动态联编?为此我们设计两个类 cbase 与 cderi,其中后者派生于前者,为了看清楚动态的真面目,我特意在 cbase 中定义两个虚函数, 其中只有一个在在派生类中重写.而在派生类中再加一个虚函数用来分析 vtable 的情况.具 体如下:class cbasepublic:cbase()nbasedata = 10;cbase()void function0()cout address=&nbasedata | cbase:nbasedata= nbasedataendl;virtual void vfunction1()cout | cbase:vfunction1endl; virtual void vfunction2()cout | cbase:vfunction2endl; private:int nbasedata;class cderi : public cbasepublic:cderi()nderidata = 20;cderi()void function0()cout address= &nderidata | cderi:nderidata= nderidataendl;virtual void vfunction2()cout | cderi:vfunction2endl; virtual void vfunction3()cout | cderi:vfunction3endl; private:int nderidata;类设计好了.要清楚 vtable 的结构,可以通过获取 vptr,因为 vptr 指 vtable,依上面的分析知道,在每个有虚函数的对象中编译器都会放个一个 vptr,.为此定义函数指针:typedef void (*pfun)(); pfun fun;并设计获取函数指针的函数 callmemberfunction()如下:void callmemberfunction(unsigned long *pmemberfunction, const int &noffset)fun = (pfun)(*(unsigned long *)(pmemberfunction + noffset); cout(noffset+1) (pmemberfunction + noffset)“fun; (*fun)(); 同时为了明晰对象的结构,为此设计函数 setmemberdata()用来设置成员变量的值,如下:void setmemberdata(unsigned long *pdatavalue, const int &noffset, const int &ndatavalue)int *pdata = (int *)(pdatavalue + noffset);*pdata = ndatavalue;最后设计主函数,在其中先实例化基类对象,然后实例派生类对象,目的在于对比基类与派生类在内存中的布局.如下:int main(int argv, char * argc )cbase *pbase = new cbase;unsigned long *vptr1 = (unsigned long *)(pbase);cout address= vptr1 | pointer of virtl funfunction0();for (int j = 0; j 2; j +) callmemberfunction(pvmemfun1, j);cbase *pderi = new cderi;unsigned long *vptr = (unsigned long *)(pderi);cout address= vptr | pointer of virtl funfunction0();(cderi *)pderi)-function0();for (int i = 0; i 3; i +) callmemberfunction(pvmemfun, i);delete pbase; delete pderi; return 0;2.3.深入分析(1)在主函数中先定义基类的对象指针 pbase,再进行强制类型转化为(unsigned long *)目 的是取 vptr,因为 vptr 指向 vtable,所以(*vptr)就是 vtable 的首地址3,然后取地址加偏 移量强制转化为函数指针类型,从下面图 1 输出内存结果可知,在对象中首地址中存放的是 vptr(地址:00a91bd8),后紧跟基类的成员变量(地址:00a91bdc).而在 vtable 中,按声明的 先后次序存放的是基类的虚函数地址,由此可画出对应在内存的分布如:图 2.图 1:基类的内存结果图 2:基类的对应内存结构(2)然后在主函数中定义派生类的对象指针 pderi,同样对其进行强制类型转换从而得到vptr, 再调用 setmemberdata() 去 修改基类与派 生类的私有 成员变 量 , 调用 callmemberfunction()去调用所有的虚函数,这个函数先输出 vtable 中每一项表的地址及所 存放的函数地址.从下图 3 输出内存结果可知,在对象中首地址同样是 vptr(地址: 00a90930), 其后是基类的成员变量(地址: 00a90934),然后才是派生类成员变量(地址: 00a90938).对于 vtable 而言, 在 vtable 的第一个地址中所指向的函数地址没有变(地址: 004011e5),那是因为 在派生类中没有重写 vfunction1,而在派生类中重写了 vfunction2 且在第二个地址所指向的 vfunction 的地址改变了(地址: 0040119a 变为地址: 004010a0),第三个是派生类新加的虚函 数地址.由此知在 vtable 中 ,放置了这个类中或是它的基类中所有虚函数的地址,这些虚函数 的顺序都是一样的,每个虚函数都在 vtable 中占了了个表项,保存着一条跳转到它的入口地 址的指令,所以通过偏移量可以容易地找到所需的函数体的地址。如果派生类没有重写虚函 数,那么 vtable 中保留基类虚函数的地址,如果重写了相应的虚函数,那么 vtable 中的地址 就会改变成指向派生类的虚函数地址。如果派生类有自己的虚函数,那么 vtable 中就会添加 该项。正是由于第二个地址里面的内容改变,从而实现动态联编技术.调用虚函数的时候,它先 根据 vtable 找到入口地址再执行,从而实现了”动态联编”.于是可画出对应内存分布如:图 4.图 3:派生类的内存结果图 4:派生类对应内存结构3. 结论(1) 如果该类没有虚拟函数,则不存在虚拟函数表.当一个类含有虚函数时,编译器就会为 这个类生成一个 vtable, 编译器另外还为每个类的对象提供了一个虚拟指针(vptr),这个指 针指向了对象所属类的虚表,并且这个 vptr 在对象的头部.(2)对象(含虚函数的类)是由 vptr 和成员变量组成,无论有多少个虚函数,只有一个 vptr, 与成员函数(如:function0)无关,因此对象的大小由一个指针的字节大小加上成员变量所占字 节的大小之和,有时候为了存取的效率,还要加上字节对齐(align).(3)虚函数表(vtable)的结构实质是数组,(如在基类中的地址为: 0047016c, 00470170)在 派生类中地址为 004701dc, 004701e0, 004701e4)而这个数组中所存储的是指向函数地址的 指针,即 vtable 的结构是函数指针数组的结构4.因此只要取到 vptr,然后采用偏移量很容易 实现函数的调用.(4)在派生类中,如果没有重写(override)基类的虚函数,那么在 vtable 中保留基类虚函数 的地址(如:vfunction1),如果派生类重写了,则在 vtable 中把基类对应虚函数地址用派生类虚 函数地址覆盖(如:vfunction2).如果在派生类中加入新的虚函数,则在 vtable 中后面加入对应 虚函数的地址(如:vfunciton3). 即:vtable 按照类中虚函数声明的顺序一一填入函数地址,派 生类会继承基类的 vtable(当然还会有其它可继承的成员),当在派生类中修改虚函数时,同 时派生类中虚表中的内容也随之被修改,表中相应的元素已经不是基类的函数地址,而是派生 类的函数地址,虚表可以继承,如果派生类没有重写虚函数,那么基类虚表中仍然会有该函数 的地址,只不过这个地址指向的是基类的虚函数实现.(5) 在对象或者在 vtable 的结构中, 基类的组成在前, 派生类在后( 如: 在 pderi 对象 中:nbasedata 在前,nderidata 在后,在 vtable 中,vfunction1,vfunction2 在前,而 vfunction3 在后)而在每个类中都是按声明的先后次序分配内存.(6)从上面的对象结构分析可知,普通成员函数在对象中不占内存, 调用时简单地跳转到 一个固定地址(编译时分配好);同样静态成员函数(这里没有举例)也不占,因为静态成员函数 只属于类,为所有对象共用.不属于某个特定的对象.(7)本例中利用 setmemberdata()函数实现了类体外对私有变量值的修改,这也提供了一个 修改私有变量的方法,访问效率较高,尤其对一些经过多重或多层继承得到的类对象的数据成 访问,使用该方法需要对类成员的布局十分清楚(尤其是有字节对齐时) ,否则很容易产生非法 操作,甚至导致系统崩溃,这种方法也说明了类封装性是不完全可靠,从此也可以得出,基类 的成员变量,派生类是可以继承的,只是在派生类中的成员函数跟对象都不能访问而已(编 译器不能通过)。4. 高效应用毫无疑问,有了继承性,使得代码重复利用率很高,明显可以缩短开发周期。但由上的 分析知道,派生类的对象中会继承基类中的所有成员变量,包括私有成员变量,这样会带来 空间开销。同样有了虚函数使类的设计更灵活也更强大,同样时间和空间的开销不可避免也 大了(不是直接跳转到编译时分配的固定地址,而是要先得到 vptr,然后经过偏移量取得虚函 数地址,然后再调用).所以在设计类的时候除非十分明确以后这个类会被继承且里面的函数 会被改写,才加关键字 virtual。因为如果继承的层次比较深,那么所有直接和间接基类的数 据成员,虚函数都会在派生类的对象中,虽然在对象中无论多少个 vf,只增加一个 vptr,但 vtable 会变得很庞大。所以在实际 oop 中,我们就可以做到心中有数,有的放矢。5. 结束语(1)以上结论的运行环境:系统为 windows xp 编译器为 visual studio c+ 6.0.对于其他的 编译器,具体实现并不完全相同,但都大同小异.(如 linux 平台上的 gnu c+编译器就把指向 vtable 的虚拟指针(vptr)放在对象尾部而不是头部,而且 vtable 中仅仅存放虚函数的入口 地址,而不是跳转到虚函数的指令.(2)以上征对仅仅是简单的单继承的情况,对于横向(如:多继承),纵向(如虚继承,更深层次 继承)等更复杂的情况,由于篇幅所限在此不加以深入,但剖析思想大同小异.参考文献1蓝雯飞.c+语言的多态性应用研究j 计算机时代 1998(19)62候捷译.深入探索 c+对象模型m武汉:华中科技大学出版社3张宇.淺谈多态中的虚拟指针(vptr)j 武汉市教育科学研究院学报 2006.11.4 4彭建盛.多态性在 c+面向对象程序设计中的实现j 河池学院学报 2004(20)4inside analysis the inheritance and polymorophism from the memory layoutkuang cui-yuncollege of computer science and technology wuhan university of science and technologywuhan (430065)abstractin oop, the derived class inherited the basic class that have a virtual function, and then define a basicpointer that point to the object of derive class can easily achieve dynamic bin
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- DB42-T 2039.1-2023 主要鲜切花采后处理技术规程 第1部分:非洲菊
- 《新疆石油地质》投稿须知
- 2023年人教版四年级语文上册期中检测卷及参考答案
- 山东省菏泽市2025年高三二模考试思想政治试题(含答案)
- 上海市宝山区海滨中学2025年下学期高三4月仿真化学试题试卷含解析
- 电子工程电路设计实践练习题库
- 山东女子学院《web渗透与漏洞挖掘课程设计》2023-2024学年第二学期期末试卷
- 数据分析与大数据处理技术题库
- 2023年经济学基础期末复习综合练习题及答案
- 北京版英语二年级下册《Lesson 23》教案下载
- 脊柱损伤搬运健康宣教
- 高考英语单词3500记忆短文40篇
- 厂内运输车辆专项安全检查表
- 企业商学院的组织架构和培训体系架构
- DL∕T 547-2020 电力系统光纤通信运行管理规程
- 切尔诺贝利核电站事故工程伦理分析
- (无线)门禁系统报价单
- 社会工作介入老年社区教育的探索
- 国开电大-工程数学(本)-工程数学第4次作业-形考答案
- 高考倒计时30天冲刺家长会课件
- 施工项目现金流预算管理培训课件
评论
0/150
提交评论