C++类,继承,多态的深入分析

整理文档很辛苦,赏杯茶钱您下走!

免费阅读已结束,点击下载阅读编辑剩下 ...

阅读已结束,您可以下载文档离线阅读编辑

资源描述

1C++对象模型在内存中的实现--基于微软VC++分析(VisualStudio2010)jhanker(蒋李军)jhanker@163.com2016-04-26相关C++对象模型深入了解的文章在互联网上有很多版本。要么排版不清晰,要么缺少图示,严重的影响阅读效果!现对每一段代码的图示都增加了VC++2010环境下编译器输出的类的对象模型图(文中黑色背景的图),通过对网上一些有用的相关资料的整合,让读者能更加直观理解其本质。本文的篇幅较长,但还是希望您能慢慢的品读,如果有不理解的地方可以先看后面的附录,阅读的过程中可以边阅读边调试附录中的调试代码,相信通过仔细的调试,分析,理解,您将会对C++有更深层次的理解!一个C++程序员,想要进一步提升技术水平的话,应该多了解一些语言的细节。对于使用VC++的程序员来说,还应该了解一些VC++对于C++的诠释。本文是深入理解C++对象模型比较好的一个出发点。了解你所使用的编程语言究竟是如何实现的,对于C++程序员可能特别有意义。首先,它可以去除我们对于所使用语言的神秘感,使我们不至于对于编译器干的活感到完全不可思议;尤其重要的是,它使我们在Debug和使用语言高级特性的时候,有更多的把握。当需要提高代码效率的时候,这些知识也能够很好地帮助我们。2对每个语言特性,我们将简要介绍该特性背后的动机,当然,本文决不是”C++入门”,大家对此要有充分认识,以及该特性在微软的VC++中是如何实现的。这里要注意区分抽象的C++语言与其特定实现。微软之外的其他C++厂商可能提供一个完全不同的实现,我们偶尔也会将VC++的实现与其他实现进行比较。首先,我们顺次考察类,单继承,多重继承,以及虚继承的布局;接着,我们讲成员变量和成员函数的访问已经访问时的开销情况,当然,这里面包含虚函数的情况;再接下来,我们考察构造函数,析构函数,以及特殊的赋值操作符成员函数是如何工作的,数组是如何动态构造和销毁的;最后,简单地介绍对异常处理的支持。1、类(class)布局本节讨论不同的继承方式造成的不同内存布局。1.1类的存储结构由于C++基于C,所以C++也”基本上”兼容C。特别地,C++规范在”类”上使用了和C”结构”相同的,简单的内存布局原则:成员变量按其被声明的顺序排列,按具体实现所规定的对齐原则在内存地址上对齐。所有的C/C++厂商都保证他们的C/C++编译器对于有效的C结构3采用完全相同的布局。这里,A是一个简单的类,其成员布局和对齐方式都一目了然1classA{2public:3charc;4inti;5};(图1)从上图(左)可见,A在内存中占有8个字节,按照声明成员的顺序,前4个字节包含一个字符(实际占用1个字节,3个字节空着,补对齐),后4个字节包含一个整数。A的指针就指向字符开始字节处。上图(右)为VisualStudio2010编译后在输出窗口中显示的内存分布情况。(项目—属性—配置属性—C/C++—命令行—其他选项中添加选项”/d1reportAllClassLayout”。再次编译时候,编译器会输出所有定义类的对象模型。由于输出的信息过多,我们可以使用”Ctrl+F”查找命令,找到对象模型的输出。)需要说明的是右图的0,4是相对字符开始地址的偏移地址。当然了,C++不是复杂的C,C++本质上是面向对象的语言:包含继承、封装,以及多态。原始的C结构经过改造,成了面向对象世界的4基石——类。除了成员变量外,C++类还可以封装成员函数和其他东西。然而,有趣的是,除非为了实现虚函数和虚继承引入的隐藏成员变量外,C++类实例的大小完全取决于一个类及其基类的成员变量!成员函数基本上不影响类实例的大小。这里提供的B是有更多C++特征的类:控制成员可见性的”public/protected/private”关键字、成员函数、静态成员,以及嵌套的类型声明。虽然看着琳琅满目,实际上,只有成员变量才占用类实例的空间。有一点要注意的是,C++标准委员会不限制由”public/protected/private”关键字分开的各段在实现时的先后顺序,因此,不同的编译器实现的内存布局可能并不相同。(在VC++中,成员变量总是按照声明时的顺序排列)。1classB{2public:3intbm1;4protected:5intbm2;6private:7intbm3;8staticintbsm;9voidbf();10staticvoidbsf();11typedefvoid*bpv;12structN{};13};(图2)5B中,为何staticintbsm不占用内存空间?因为它是静态成员,该数据存放在程序的静态数据段中,不在类实例中。(相关内存区域划分知识,见附1.)1.2单继承C++提供继承的目的是在不同的类型之间提取共性。比如,科学家对物种进行分类,从而有种、属、纲等说法。有了这种层次结构,我们才可能将某些具备特定性质的东西归入到最合适的分类层次上,如”怀孩子的是哺乳动物”。由于这些属性可以被子类继承,所以,我们只要知道”鲸鱼、人”是哺乳动物,就可以方便地指出”鲸鱼、人都可以怀孩子”。那些特例,如鸭嘴兽(生蛋的哺乳动物),则要求我们对缺省的属性或行为进行覆盖。C++中的继承语法很简单,在子类后加上”:public基类名(base)”就可以了。(附2.C++之继承与派生)下面的D继承自基类C。(本文全部的调试代码见附4.)1classC{//类C2public:3intc1;//类C的成员变量4voidcf();//类C的成员函数5};(图3)61classD:publicC{//类D,继承类C2public:3intd1;//类D的成员变量4voiddf();//类D的成员函数5};(图4)既然派生类要保留基类的所有属性和行为,自然地,每个派生类的实例都包含了一份完整的基类实例数据。在D中,并不是说基类C的数据一定要放在D的数据之前,只不过这样放的话,能够保证D中的C对象地址,恰好是D对象地址的第一个字节。这种安排之下,有了派生类D的指针,要获得基类C的指针,就不必要计算偏移量了。几乎所有知名的C++厂商都采用这种内存安排(基类成员在前)。在单继承类层次下,每一个新的派生类都简单地把自己的成员变量添加到基类的成员变量之后。看看上图,C对象指针和D对象指针指向同一地址。(上图(右)baseclassC,classD的框架结构的起始的上边界”+---”重合形象的说明C对象指针和D对象指针指向同一地址)1.3多重继承大多数情况下,其实单继承就足够了。但是,C++为了我们的方便,还提供了多重继承。7比如,我们有一个组织模型,其中有经理类(分任务),工人类(干活)。那么,对于一线经理类,即既要从上级经理那里领取任务干活,又要向下级工人分任务,这样的角色,如何在类层次中表达呢?单继承在此就有点力不从心。我们可以安排经理类先继承工人类,一线经理类再继承经理类,但这种层次结构错误地让经理类继承了工人类的属性和行为。反之亦然。当然,一线经理类也可以仅仅从一个类(经理类或工人类)继承,或者一个都不继承,重新声明一个或两个接口(函数),但这样的实现弊处太多:多态不可能了---未能重用现有的接口(函数);最严重的是,当接口(函数)变化时,必须多处维护。最合理的情况似乎是一线经理从两个地方继承属性和行为——经理类、工人类。C++就允许用多重继承来解决这样的问题:1classManager...{...};//经理类2classWorker...{...};//工人类3classMiddleManager:Manager,Worker{...};//一线经理类这样的继承将造成怎样的类布局呢?下面我们还是用”字母”类来举例:1classE{2public:3inte1;4voidef();5};(图5)81classF:publicC,publicE{2public:3intf1;4voidff();5};(图6)类F从C和E多重继承得来。与单继承相同的是,F实例拷贝了每个基类的所有数据。与单继承不同的是,在多重继承下,内嵌的两个基类的对象指针不可能全都与派生类对象指针相同:1Ff;2//(void*)&f==(void*)(C*)&f;//说明C对象指针与F对象指针相同3//(void*)&f(void*)(E*)&f;//说明E对象指针与F对象指针不同4//且基类E的地址比子类F的地址数值大观察类布局,可以看到F中内嵌的E对象,其指针与F指针并不相同。正如后文讨论强制转化和成员函数时指出的,这个偏移量会造成少量的调用开销。具体的编译器实现可以自由地选择内嵌基类和派生类的布局。VC++按照基类的声明顺序先排列基类实例数据,最后才排列派生类数据。当然,派生类数据本身也是按照声明顺序布局的(本规则并非一成不变,现在你可以不要在脑海中纠结排序的问题,因为阅读到后文我9们会看到,当一些基类有虚函数而另一些基类没有时,内存布局并非如此)。1.4虚继承回到我们讨论的一线经理类例子。让我们考虑这种情况:如果经理类和工人类都继承自”雇员类”,将会发生什么?1classEmployee{...};//雇员类2classManager:publicEmployee{...};//经理类3classWorker:publicEmployee{...};//工人类4classMiddleManager:publicManager,publicWorker{...};//一线经理类如果经理类和工人类都继承自雇员类,很自然地,它们每个类都会从雇员类获得一份数据拷贝。如果不作特殊处理,一线经理类的实例将含有两个雇员类实例,它们分别来自两个雇员基类。如果雇员类成员变量不多,问题不严重;如果雇员类成员变量众多,则那份多余的拷贝将造成实例生成时的严重开销。更糟的是,这两份不同的雇员实例可能分别被修改,造成数据的不一致。因此,我们需要让经理类和工人类进行特殊的声明,说明它们愿意共享一份雇员基类实例数据。很不幸,在C++中,这种”共享继承”被称为”虚继承”,把问题搞得似乎很抽象,但不要担心,其实虚继承的语法很简单,在指定基类时加上virtual关键字即可。把上面的继承关系改成虚继承,代码如下:1classEmployee{...};102classManager:virtualpublicEmployee{...};3classWorker:virtualpublicEmployee{...};4classMiddleManager:publicManager,publicWorker{...};使用虚继承,比起单继承和多重继承有更大的实现开销、调用开销。回忆一下,在单继承和多重继承的情况下,内嵌的基类实例地址比起派生类实例地址来,要么地址相同(单继承,以及多重继承的最靠左基的类),要么地址相差一个固定偏移量(多重继承的非最靠左的基类)。然而,当虚继承时,一般说来,派生类地址和其虚基类地址之间的偏移量是不固定的,因为如果这个派生类又被进一步继承的话,最终派生类会把共享的虚基类实例数据放到一个与上一层派生类不同的偏移量处。请看下面的实例,请大家仔细观察虚基类在派生类中的位置,至于其他的内容,如vbptr成员变量从何而来?什么作用?GdGvbptrG,GdGvbptrC,vbtable等是什么东东?干什么用的?为什么要有这样的设计?暂时都不要追究,后面会详细解释:1classG:virtualpublicC{2public:3intg1;4voidgf();5};11(图7)与上述的G一样建立一个H,也虚继承C,代码和内存分布见下图:1classH:virtualpublicC{2public:3inth1;4voidhf();5};(图8)接着建立一个结构体I,多重继承G和H,代码和内存分布见下图:1classI:publicG,publicH{122public:3inti1;4void_if();5};(图9)从上述图中可以直观地看到:在G对象中(图7),内嵌的C

1 / 77
下载文档,编辑使用

©2015-2020 m.777doc.com 三七文档.

备案号:鲁ICP备2024069028号-1 客服联系 QQ:2149211541

×
保存成功