ImageVerifierCode 换一换
格式:DOC , 页数:18 ,大小:97.50KB ,
资源ID:3498388      下载积分:20 文钱
快捷下载
登录下载
邮箱/手机:
温馨提示:
快捷下载时,用户名和密码都是您填写的邮箱或者手机号,方便查询和重复下载(系统自动生成)。 如填写123,账号就是123,密码也是123。
特别说明:
请自助下载,系统不会自动发送文件的哦; 如果您已付费,想二次下载,请登录后访问:我的下载记录
支付方式: 支付宝    微信支付   
验证码:   换一换

加入VIP,省得不是一点点
 

温馨提示:由于个人手机设置不同,如果发现不能下载,请复制以下地址【https://www.wenke99.com/d-3498388.html】到电脑端继续下载(重复下载不扣费)。

已注册用户请登录:
账号:
密码:
验证码:   换一换
  忘记密码?
三方登录: QQ登录   微博登录 

下载须知

1: 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。
2: 试题试卷类文档,如果标题没有明确说明有答案则都视为没有答案,请知晓。
3: 文件的所有权益归上传用户所有。
4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
5. 本站仅提供交流平台,并不能对任何下载内容负责。
6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

版权提示 | 免责声明

本文(谈VC++对象模型.doc)为本站会员(hw****26)主动上传,文客久久仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。 若此文所含内容侵犯了您的版权或隐私,请立即通知文客久久(发送邮件至hr@wenke99.com或直接QQ联系客服),我们立即给予删除!

谈VC++对象模型.doc

1、VC+,掀起你的盖头来谈 VC+对象模型(美)简 格雷程化 译译者前言一个 C+程序员,想要进一步提升技术水平的话,应该多了解一些语言的语意细节。对于使用 VC+的程序员来说,还应该了解一些 VC+对于 C+的诠释。Inside the C+ Object Model 虽然是一本好书,然而,书的篇幅多一些,又和具体的 VC+关系小一些。因此,从篇幅和内容来看,译者认为本文是深入理解 C+对象模型比较好的一个出发点。这篇文章以前看到时就觉得很好,旧文重读,感觉理解得更多一些了,于是产生了翻译出来,与大家共享的想法。虽然文章不长,但时间有限,又若干次在翻译时打盹睡着,拖拖拉拉用了小一个月。一方面因

2、本人水平所限,另一方面因翻译时经常打盹,错误之处恐怕不少,欢迎大家批评指正。1 前言了解你所使用的编程语言究竟是如何实现的,对于 C+程序员可能特别有意义。首先,它可以去除我们对于所使用语言的神秘感,使我们不至于对于编译器干的活感到完全不可思议;尤其重要的是,它使我们在 Debug 和使用语言高级特性的时候,有更多的把握。当需要提高代码效率的时候,这些知识也能够很好地帮助我们。本文着重回答这样一些问题: * 类如何布局? * 成员变量如何访问? * 成员函数如何访问? * 所谓的“调整块”(adjuster thunk)是怎么回事? * 使用如下机制时,开销如何: * 单继承、多重继承、虚继承

3、 * 虚函数调用 * 强制转换到基类,或者强制转换到虚基类 * 异常处理首先,我们顺次考察 C 兼容的结构(struct)的布局,单继承,多重继承,以及虚继承;接着,我们讲成员变量和成员函数的访问,当然,这里面包含虚函数的情况;再接下来,我们考察构造函数,析构函数,以及特殊的赋值操作符成员函数是如何工作的,数组是如何动态构造和销毁的;最后,简单地介绍对异常处理的支持。对每个语言特性,我们将简要介绍该特性背后的动机,该特性自身的语意(当然,本文决不是“C+入门” ,大家对此要有充分认识) ,以及该特性在微软的 VC+中是如何实现的。这里要注意区分抽象的 C+语言语意与其特定实现。微软之外的其他

4、C+厂商可能提供一个完全不同的实现,我们偶尔也会将 VC+的实现与其他实现进行比较。2 类布局本节讨论不同的继承方式造成的不同内存布局。2.1 C 结构(struct)由于 C+基于 C,所以 C+也“基本上”兼容 C。特别地,C+规范在“结构”上使用了和 C 相同的,简单的内存布局原则:成员变量按其被声明的顺序排列,按具体实现所规定的对齐原则在内存地址上对齐。所有的 C/C+厂商都保证他们的 C/C+编译器对于有效的 C 结构采用完全相同的布局。这里,A 是一个简单的 C 结构,其成员布局和对齐方式都一目了然。struct A char c;int i;译者注:从上图可见,A 在内存中占有

5、8个字节,按照声明成员的顺序,前4 个字节包含一个字符(实际占用1个字节,3个字节空着,补对齐) ,后4个字节包含一个整数。A 的指针就指向字符开始字节处。2.2 有 C+特征的 C 结构当然了,C+不是复杂的 C,C+本质上是面向对象的语言:包含继承、封装,以及多态。原始的 C 结构经过改造,成了面向对象世界的基石类。除了成员变量外,C+类还可以封装成员函数和其他东西。然而,有趣的是,除非为了实现虚函数和虚继承引入的隐藏成员变量外,C+类实例的大小完全取决于一个类及其基类的成员变量!成员函数基本上不影响类实例的大小。这里提供的 B 是一个 C 结构,然而,该结构有一些 C+特征:控制成员可见

6、性的“public/protected/private”关键字、成员函数、静态成员,以及嵌套的类型声明。虽然看着琳琅满目,实际上只有成员变量才占用类实例的空间。要注意的是,C+标准委员会不限制由“public/protected/private” 关键字分开的各段在实现时的先后顺序,因此,不同的编译器实现的内存布局可能并不相同。 (在 VC+中,成员变量总是按照声明时的顺序排列) 。struct B public:int bm1;protected:int bm2;private:int bm3;static int bsm;void bf();static void bsf();typede

7、f void* bpv;struct N ;译者注:B 中,为何 static int bsm 不占用内存空间?因为它是静态成员,该数据存放在程序的数据段中,不在类实例中。2.3 单继承C+提供继承的目的是在不同的类型之间提取共性。比如,科学家对物种进行分类,从而有种、属、纲等说法。有了这种层次结构,我们才可能将某些具备特定性质的东西归入到最合适的分类层次上,如“怀孩子的是哺乳动物”。由于这些属性可以被子类继承,所以,我们只要知道“ 鲸鱼、人” 是哺乳动物,就可以方便地指出“鲸鱼、人都可以怀孩子”。那些特例,如鸭嘴兽(生蛋的哺乳动物) ,则要求我们对缺省的属性或行为进行覆盖。C+中的继承语法很

8、简单,在子类后加上“:base”就可以了。下面的 D 继承自基类 C。struct C int c1;void cf();struct D : C int d1;void df();既然派生类要保留基类的所有属性和行为,自然地,每个派生类的实例都包含了一份完整的基类实例数据。在 D 中,并不是说基类 C 的数据一定要放在 D 的数据之前,只不过这样放的话,能够保证 D 中的 C 对象地址,恰好是 D 对象地址的第一个字节。这种安排之下,有了派生类 D 的指针,要获得基类 C 的指针,就不必要计算偏移量了。几乎所有知名的 C+厂商都采用这种内存安排。在单继承类层次下,每一个新的派生类都简单地把自

9、己的成员变量添加到基类的成员变量之后。看看上图,C 对象指针和 D 对象指针指向同一地址。2.4 多重继承大多数情况下,其实单继承就足够了。但是,C+为了我们的方便,还提供了多重继承。比如,我们有一个组织模型,其中有经理类(分任务) ,工人类(干活) 。那么,对于一线经理类,即既要从上级经理那里领取任务干活,又要向下级工人分任务的角色来说,如何在类层次中表达呢?单继承在此就有点力不胜任。我们可以安排经理类先继承工人类,一线经理类再继承经理类,但这种层次结构错误地让经理类继承了工人类的属性和行为。反之亦然。当然,一线经理类也可以仅仅从一个类(经理类或工人类)继承,或者一个都不继承,重新声明一个或

10、两个接口,但这样的实现弊处太多:多态不可能了;未能重用现有的接口;最严重的是,当接口变化时,必须多处维护。最合理的情况似乎是一线经理从两个地方继承属性和行为经理类、工人类。C+就允许用多重继承来解决这样的问题:struct Manager . . ;struct Worker . . ;struct MiddleManager : Manager, Worker . ;这样的继承将造成怎样的类布局呢?下面我们还是用“字母类”来举例:struct E int e1;void ef();struct F : C, E int f1;void ff();结构 F 从 C 和 E 多重继承得来。与单继

11、承相同的是,F 实例拷贝了每个基类的所有数据。与单继承不同的是,在多重继承下,内嵌的两个基类的对象指针不可能全都与派生类对象指针相同:F f;/ (void*)/ (void*) / *(pc + dCc1);译者注:pc 是指向 C 的指针。 访问 C 的成员变量 c1,只需要在 pc 上加上固定的偏移量 dCc1(在 C 中,C 指针地址与其 c1成员变量之间的偏移量值) ,再获取该指针的内容即可。单继承。由于派生类实例与其基类实例之间的偏移量是常数0,所以,可以直接利用基类指针和基类成员之间的偏移量关系,如此计算得以简化。D* pd;pd-c1; / *(pd + dDC + dCc1)

12、; / *(pd + dDc1);pd-d1; / *(pd + dDd1);译者注:D 从 C 单继承,pd 为指向 D 的指针。 当访问基类成员 c1时,计算步骤本来应该为“pd+dDC+dCc1” ,即为先计算 D 对象和C 对象之间的偏移,再在此基础上加上 C 对象指针与成员变量 c1之间的偏移量。然而,由于 dDC 恒定为 0,所以直接计算 C 对象地址与 c1之间的偏移就可以了。 当访问派生类成员 d1时,直接计算偏移量。多重继承。虽然派生类与某个基类之间的偏移量可能不为0,然而,该偏移量总是一个常数。只要是个常数,访问成员变量,计算成员变量偏移时的计算就可以被简化。可见即使对于多

13、重继承来说,访问成员变量开销仍然不大。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时,直接计算偏移量。虚

14、继承。当类有虚基类时,访问非虚基类的成员仍然是计算固定偏移量的问题。然而,访问虚基类的成员变量,开销就增大了,因为必须经过如下步骤才能获得成员变量的地址:获取“虚基类表指针” ;获取虚基类表中某一表项的内容;把内容中指出的偏移量加到“虚基类表指针”的地址上。然而,事情并非永远如此。正如下面访问 I 对象的 c1成员那样,如果不是通过指针访问,而是直接通过对象实例,则派生类的布局可以在编译期间静态获得,偏移量也可以在编译时计算,因此也就不必要根据虚基类表的表项来间接计算了。I* pi;pi-c1; / *(pi + dIGvbptr + (*(pi+dIGvbptr)1 + dCc1);pi-g

15、1; / *(pi + dIG + dGg1); / *(pi + dIg1);pi-h1; / *(pi + dIH + dHh1); / *(pi + dIh1);pi-i1; / *(pi + dIi1);I i;i.c1; / *( / *(译者注:I 继承自 G 和 H,G 和 H 的虚基类是 C,pi 是指向 I 对象的指针。 访问虚基类 C 的成员 c1时, dIGvbptr 是“在 I 中,I 对象指针与 G 的“虚基类表指针”之间的偏移”, *(pi + dIGvbptr)是虚基类表的开始地址,*(pi + dIGvbptr)1是虚基类表的第二项的内容(在 I 对象中,G 对

16、象的“ 虚基类表指针”与虚基类之间的偏移) ,dCc1是 C 对象指针与成员变量 c1之间的偏移; 访问非虚基类 G 的成员 g1时,直接计算偏移量; 访问非虚基类 H 的成员 h1时,直接计算偏移量; 访问自身成员 i1时,直接使用偏移量; 当声明了一个对象实例,用点 “.”操作符访问虚基类成员 c1时,由于编译时就完全知道对象的布局情况,所以可以直接计算偏移量。当访问类继承层次中,多层虚基类的成员变量时,情况又如何呢?比如,访问虚基类的虚基类的成员变量时?一些实现方式为:保存一个指向直接虚基类的指针,然后就可以从直接虚基类找到它的虚基类,逐级上推。VC+优化了这个过程。VC+在虚基类表中增

17、加了一些额外的项,这些项保存了从派生类到其各层虚基类的偏移量。4 强制转化如果没有虚基类的问题,将一个指针强制转化为另一个类型的指针代价并不高昂。如果在要求转化的两个指针之间有“基类- 派生类”关系,编译器只需要简单地在两者之间加上或者减去一个偏移量即可(并且该量还往往为0) 。F* pf;(C*)pf; / (C*)(pf ? pf + dFC : 0); / (C*)pf;(E*)pf; / (E*)(pf ? pf + dFE : 0);C 和 E 是 F 的基类,将 F 的指针 pf 转化为 C*或 E*,只需要将 pf 加上一个相应的偏移量。转化为 C 类型指针 C*时,不需要计算,

18、因为 F 和 C 之间的偏移量为0 。转化为 E类型指针 E*时,必须在指针上加一个非0的偏移常量 dFE。C+规范要求 NULL 指针在强制转化后依然为 NULL,因此在做强制转化需要的运算之前,VC+会检查指针是否为NULL。当然,这个检查只有当指针被显示或者隐式转化为相关类型指针时才进行;当在派生类对象中调用基类的方法,从而派生类指针被在后台转化为一个基类的 Const “this” 指针时,这个检查就不需要进行了,因为在此时,该指针一定不为 NULL。正如你猜想的,当继承关系中存在虚基类时,强制转化的开销会比较大。具体说来,和访问虚基类成员变量的开销相当。I* pi;(G*)pi; /

19、 (G*)pi;(H*)pi; / (H*)(pi ? pi + dIH : 0);(C*)pi; / (C*)(pi ? (pi+dIGvbptr + (*(pi+dIGvbptr)1) : 0);译者注:pi 是指向 I 对象的指针, G,H 是 I 的基类,C 是 G,H 的虚基类。 强制转化 pi 为 G*时,由于 G*和 I*的地址相同,不需要计算; 强制转化 pi 为 H*时,只需要考虑一个常量偏移; 强制转化 pi 为 C*时,所作的计算和访问虚基类成员变量的开销相同,首先得到 G 的虚基类表指针,再从虚基类表的第二项中取出 G 到虚基类 C 的偏移量,最后根据 pi、虚基类表偏

20、移和虚基类 C 与虚基类表指针之间的偏移计算出 C*。一般说来,当从派生类中访问虚基类成员时,应该先强制转化派生类指针为虚基类指针,然后一直使用虚基类指针来访问虚基类成员变量。这样做,可以避免每次都要计算虚基类地址的开销。见下例。/* before: */ . pi-c1 . pi-c1 ./* faster: */ C* pc = pi; . pc-c1 . pc-c1 .译者注:前者一直使用派生类指针 pi,故每次访问 c1都有计算虚基类地址的较大开销;后者先将 pi 转化为虚基类指针 pc,故后续调用可以省去计算虚基类地址的开销。5 成员函数一个 C+成员函数只是类范围内的又一个成员。X

21、 类每一个非静态的成员函数都会接受一个特殊的隐藏参数this 指针,类型为 X* const。该指针在后台初始化为指向成员函数工作于其上的对象。同样,在成员函数体内,成员变量的访问是通过在后台计算与this 指针的偏移来进行。struct P int p1;void pf(); / newvirtual void pvf(); / new;P 有一个非虚成员函数 pf(),以及一个虚成员函数 pvf()。很明显,虚成员函数造成对象实例占用更多内存空间,因为虚成员函数需要虚函数表指针。这一点以后还会谈到。这里要特别指出的是,声明非虚成员函数不会造成任何对象实例的内存开销。现在,考虑P:pf()的

22、定义。void P:pf() / void P:pf(P *const this)+p1; / +(this-p1);这里 P:pf()接受了一个隐藏的 this 指针参数,对于每个成员函数调用,编译器都会自动加上这个参数。同时,注意成员变量访问也许比看起来要代价高昂一些,因为成员变量访问通过 this 指针进行,在有的继承层次下,this 指针需要调整,所以访问的开销可能会比较大。然而,从另一方面来说,编译器通常会把 this 指针缓存到寄存器中,所以,成员变量访问的代价不会比访问局部变量的效率更差。译者注:访问局部变量,需要到 SP 寄存器中得到栈指针,再加上局部变量与栈顶的偏移。在没有虚

23、基类的情况下,如果编译器把 this 指针缓存到了寄存器中,访问成员变量的过程将与访问局部变量的开销相似。5.1 覆盖成员函数和成员变量一样,成员函数也会被继承。与成员变量不同的是,通过在派生类中重新定义基类函数,一个派生类可以覆盖,或者说替换掉基类的函数定义。覆盖是静态(根据成员函数的静态类型在编译时决定)还是动态(通过对象指针在运行时动态决定) ,依赖于成员函数是否被声明为“虚函数” 。Q 从 P 继承了成员变量和成员函数。Q 声明了 pf(),覆盖了 P:pf()。Q 还声明了pvf(),覆盖了 P:pvf()虚函数。Q 还声明了新的非虚成员函数 qf(),以及新的虚成员函数 qvf()。struct Q : P

Copyright © 2018-2021 Wenke99.com All rights reserved

工信部备案号浙ICP备20026746号-2  

公安局备案号:浙公网安备33038302330469号

本站为C2C交文档易平台,即用户上传的文档直接卖给下载用户,本站只是网络服务中间平台,所有原创文档下载所得归上传人所有,若您发现上传作品侵犯了您的权利,请立刻联系网站客服并提供证据,平台将在3个工作日内予以改正。