深入理解C++对象模型之Data Member存取成本

来源:互联网 发布:网络射击游戏 编辑:程序博客网 时间:2024/06/04 18:37

一、前言

    对于C++语言而言,不好理解的地方大多数由继承和多态这两个东西产生,比如说一个C++类型的内存布局,会因为其是继承而来的派生类或者独立类而不同,如果是派生类,由于继承的存在,它需要包含基类部分,因此其内存布局(模型)需要改变。同时,这里介绍的对一个Class的member的存取成本也与继承和多态有关?


二、问题与结论

1、问题描述

    如果有一个Class Base(包含x,y两个数据成员),定义如下代码:

Base Bobj;Bobj.x = 0.0;
第一个问题是对于上面的代码,x的存取成本是什么呢?如果又有下面的代码定义:

Base Bobj;Base *pB = &Bobj;Bobj.x = 0.0;   //(1)pB->x = 0.0;    //(2)

第二个问题是对于上面的代码,代码(1)和(2)的存取成本有什么差异?

2、结论

    这里先给出具体的结论,然后再做详细解释。对于第一个问题成员变量x的存取成本,你需要考虑到成员变量x的声明类型(static或non-static)、以及类型Base是继承而来,还是独立的Class,如果是继承,那具体是单一继承(没有多态,即没有虚函数)、单一继承(加上多态)、多重继承、还是虚拟继承呢?当然,对于第一个问题,以上列出的所有情况对成员x的存取成本都没有影响。

    对于第二个问题,两种形式的存取成本有没有差异呢?答案是有差异但是只在以上列出的情况中的一种情况下有差异,那就是虚拟继承体系下,如果成员变量x是从virtual base class继承而来(且是通过指针来访问),那么代码(2)的存取成本就会变慢,因为在编译阶段我们不能确定指针pB具体指向虚拟基类还是派生类,所以我们就不知道这个成员x在Class内存模型中的具体offset,所以这个存取操作必须要延迟到执行阶段才能确定(具体分析下面会给出)。


三、继承与Class Data Member布局

    以上结论也说明了:具体继承(相对于虚拟继承)并不会增加存取时间上的额外负担。下面将会主要针对4种情况对“继承对Class Data Member布局影响”进行分析,也正是因为虚拟继承对Class Data Member的影响才导致了上述第二个问题中代码(2)的差异,这4种情况分别为:单一继承(没有多态)、单一继承(有多态)、多重继承、虚拟继承。

1、单一继承(没有多态)

    首先要理解C++语言的类对象模型,及对象的内存布局, 在C++class定义中,有两种class data member:static和non-static,以及三种class member function:static、non-static和virtual。而在class的对象模型中却只包含non-static data member和虚函数表指针(如果是虚拟继承则需要另外讨论)。

    在单一继承且没有多态的情况下,那么派生类中也就不存在虚函数表指针了,即只有其自身和继承而来的data member,也就是说class object的内存模型布局在编译阶段已经确定,这时,如果是代码(1)来存取data member,其就是直接存取,如果通过代码(2)来存取,只需要计算data member在内存模型中的offset即可,也是直接存取。所以无论是通过代码(1)还是代码(2)的形式来存取数据成员,对存取时间都没有影响。

2、单一继承(有多态)

    当存在多态时,对于class object的内存模型只是多了一个虚函数表指针,这个指针具体放在内存模型的哪个位置(可以是基类的首端、也可以是末端,甚至中间)在C++标准中没有要求,这就与不同的编译器有关,不同编译器可以有不同的实现方法,但不管怎么实现,class object的内存模型布局同样在编译阶段已经确定,因此对data member的存取时间也没有影响。

3、多重继承

    在解释了前面两种情况之后,多重继承应该也就自然明白了,多重继承还是具体继承,不是虚拟继承,即是前两种情况的综合,所以其对data member的存取时间同样没有影响。

    对于多重继承,尽管其对data member的存取时间没有影响的,但是它对于继承体系中的类型转换有很大的影响,主要表现在:derived class object和其(派生列表中)第二或者后继base class object之间的转换会存在很多问题与陷阱(这需要各位同学去查阅相关资料)。

4、虚拟继承

    关于虚拟继承,即在派生列表中经由virtual关键字所指的基类,如果一个class继承于一个virtual base class,那么这个class内存模型被分为两个部分:不变部分和共享部分,其中共享部分指从虚拟基类派生而来的部分。所谓不变部分,就是其在class的内存模型中具有固定的offset(从object的开头算起),而共享部分在class内存模型中的位置会因为每次派生操作而有变化,因此,派生类的不变部分是可以直接存取的(根据其offset),而共享部分是不可以的。

    同时,我们也清楚,一个派生类其内存模型中也可以分为基类部分和派生类部分,因此综合不变部分和共享部分的分类方法,一个包含virtual base class的派生类的内存模型包含3个部分:non-virtual base class、virtual base class和派生类本身部分,这三个部分在内存模型中的逻辑顺序视不同的编译器而不同,一种主流的做法是根据派生列表,在class的内存模型中,从上到下(地址从小到大)依次是base1、base2……派生类本身、共享部分(virtual base class),如下所示,有如下继承关系

class Base{public:int x;};class Derive1:virtual public Base{public://含有虚函数public:int y;};class Derive2:virtual public Base{public://含有虚函数public:int z;};class Derive:public Derive1, Derive2{int m;};

则class Derive的内存模型可以表示为:


    可以看出,在这个模型中共享部分的位置是有class Derive1和class Derive2的虚函数表指针(vptr)所指出,其实关于怎么表示或指出共享部分的位置也有很多种不同,各种编译器的实现也不同,上图中的表示是一种流行的表示方法,可以解决其他一些方法的不足(在侯捷先生的《深度探索C++对象模型》中3.4节有详细的解释)。所以,在这种模型下,每次对共享部分数据的存储都需要经由派生类虚函数表指针进行间接操作,就会消耗更多的时间,当然这种时间消耗是建立在通过指针进行数据访问的情况下的,即Derive *pd的形式,因为在通过指针访问时,class的动态类型是未知的,所以无法在编译期间确定class内存模型中共享区域的成员变量的offset,导致对共享区域成员变量的访问操作要延迟到执行期间。而通过class object进行共享区域的访问是不需要延迟到执行期的,因为使用class object时,如Derive ObjD时,对象ObjD的静态类型和动态类型一致,即在编译器可以知道其内存模型中所有成员变量的offset,不管是不变部分还是共享部分,都是确定的,所以即使是对共享区域的变量进行存取操作也是进行直接存取的。

    现在再回过来看开头提出的两个问题和结论,各位同学就应该明白是什么情况了。


0 0