VC++对象模型.doc

上传人:hw****26 文档编号:3220084 上传时间:2019-05-26 格式:DOC 页数:18 大小:97.50KB
下载 相关 举报
VC++对象模型.doc_第1页
第1页 / 共18页
VC++对象模型.doc_第2页
第2页 / 共18页
VC++对象模型.doc_第3页
第3页 / 共18页
VC++对象模型.doc_第4页
第4页 / 共18页
VC++对象模型.doc_第5页
第5页 / 共18页
点击查看更多>>
资源描述

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个工作日内予以改正。